Add av filter components
This commit is contained in:
parent
9d7431a4bd
commit
ec90f6730f
@ -2,6 +2,10 @@
|
||||
|
||||
#### v1.1.0 > v1.2.0
|
||||
|
||||
- Add video rotation filter ([#347](https://github.com/datarhei/restreamer/discussions/347))
|
||||
- Add video h/v flip filter
|
||||
- Add audio volume filter ([#313](https://github.com/datarhei/restreamer/issues/313))
|
||||
- Add audio loudness normalization filter
|
||||
- Add stream distribution across multiple internal servers
|
||||
- Add SRT settings
|
||||
- Add HLS version selection (Dwaynarang, Electra Player compatibility)
|
||||
@ -9,6 +13,7 @@
|
||||
- Add Telegram to publication services (thx Martin Held)
|
||||
- Add Polish translations (thx Robert Rykała)
|
||||
- Mod Allow decoders and encoders to set global options
|
||||
- Fix player problem with different stream formats (9:16)
|
||||
- Fix process report naming
|
||||
- Fix publication service icon styles
|
||||
- Fix VAAPI encoder
|
||||
|
||||
@ -373,7 +373,7 @@
|
||||
<div class="col-xs-12 player-l2">
|
||||
<div class="player-l3">
|
||||
{{#ifEquals player "videojs"}}
|
||||
<video id="player" class="vjs-public video-js player-l4" playsinline></video>
|
||||
<video id="player" class="vjs-public video-js vjs-16-9 player-l4" playsinline></video>
|
||||
{{else}}
|
||||
<div id="player" class="player-l4"></div>
|
||||
{{/ifEquals}}
|
||||
|
||||
127
src/misc/FilterSelect.js
Normal file
127
src/misc/FilterSelect.js
Normal file
@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// Import all filters (audio/video)
|
||||
import * as Filters from './filters';
|
||||
|
||||
// Import all encoders (audio/video)
|
||||
import * as Encoders from './coders/Encoders';
|
||||
|
||||
export default function FilterSelect(props) {
|
||||
const profile = props.profile;
|
||||
|
||||
// handleFilterChange
|
||||
// what: Filter name
|
||||
// settings (component settings): {Key: Value}
|
||||
// mapping (FFmpeg -af/-vf args): ['String', ...]
|
||||
const handleFilterSettingsChange = (what) => (settings, mapping, automatic) => {
|
||||
const filter = profile.filter;
|
||||
|
||||
// Store mapping/settings per component
|
||||
filter.settings[what] = {
|
||||
mapping: mapping,
|
||||
settings: settings,
|
||||
};
|
||||
|
||||
// Combine FFmpeg args
|
||||
let settings_mapping = [];
|
||||
for (let i in filter.settings) {
|
||||
if (filter.settings[i].mapping.length !== 0) {
|
||||
settings_mapping.push(filter.settings[i].mapping);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the real filter mapping
|
||||
// ['-af/-vf', 'args,args']
|
||||
if (settings_mapping.length !== 0) {
|
||||
if (props.type === 'video') {
|
||||
filter.mapping = ['-vf', settings_mapping.join(',')];
|
||||
} else if (props.type === 'audio') {
|
||||
filter.mapping = ['-af', settings_mapping.join(',')];
|
||||
}
|
||||
} else {
|
||||
filter.mapping = [];
|
||||
}
|
||||
|
||||
props.onChange(profile.filter, filter, automatic);
|
||||
};
|
||||
|
||||
// Set filterRegistry by type
|
||||
let filterRegistry = null;
|
||||
if (props.type === 'video') {
|
||||
filterRegistry = Filters.Video;
|
||||
} else if (props.type === 'audio') {
|
||||
filterRegistry = Filters.Audio;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Checks the state of hwaccel (gpu encoding)
|
||||
let encoderRegistry = null;
|
||||
let hwaccel = false;
|
||||
if (props.type === 'video') {
|
||||
encoderRegistry = Encoders.Video;
|
||||
for (let i in encoderRegistry.List()) {
|
||||
if (encoderRegistry.List()[i].codec === props.videoProfile.encoder.coder && encoderRegistry.List()[i].hwaccel) {
|
||||
hwaccel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creates filter components
|
||||
let filterSettings = [];
|
||||
if (!hwaccel) {
|
||||
for (let c of filterRegistry.List()) {
|
||||
// Checks FFmpeg skills (filter is available)
|
||||
if (props.availableFilters.includes(c.filter)) {
|
||||
const Settings = c.component;
|
||||
|
||||
filterSettings.push(
|
||||
<Settings
|
||||
key={c.filter}
|
||||
settings={profile.filter.settings[c.filter] ? profile.filter.settings[c.filter].settings : []}
|
||||
onChange={handleFilterSettingsChange(c.filter)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No suitable filter found
|
||||
if (filterSettings === null && !hwaccel) {
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>
|
||||
<Trans>No suitable filter found.</Trans>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
// hwaccel requires further settings
|
||||
} else if (hwaccel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>
|
||||
<Trans>Select your filter settings (optional):</Trans>
|
||||
</Typography>
|
||||
</Grid>
|
||||
{filterSettings}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
FilterSelect.defaultProps = {
|
||||
type: '',
|
||||
filters: [],
|
||||
availableFilters: [],
|
||||
onChange: function (filter) {},
|
||||
};
|
||||
@ -31,6 +31,7 @@ export default function VideoJS(props) {
|
||||
player.addClass('vjs-internal');
|
||||
}
|
||||
player.addClass('video-js');
|
||||
player.addClass('vjs-16-9');
|
||||
} else {
|
||||
// you can update player here [update player through props]
|
||||
// const player = playerRef.current;
|
||||
|
||||
104
src/misc/filters/audio/Loudnorm.js
Normal file
104
src/misc/filters/audio/Loudnorm.js
Normal file
@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
import Checkbox from '../../Checkbox';
|
||||
|
||||
// Loudnorm Filter
|
||||
// http://ffmpeg.org/ffmpeg-all.html#loudnorm
|
||||
|
||||
function init(initialState) {
|
||||
const state = {
|
||||
value: false,
|
||||
...initialState,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
|
||||
if (settings.value) {
|
||||
mapping.push('loudnorm');
|
||||
}
|
||||
|
||||
console.log(mapping);
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function Loudness(props) {
|
||||
return <Checkbox label={<Trans>Loudness Normalization</Trans>} checked={props.value} onChange={props.onChange} />;
|
||||
}
|
||||
|
||||
Loudness.defaultProps = {
|
||||
value: '',
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function Filter(props) {
|
||||
const settings = init(props.settings);
|
||||
|
||||
const handleChange = (newSettings) => {
|
||||
let automatic = false;
|
||||
if (!newSettings) {
|
||||
newSettings = settings;
|
||||
automatic = true;
|
||||
}
|
||||
|
||||
props.onChange(newSettings, createMapping(newSettings), automatic);
|
||||
};
|
||||
|
||||
const update = (what) => (event) => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
};
|
||||
if (['value'].includes(what)) {
|
||||
newSettings[what] = !settings.value;
|
||||
} else {
|
||||
newSettings[what] = event.target.value;
|
||||
}
|
||||
|
||||
handleChange(newSettings);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleChange(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item>
|
||||
<Loudness value={settings.value} onChange={update('value')} allowCustom />
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Filter.defaultProps = {
|
||||
settings: {},
|
||||
onChange: function (settings, mapping) {},
|
||||
};
|
||||
|
||||
const filter = 'loudnorm';
|
||||
const name = 'Loudness Normalization';
|
||||
const type = 'audio';
|
||||
const hwaccel = false;
|
||||
|
||||
function summarize(settings) {
|
||||
return `${name}`;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
const settings = init({});
|
||||
|
||||
return {
|
||||
settings: settings,
|
||||
mapping: createMapping(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };
|
||||
146
src/misc/filters/audio/Volume.js
Normal file
146
src/misc/filters/audio/Volume.js
Normal file
@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
import Select from '../../Select';
|
||||
|
||||
// Volume Filter
|
||||
// http://ffmpeg.org/ffmpeg-all.html#volume
|
||||
|
||||
function init(initialState) {
|
||||
const state = {
|
||||
level: false,
|
||||
db: 0,
|
||||
...initialState,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
|
||||
if (settings.level) {
|
||||
if (settings.level !== 'custom') {
|
||||
mapping.push(`volume=volume=${settings.level}`);
|
||||
} else {
|
||||
mapping.push(`volume=volume=${settings.db}dB`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(mapping);
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function VolumeLevel(props) {
|
||||
return (
|
||||
<Select label={<Trans>Volume</Trans>} value={props.value} onChange={props.onChange}>
|
||||
<MenuItem value={false}>None</MenuItem>
|
||||
<MenuItem value={0.1}>10%</MenuItem>
|
||||
<MenuItem value={0.2}>20%</MenuItem>
|
||||
<MenuItem value={0.3}>30%</MenuItem>
|
||||
<MenuItem value={0.4}>40%</MenuItem>
|
||||
<MenuItem value={0.5}>50%</MenuItem>
|
||||
<MenuItem value={0.6}>60%</MenuItem>
|
||||
<MenuItem value={0.7}>70%</MenuItem>
|
||||
<MenuItem value={0.8}>80%</MenuItem>
|
||||
<MenuItem value={0.9}>90%</MenuItem>
|
||||
<MenuItem value={1.0}>100%</MenuItem>
|
||||
<MenuItem value="custom">Custom</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
VolumeLevel.defaultProps = {
|
||||
value: '',
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function VolumeDB(props) {
|
||||
console.log(props);
|
||||
return (
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
label={<Trans>Decibels (dB)</Trans>}
|
||||
type="number"
|
||||
value={props.value}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
VolumeDB.defaultProps = {
|
||||
value: '',
|
||||
disabled: false,
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function Filter(props) {
|
||||
const settings = init(props.settings);
|
||||
|
||||
const handleChange = (newSettings) => {
|
||||
let automatic = false;
|
||||
if (!newSettings) {
|
||||
newSettings = settings;
|
||||
automatic = true;
|
||||
}
|
||||
|
||||
props.onChange(newSettings, createMapping(newSettings), automatic);
|
||||
};
|
||||
|
||||
const update = (what) => (event) => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
[what]: event.target.value,
|
||||
};
|
||||
|
||||
handleChange(newSettings);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleChange(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={6}>
|
||||
<VolumeLevel value={settings.level} onChange={update('level')} allowCustom />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<VolumeDB value={settings.db} onChange={update('db')} disabled={settings.level !== 'custom'} allowCustom />
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Filter.defaultProps = {
|
||||
settings: {},
|
||||
onChange: function (settings, mapping) {},
|
||||
};
|
||||
|
||||
const filter = 'volume';
|
||||
const name = 'Volume level';
|
||||
const type = 'audio';
|
||||
const hwaccel = false;
|
||||
|
||||
function summarize(settings) {
|
||||
return `${name}`;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
const settings = init({});
|
||||
|
||||
return {
|
||||
settings: settings,
|
||||
mapping: createMapping(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };
|
||||
56
src/misc/filters/index.js
Normal file
56
src/misc/filters/index.js
Normal file
@ -0,0 +1,56 @@
|
||||
// Audio Filter
|
||||
import * as Volume from './audio/Volume';
|
||||
import * as Loudnorm from './audio/Loudnorm';
|
||||
|
||||
// Video Filter
|
||||
import * as Transpose from './video/Transpose';
|
||||
import * as HFlip from './video/HFlip';
|
||||
import * as VFlip from './video/VFlip';
|
||||
|
||||
// Registrate Filters by:
|
||||
// type: audio/video
|
||||
class Registry {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
this.services = new Map();
|
||||
}
|
||||
|
||||
Register(service) {
|
||||
if (service.type !== this.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.services.set(service.filter, service);
|
||||
}
|
||||
|
||||
Get(filter) {
|
||||
const service = this.services.get(filter);
|
||||
if (service) {
|
||||
return service;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Filters() {
|
||||
return Array.from(this.services.keys());
|
||||
}
|
||||
|
||||
List() {
|
||||
return Array.from(this.services.values());
|
||||
}
|
||||
}
|
||||
|
||||
// Audio Filters
|
||||
const audioRegistry = new Registry('audio');
|
||||
audioRegistry.Register(Volume);
|
||||
audioRegistry.Register(Loudnorm);
|
||||
|
||||
// Video Filters
|
||||
const videoRegistry = new Registry('video');
|
||||
videoRegistry.Register(Transpose);
|
||||
videoRegistry.Register(HFlip);
|
||||
videoRegistry.Register(VFlip);
|
||||
|
||||
// Export registrys for ../SelectFilters.js
|
||||
export { audioRegistry as Audio, videoRegistry as Video };
|
||||
102
src/misc/filters/video/HFlip.js
Normal file
102
src/misc/filters/video/HFlip.js
Normal file
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
import Checkbox from '../../Checkbox';
|
||||
|
||||
// HFlip Filter
|
||||
// http://ffmpeg.org/ffmpeg-all.html#hflip
|
||||
|
||||
function init(initialState) {
|
||||
const state = {
|
||||
value: false,
|
||||
...initialState,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
|
||||
if (settings.value) {
|
||||
mapping.push('hflip');
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function HFlip(props) {
|
||||
return (
|
||||
<Checkbox label={<Trans>Horizontal Flip</Trans>} checked={props.value} onChange={props.onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
HFlip.defaultProps = {
|
||||
value: '',
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function Filter(props) {
|
||||
const settings = init(props.settings);
|
||||
|
||||
const handleChange = (newSettings) => {
|
||||
let automatic = false;
|
||||
if (!newSettings) {
|
||||
newSettings = settings;
|
||||
automatic = true;
|
||||
}
|
||||
|
||||
props.onChange(newSettings, createMapping(newSettings), automatic);
|
||||
};
|
||||
|
||||
const update = (what) => (event) => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
};
|
||||
if (['value'].includes(what)) {
|
||||
newSettings[what] = !settings.value;
|
||||
} else {
|
||||
newSettings[what] = event.target.value;
|
||||
}
|
||||
|
||||
handleChange(newSettings);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleChange(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid item>
|
||||
<HFlip value={settings.value} onChange={update('value')} allowCustom />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
Filter.defaultProps = {
|
||||
settings: {},
|
||||
onChange: function (settings, mapping) {},
|
||||
};
|
||||
|
||||
const filter = 'hflip';
|
||||
const name = 'Horizonal Flip';
|
||||
const type = 'video';
|
||||
const hwaccel = false;
|
||||
|
||||
function summarize(settings) {
|
||||
return `${name}`;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
const settings = init({});
|
||||
|
||||
return {
|
||||
settings: settings,
|
||||
mapping: createMapping(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };
|
||||
109
src/misc/filters/video/Transpose.js
Normal file
109
src/misc/filters/video/Transpose.js
Normal file
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
import Select from '../../Select';
|
||||
|
||||
// Transpose Filter
|
||||
// http://ffmpeg.org/ffmpeg-all.html#transpose-1
|
||||
|
||||
function init(initialState) {
|
||||
const state = {
|
||||
value: false,
|
||||
...initialState,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
|
||||
if (settings.value) {
|
||||
if (settings.value === 3) {
|
||||
mapping.push('transpose=2', 'transpose=2');
|
||||
} else {
|
||||
mapping.push(`transpose=${settings.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// filter
|
||||
function Rotate(props) {
|
||||
return (
|
||||
<Select label={<Trans>Rotate</Trans>} value={props.value} onChange={props.onChange}>
|
||||
<MenuItem value={false}>None</MenuItem>
|
||||
<MenuItem value={1}>90°</MenuItem>
|
||||
<MenuItem value={3}>180°</MenuItem>
|
||||
<MenuItem value={2}>270°</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
Rotate.defaultProps = {
|
||||
value: '',
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function Filter(props) {
|
||||
const settings = init(props.settings);
|
||||
|
||||
const handleChange = (newSettings) => {
|
||||
let automatic = false;
|
||||
if (!newSettings) {
|
||||
newSettings = settings;
|
||||
automatic = true;
|
||||
}
|
||||
|
||||
props.onChange(newSettings, createMapping(newSettings), automatic);
|
||||
};
|
||||
|
||||
const update = (what) => (event) => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
[what]: event.target.value,
|
||||
};
|
||||
|
||||
handleChange(newSettings);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleChange(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
<Rotate value={settings.value} onChange={update('value')} allowCustom />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
Filter.defaultProps = {
|
||||
settings: {},
|
||||
onChange: function (settings, mapping) {},
|
||||
};
|
||||
|
||||
const filter = 'transpose';
|
||||
const name = 'Filter (transpose)';
|
||||
const type = 'video';
|
||||
const hwaccel = false;
|
||||
|
||||
function summarize(settings) {
|
||||
return `${name}`;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
const settings = init({});
|
||||
|
||||
return {
|
||||
settings: settings,
|
||||
mapping: createMapping(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };
|
||||
102
src/misc/filters/video/VFlip.js
Normal file
102
src/misc/filters/video/VFlip.js
Normal file
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
import Checkbox from '../../Checkbox';
|
||||
|
||||
// VFlip Filter
|
||||
// http://ffmpeg.org/ffmpeg-all.html#vflip
|
||||
|
||||
function init(initialState) {
|
||||
const state = {
|
||||
value: false,
|
||||
...initialState,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
|
||||
if (settings.value) {
|
||||
mapping.push('vflip');
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function VFlip(props) {
|
||||
return (
|
||||
<Checkbox label={<Trans>Vertical Flip</Trans>} checked={props.value} onChange={props.onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
VFlip.defaultProps = {
|
||||
value: '',
|
||||
onChange: function (event) {},
|
||||
};
|
||||
|
||||
function Filter(props) {
|
||||
const settings = init(props.settings);
|
||||
|
||||
const handleChange = (newSettings) => {
|
||||
let automatic = false;
|
||||
if (!newSettings) {
|
||||
newSettings = settings;
|
||||
automatic = true;
|
||||
}
|
||||
|
||||
props.onChange(newSettings, createMapping(newSettings), automatic);
|
||||
};
|
||||
|
||||
const update = (what) => (event) => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
};
|
||||
if (['value'].includes(what)) {
|
||||
newSettings[what] = !settings.value;
|
||||
} else {
|
||||
newSettings[what] = event.target.value;
|
||||
}
|
||||
|
||||
handleChange(newSettings);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleChange(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid item>
|
||||
<VFlip value={settings.value} onChange={update('value')} allowCustom />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
Filter.defaultProps = {
|
||||
settings: {},
|
||||
onChange: function (settings, mapping) {},
|
||||
};
|
||||
|
||||
const filter = 'vflip';
|
||||
const name = 'Vertical Flip';
|
||||
const type = 'video';
|
||||
const hwaccel = false;
|
||||
|
||||
function summarize(settings) {
|
||||
return `${name}`;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
const settings = init({});
|
||||
|
||||
return {
|
||||
settings: settings,
|
||||
mapping: createMapping(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };
|
||||
@ -597,7 +597,15 @@ const createInputsOutputs = (sources, profiles) => {
|
||||
|
||||
global = [...global, ...profile.video.encoder.mapping.global];
|
||||
|
||||
const options = ['-map', index + ':' + stream.stream, ...profile.video.encoder.mapping.local];
|
||||
// Merge video filters
|
||||
for (let i = 0; i < profile.video.encoder.mapping.local.length; i++) {
|
||||
if (profile.video.encoder.mapping.local[i] === '-vf' && profile.video.filter.mapping.length !== 0) {
|
||||
profile.video.encoder.mapping.local[i + 1] = profile.video.encoder.mapping.local[i + 1] + ',' + profile.video.filter.mapping[1];
|
||||
profile.video.filter.mapping = [];
|
||||
}
|
||||
}
|
||||
|
||||
const options = ['-map', index + ':' + stream.stream, ...profile.video.filter.mapping, ...profile.video.encoder.mapping.local];
|
||||
|
||||
if (profile.audio.encoder.coder !== 'none' && profile.audio.source !== -1 && profile.audio.stream !== -1) {
|
||||
global = [...global, ...profile.audio.decoder.mapping.global];
|
||||
@ -619,7 +627,15 @@ const createInputsOutputs = (sources, profiles) => {
|
||||
|
||||
global = [...global, ...profile.audio.encoder.mapping.global];
|
||||
|
||||
options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping.local);
|
||||
// Merge audio filters
|
||||
for (let i = 0; i < profile.audio.encoder.mapping.local.length; i++) {
|
||||
if (profile.audio.encoder.mapping.local[i] === '-af' && profile.audio.filter.mapping.length !== 0) {
|
||||
profile.audio.encoder.mapping.local[i + 1] = profile.audio.encoder.mapping.local[i + 1] + ',' + profile.audio.filter.mapping[1];
|
||||
profile.audio.filter.mapping = [];
|
||||
}
|
||||
}
|
||||
|
||||
options.push('-map', index + ':' + stream.stream, ...profile.audio.filter.mapping, ...profile.audio.encoder.mapping.local);
|
||||
} else {
|
||||
options.push('-an');
|
||||
}
|
||||
@ -745,6 +761,7 @@ const initProfile = (initialProfile) => {
|
||||
stream: -1,
|
||||
encoder: {},
|
||||
decoder: {},
|
||||
filter: {},
|
||||
...profile.video,
|
||||
};
|
||||
|
||||
@ -789,11 +806,19 @@ const initProfile = (initialProfile) => {
|
||||
};
|
||||
}
|
||||
|
||||
profile.video.filter = {
|
||||
filter: 'default',
|
||||
settings: {},
|
||||
mapping: {},
|
||||
...profile.video.filter,
|
||||
};
|
||||
|
||||
profile.audio = {
|
||||
source: -1,
|
||||
stream: -1,
|
||||
encoder: {},
|
||||
decoder: {},
|
||||
filter: {},
|
||||
...profile.audio,
|
||||
};
|
||||
|
||||
@ -837,6 +862,13 @@ const initProfile = (initialProfile) => {
|
||||
};
|
||||
}
|
||||
|
||||
profile.audio.filter = {
|
||||
filter: 'default',
|
||||
settings: {},
|
||||
mapping: {},
|
||||
...profile.audio.filter,
|
||||
};
|
||||
|
||||
profile.custom = {
|
||||
selected: profile.audio.source === 1,
|
||||
stream: profile.audio.source === 1 ? -2 : profile.audio.stream,
|
||||
|
||||
@ -495,6 +495,7 @@ class Restreamer {
|
||||
input: [],
|
||||
output: [],
|
||||
},
|
||||
filter: [],
|
||||
sources: {
|
||||
network: [],
|
||||
virtualaudio: [],
|
||||
@ -516,6 +517,7 @@ class Restreamer {
|
||||
formats: {},
|
||||
protocols: {},
|
||||
devices: {},
|
||||
filter: [],
|
||||
...val,
|
||||
};
|
||||
|
||||
@ -558,6 +560,10 @@ class Restreamer {
|
||||
skills.decoders.video.push(hwaccel.id);
|
||||
}
|
||||
|
||||
for (let filter of val.filter) {
|
||||
skills.filter.push(filter.id);
|
||||
}
|
||||
|
||||
val.formats = {
|
||||
demuxers: [],
|
||||
muxers: [],
|
||||
|
||||
@ -18,6 +18,8 @@ import ProbeModal from '../../misc/modals/Probe';
|
||||
import SourceSelect from './SourceSelect';
|
||||
import StreamSelect from './StreamSelect';
|
||||
|
||||
import FilterSelect from '../../misc/FilterSelect';
|
||||
|
||||
export default function Source(props) {
|
||||
const [$sources, setSources] = React.useState({
|
||||
video: M.initSource('video', props.sources[0]),
|
||||
@ -212,6 +214,17 @@ export default function Source(props) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleFilter = (type) => (filter) => {
|
||||
const profile = $profile[type];
|
||||
|
||||
profile.filter = filter;
|
||||
|
||||
setProfile({
|
||||
...$profile,
|
||||
[type]: profile,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDone = () => {
|
||||
const sources = M.cleanupSources($sources);
|
||||
const profile = M.cleanupProfile($profile);
|
||||
@ -395,6 +408,17 @@ export default function Source(props) {
|
||||
onChange={handleEncoding('video')}
|
||||
/>
|
||||
</Grid>
|
||||
{($profile.video.encoder.coder !== 'none' && $profile.video.encoder.coder !== 'copy') && (
|
||||
<Grid item xs={12}>
|
||||
<FilterSelect
|
||||
type="video"
|
||||
profile={$profile.video}
|
||||
videoProfile={$profile.video}
|
||||
availableFilters={props.skills.filter}
|
||||
onChange={handleFilter('video')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@ -457,6 +481,16 @@ export default function Source(props) {
|
||||
onChange={handleEncoding('audio')}
|
||||
/>
|
||||
</Grid>
|
||||
{($profile.audio.encoder.coder !== 'none' && $profile.audio.encoder.coder !== 'copy') && (
|
||||
<Grid item xs={12}>
|
||||
<FilterSelect
|
||||
type="audio"
|
||||
profile={$profile.audio}
|
||||
availableFilters={props.skills.filter}
|
||||
onChange={handleFilter('audio')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{$profile.custom.selected === true && (
|
||||
@ -524,6 +558,16 @@ export default function Source(props) {
|
||||
onChange={handleEncoding('audio')}
|
||||
/>
|
||||
</Grid>
|
||||
{($profile.audio.encoder.coder !== 'none' && $profile.audio.encoder.coder !== 'copy') && (
|
||||
<Grid item xs={12}>
|
||||
<FilterSelect
|
||||
type="audio"
|
||||
profile={$profile.audio}
|
||||
availableFilters={props.skills.filter}
|
||||
onChange={handleFilter('audio')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user