Compare commits

...

8 Commits

Author SHA1 Message Date
Ingo Oppermann
f422b0b45e
Remove resample filter from audio decoders, add filters to summary 2022-07-14 19:04:03 +02:00
Ingo Oppermann
85f6d36f0a
Merge branch 'dev' into av_filter 2022-07-14 18:32:07 +02:00
Ingo Oppermann
1aa43fff4d
Add aresample filter to replace filter in encoders (WIP) 2022-07-14 17:56:30 +02:00
Ingo Oppermann
827f5bec54
Merge branch 'dev' into av_filter 2022-07-14 17:30:10 +02:00
Ingo Oppermann
a91e86bc66
Use graphs instead of mappings for filters, use -filter:(a|v) in command line 2022-07-14 16:35:59 +02:00
Ingo Oppermann
7639c4de18
Fix type changes in filter settings 2022-07-14 10:24:09 +02:00
Jan Stabenow
f610e2739e
Mod removes console.log 2022-07-13 20:26:21 +02:00
Jan Stabenow
ec90f6730f
Add av filter components 2022-07-13 20:23:12 +02:00
24 changed files with 1145 additions and 280 deletions

View File

@ -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 AirPlay support with silvermine videojs plugin
- Add Chromecast support (thx badincite, [#10](https://github.com/datarhei/restreamer-ui/pull/10))
- Add stream distribution across multiple internal servers
@ -11,6 +15,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

View File

@ -379,7 +379,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}}

View File

@ -239,5 +239,5 @@ EncodingSelect.defaultProps = {
codecs: [],
availableEncoders: [],
availableDecoders: [],
onChange: function (encoder, decoder) {},
onChange: function (encoder, decoder, automatic) {},
};

134
src/misc/FilterSelect.js Normal file
View File

@ -0,0 +1,134 @@
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, graph, automatic) => {
const filter = profile.filter;
// Store mapping/settings per component
filter.settings[what] = {
settings: settings,
graph: graph,
};
// Get the order of the filters
let filterOrder = [];
if (props.type === 'video') {
filterOrder = Filters.Video.Filters();
} else {
filterOrder = Filters.Audio.Filters();
}
// Create the filter graph in the order as the filters are registered
const graphs = [];
for (let f of filterOrder) {
if (!(f in filter.settings)) {
continue;
}
if (filter.settings[f].graph.length !== 0) {
graphs.push(filter.settings[f].graph);
}
}
filter.graph = graphs.join(',');
props.onChange(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 encoder of encoderRegistry.List()) {
if (encoder.codec === props.profile.encoder.coder && encoder.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;
if (!(c.filter in profile.filter.settings)) {
profile.filter.settings[c.filter] = c.defaults();
} else {
profile.filter.settings[c.filter] = {
...c.defaults(),
...profile.filter.settings[c.filter],
};
}
filterSettings.push(
<Settings key={c.filter} settings={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: '',
profile: {},
availableFilters: [],
onChange: function (filter, automatic) {},
};

View File

@ -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;

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,18 +14,7 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
const local = ['-codec:a', 'aac', '-b:a', `${settings.bitrate}k`, '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'aac', '-b:a', `${settings.bitrate}k`, '-shortest'];
if (stream.codec === 'aac') {
local.push('-bsf:a', 'aac_adtstoasc');
@ -64,23 +50,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -94,12 +63,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -117,7 +80,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,18 +14,7 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
const local = ['-codec:a', 'aac_at', '-b:a', `${settings.bitrate}k`, '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'aac_at', '-b:a', `${settings.bitrate}k`, '-shortest'];
if (stream.codec === 'aac') {
local.push('-bsf:a', 'aac_adtstoasc');
@ -64,23 +50,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -94,12 +63,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowCustom allowInherit />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -117,7 +80,7 @@ const type = 'audio';
const hwaccel = true;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,18 +14,7 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
const local = ['-codec:a', 'libopus', '-b:a', `${settings.bitrate}k`, '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'libopus', '-b:a', `${settings.bitrate}k`, '-shortest'];
const mapping = {
global: [],
@ -60,23 +46,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -90,12 +59,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -113,7 +76,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,18 +14,7 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
const local = ['-codec:a', 'libvorbis', '-b:a', `${settings.bitrate}k`, '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'libvorbis', '-b:a', `${settings.bitrate}k`, '-shortest'];
const mapping = {
global: [],
@ -60,23 +46,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -90,12 +59,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -113,7 +76,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,19 +14,8 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
// '-qscale:a', '6'
const local = ['-codec:a', 'libmp3lame', '-b:a', `${settings.bitrate}k`, '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'libmp3lame', '-b:a', `${settings.bitrate}k`, '-shortest'];
const mapping = {
global: [],
@ -61,23 +47,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -91,12 +60,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -114,7 +77,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -13,9 +13,6 @@ function init(initialState) {
const state = {
bitrate: '64',
delay: 'auto',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -34,7 +31,7 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const local = ['-codec:a', 'opus', '-b:a', `${settings.bitrate}k`, '-vbr', 'on', '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'opus', '-b:a', `${settings.bitrate}k`, '-vbr', 'on', '-shortest'];
if (settings.delay !== 'auto') {
local.push('opus_delay', settings.delay);
@ -113,23 +110,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -146,12 +126,6 @@ function Coder(props) {
<Grid item xs={12}>
<Delay value={settings.delay} onChange={update('delay')} allowAuto allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -169,7 +143,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -7,9 +7,6 @@ import Audio from '../../settings/Audio';
function init(initialState) {
const state = {
bitrate: '64',
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
@ -17,18 +14,7 @@ function init(initialState) {
}
function createMapping(settings, stream) {
let sampling = settings.sampling;
let layout = settings.layout;
if (sampling === 'inherit') {
sampling = stream.sampling_hz;
}
if (layout === 'inherit') {
layout = stream.layout;
}
const local = ['-codec:a', 'vorbis', '-b:a', `${settings.bitrate}k`, '-qscale:a', '3', '-shortest', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const local = ['-codec:a', 'vorbis', '-b:a', `${settings.bitrate}k`, '-qscale:a', '3', '-shortest'];
const mapping = {
global: [],
@ -60,23 +46,6 @@ function Coder(props) {
[what]: value,
};
if (what === 'layout') {
let channels = stream.channels;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
@ -90,12 +59,6 @@ function Coder(props) {
<Grid item xs={12}>
<Audio.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Audio.Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</Grid>
);
}
@ -113,7 +76,7 @@ const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.layout}, ${settings.sampling}Hz`;
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {

View File

@ -0,0 +1,93 @@
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 = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
if (settings.enabled) {
mapping.push('loudnorm');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} 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>
<Checkbox label={<Trans>Loudness Normalization</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
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,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

View File

@ -0,0 +1,224 @@
import React from 'react';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import SelectCustom from '../../../misc/SelectCustom';
// Resample Filter
// https://ffmpeg.org/ffmpeg-filters.html#toc-aresample-1
function init(initialState) {
const state = {
channels: '2',
layout: 'stereo',
sampling: '44100',
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
const sampling = settings.sampling;
const layout = settings.layout;
if (sampling !== 'inherit') {
mapping.push(`osr=${sampling}`);
}
if (layout !== 'inherit') {
mapping.push(`ocl=${layout}`);
}
if (mapping.length === 0) {
return '';
}
return 'aresample=' + mapping.join(':');
}
function Layout(props) {
const { i18n } = useLingui();
const options = [
{ value: 'mono', label: 'mono' },
{ value: 'stereo', label: 'stereo' },
];
if (props.allowAuto === true) {
options.unshift({ value: 'auto', label: 'auto' });
}
if (props.allowInherit === true) {
options.unshift({ value: 'inherit', label: i18n._(t`Inherit`) });
}
if (props.allowCustom === true) {
options.push({ value: 'custom', label: i18n._(t`Custom ...`) });
}
return (
<React.Fragment>
<SelectCustom
options={options}
label={props.label}
customLabel={props.customLabel}
value={props.value}
onChange={props.onChange}
variant={props.variant}
allowCustom={props.allowCustom}
/>
<Typography variant="caption">
<Trans>The layout of the audio stream.</Trans>
</Typography>
</React.Fragment>
);
}
Layout.defaultProps = {
variant: 'outlined',
allowAuto: false,
allowInherit: false,
allowCustom: false,
label: <Trans>Layout</Trans>,
customLabel: <Trans>Custom layout</Trans>,
onChange: function () {},
};
function Sampling(props) {
const { i18n } = useLingui();
const options = [
{ value: '96000', label: '96000 Hz' },
{ value: '88200', label: '88200 Hz' },
{ value: '48000', label: '48000 Hz' },
{ value: '44100', label: '44100 Hz' },
{ value: '22050', label: '22050 Hz' },
{ value: '8000', label: '8000 Hz' },
];
if (props.allowAuto === true) {
options.unshift({ value: 'auto', label: 'auto' });
}
if (props.allowInherit === true) {
options.unshift({ value: 'inherit', label: i18n._(t`Inherit`) });
}
if (props.allowCustom === true) {
options.push({ value: 'custom', label: i18n._(t`Custom ...`) });
}
return (
<React.Fragment>
<SelectCustom
options={options}
label={props.label}
customLabel={props.customLabel}
value={props.value}
onChange={props.onChange}
variant={props.variant}
allowCustom={props.allowCustom}
/>
<Typography variant="caption">
<Trans>The sample rate of the audio stream.</Trans>
</Typography>
</React.Fragment>
);
}
Sampling.defaultProps = {
variant: 'outlined',
allowAuto: false,
allowInherit: false,
allowCustom: false,
label: <Trans>Sampling</Trans>,
customLabel: <Trans>Custom sampling (Hz)</Trans>,
onChange: function () {},
};
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const value = event.target.value;
const newSettings = {
...settings,
[what]: value,
};
if (what === 'layout') {
let channels = 2;
switch (value) {
case 'mono':
channels = 1;
break;
case 'stereo':
channels = 2;
break;
default:
break;
}
newSettings.channels = channels;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<React.Fragment>
<Grid item xs={12}>
<Sampling value={settings.sampling} onChange={update('sampling')} allowInherit allowCustom />
</Grid>
<Grid item xs={12}>
<Layout value={settings.layout} onChange={update('layout')} allowInherit />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'aresample';
const name = 'Resample';
const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name} (${settings.layout}, ${settings.sampling}Hz)`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

View File

@ -0,0 +1,160 @@
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: 'none',
db: 0,
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
switch (settings.level) {
case 'none':
break;
case 'custom':
mapping.push(`volume=volume=${settings.db}dB`);
break;
default:
mapping.push(`volume=volume=${parseInt(settings.level) / 100}`);
break;
}
return mapping.join(',');
}
function VolumeLevel(props) {
return (
<Select label={<Trans>Volume</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="none">
<Trans>None</Trans>
</MenuItem>
<MenuItem value="10">10%</MenuItem>
<MenuItem value="20">20%</MenuItem>
<MenuItem value="30">30%</MenuItem>
<MenuItem value="40">40%</MenuItem>
<MenuItem value="50">50%</MenuItem>
<MenuItem value="60">60%</MenuItem>
<MenuItem value="70">70%</MenuItem>
<MenuItem value="80">80%</MenuItem>
<MenuItem value="90">90%</MenuItem>
<MenuItem value="100">100%</MenuItem>
<MenuItem value="custom">
<Trans>Custom ...</Trans>
</MenuItem>
</Select>
);
}
VolumeLevel.defaultProps = {
value: '',
onChange: function (event) {},
};
function VolumeDB(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, createGraph(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')} />
</Grid>
<Grid item xs={6}>
<VolumeDB value={settings.db} onChange={update('db')} disabled={settings.level !== 'custom'} />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'volume';
const name = 'Volume';
const type = 'audio';
const hwaccel = false;
function summarize(settings) {
let summary = `${name} (`;
if (settings.level === 'custom') {
summary += `${settings.db}dB`;
} else {
summary += `${settings.level}%`;
}
summary += ')';
return summary;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

57
src/misc/filters/index.js Normal file
View File

@ -0,0 +1,57 @@
// Audio Filter
import * as AResample from './audio/Resample';
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';
// Register filters 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(AResample);
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 };

View File

@ -0,0 +1,91 @@
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 = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
if (settings.enabled) {
mapping.push('hflip');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} else {
newSettings[what] = event.target.value;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid item>
<Checkbox label={<Trans>Horizontal Flip</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
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,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

View File

@ -0,0 +1,115 @@
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: 'none',
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
switch (settings.value) {
case '90':
mapping.push('transpose=dir=clock:passthrough=none');
break;
case '180':
mapping.push('transpose=dir=clock:passthrough=none', 'transpose=dir=clock:passthrough=none');
break;
case '270':
mapping.push('transpose=dir=cclock:passthrough=none');
break;
default:
break;
}
return mapping.join(',');
}
// filter
function Rotate(props) {
return (
<Select label={<Trans>Rotate</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="none">None</MenuItem>
<MenuItem value="90">90°</MenuItem>
<MenuItem value="180">180°</MenuItem>
<MenuItem value="270">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, createGraph(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 = 'Transpose';
const type = 'video';
const hwaccel = false;
function summarize(settings) {
return `${name} (${settings.value}° clockwise)`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

View File

@ -0,0 +1,91 @@
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 = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
const mapping = [];
if (settings.enabled) {
mapping.push('vflip');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} else {
newSettings[what] = event.target.value;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid item>
<Checkbox label={<Trans>Vertical Flip</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
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,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, Filter as component };

View File

@ -3,7 +3,7 @@
Ingest Metadata Layout:
data = {
version: 1,
version: "1.2.0",
meta: {
name: 'Livestream 1',
description: 'Live from earth. Powered by datarhei/restreamer.',
@ -99,12 +99,24 @@ data = {
'-codec:a', 'aac',
'-b:a', '64k',
'-bsf:a', 'aac_adtstoasc',
'-shortest',
'-af', 'aresample=osr=44100:ocl=2'
'-shortest'
]
}
},
decoder: null,
filter: {
graph: 'aresample=osr=44100:ocl=stereo',
settings: {
aresample: {
graph: 'aresample=osr=44100:ocl=stereo',
settings: {
channels: 2,
layout: 'stereo',
sampling: 44100
}
}
}
},
},
video: {
source: 0,
@ -124,6 +136,7 @@ data = {
}
},
decoder: null,
filter: null,
},
"or": {},
"video": {
@ -210,7 +223,7 @@ data = {
Egress Metadata Layout:
data = {
version: 1,
version: "1.2.0",
name: "foobar",
control: {
process: {
@ -232,14 +245,15 @@ data = {
*/
import * as Coders from '../misc/coders/Encoders';
import * as version from '../version';
const defaultMetadata = {
version: 1,
version: version.Version,
playersite: {},
};
const defaultIngestMetadata = {
version: 1,
version: version.Version,
sources: [],
profiles: [{}],
streams: [],
@ -282,7 +296,7 @@ const defaultIngestMetadata = {
};
const defaultEgressMetadata = {
version: 1,
version: version.Version,
name: '',
control: {
process: {
@ -597,7 +611,19 @@ const createInputsOutputs = (sources, profiles) => {
global = [...global, ...profile.video.encoder.mapping.global];
const options = ['-map', index + ':' + stream.stream, ...profile.video.encoder.mapping.local];
const local = profile.video.encoder.mapping.local.slice();
if (profile.video.filter.graph.length !== 0) {
// Check if there's already a video filter in the local mapping
let filterIndex = local.indexOf('-filter:v');
if (filterIndex !== -1) {
local[filterIndex + 1] += ',' + profile.video.filter.graph;
} else {
local.unshift('-filter:v', profile.video.filter.graph);
}
}
const options = ['-map', index + ':' + stream.stream, ...local];
if (profile.audio.encoder.coder !== 'none' && profile.audio.source !== -1 && profile.audio.stream !== -1) {
global = [...global, ...profile.audio.decoder.mapping.global];
@ -619,7 +645,19 @@ const createInputsOutputs = (sources, profiles) => {
global = [...global, ...profile.audio.encoder.mapping.global];
options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping.local);
const local = profile.audio.encoder.mapping.local.slice();
if (profile.audio.filter.graph.length !== 0) {
// Check if there's already a audio filter in the local mapping
let filterIndex = local.indexOf('-filter:a');
if (filterIndex !== -1) {
local[filterIndex + 1] += ',' + profile.audio.filter.graph;
} else {
local.unshift('-filter:a', profile.audio.filter.graph);
}
}
options.push('-map', index + ':' + stream.stream, ...local);
} else {
options.push('-an');
}
@ -745,6 +783,7 @@ const initProfile = (initialProfile) => {
stream: -1,
encoder: {},
decoder: {},
filter: {},
...profile.video,
};
@ -789,11 +828,18 @@ const initProfile = (initialProfile) => {
};
}
profile.video.filter = {
graph: '',
settings: {},
...profile.video.filter,
};
profile.audio = {
source: -1,
stream: -1,
encoder: {},
decoder: {},
filter: {},
...profile.audio,
};
@ -837,6 +883,12 @@ const initProfile = (initialProfile) => {
};
}
profile.audio.filter = {
graph: '',
settings: {},
...profile.audio.filter,
};
profile.custom = {
selected: profile.audio.source === 1,
stream: profile.audio.source === 1 ? -2 : profile.audio.stream,

View File

@ -502,6 +502,7 @@ class Restreamer {
input: [],
output: [],
},
filter: [],
sources: {
network: [],
virtualaudio: [],
@ -523,6 +524,7 @@ class Restreamer {
formats: {},
protocols: {},
devices: {},
filter: [],
...val,
};
@ -565,6 +567,10 @@ class Restreamer {
skills.decoders.video.push(hwaccel.id);
}
for (let filter of val.filter) {
skills.filter.push(filter.id);
}
val.formats = {
demuxers: [],
muxers: [],

View File

@ -3,5 +3,6 @@ import { name, version, bundle } from '../package.json';
const Core = '^16.9.0';
const FFmpeg = '^4.1.0 || ^5.0.0';
const UI = bundle ? bundle : name + ' v' + version;
const Version = version;
export { Core, FFmpeg, UI };
export { Core, FFmpeg, UI, Version };

View File

@ -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,16 @@ 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}
availableFilters={props.skills.filter}
onChange={handleFilter('video')}
/>
</Grid>
)}
</React.Fragment>
)}
</React.Fragment>
@ -457,6 +480,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 +557,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>

View File

@ -6,6 +6,7 @@ import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import * as Coders from '../../misc/coders/Encoders';
import * as Filters from '../../misc/filters';
import BoxText from '../../misc/BoxText';
import Sources from './Sources';
@ -28,6 +29,7 @@ export default function Summary(props) {
let name = i18n._(t`No source selected`);
let address = '';
let encodingSummary = i18n._(t`None`);
let filterSummary = [];
let showEncoding = false;
@ -51,6 +53,30 @@ export default function Summary(props) {
if (coder !== null) {
encodingSummary = coder.summarize(profile.encoder.settings);
}
if (profile.filter.graph.length !== 0) {
let filters = null;
if (props.type === 'video') {
filters = Filters.Video;
} else if (props.type === 'audio') {
filters = Filters.Audio;
}
for (let filter of filters.List()) {
const name = filter.filter;
if (!(name in profile.filter.settings)) {
continue;
}
if (profile.filter.settings[name].graph.length === 0) {
continue;
}
filterSummary.push(filter.summarize(profile.filter.settings[name].settings));
}
}
}
return (
@ -61,12 +87,26 @@ export default function Summary(props) {
<Typography variant="body1">{address}</Typography>
</Grid>
{showEncoding === true && (
<Grid item xs={12}>
<Typography variant="subtitle2">
<Trans>Encoding</Trans>
</Typography>
<Typography variant="body1">{encodingSummary}</Typography>
</Grid>
<React.Fragment>
<Grid item xs={12}>
<Typography variant="subtitle2">
<Trans>Encoding</Trans>
</Typography>
<Typography variant="body1">{encodingSummary}</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle2">
<Trans>Filter</Trans>
</Typography>
{filterSummary.length ? (
<Typography variant="body1">{filterSummary.join(', ')}</Typography>
) : (
<Typography variant="body1">
<Trans>None</Trans>
</Typography>
)}
</Grid>
</React.Fragment>
)}
</Grid>
</BoxText>