Allow decoders and encoders to set global options

For more details please read the src/misc/coders/README.md
This commit is contained in:
Ingo Oppermann 2022-06-29 21:51:10 +02:00
parent f8a899ca0f
commit 28c376f5a7
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
45 changed files with 493 additions and 201 deletions

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [];
const mapping = {
global: [],
local: [],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [];
const mapping = {
global: [],
local: [],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'h264_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'h264_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'h264_mmal'];
const mapping = {
global: [],
local: ['-c:v', 'h264_mmal'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'hevc_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'hevc_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mjpeg_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'mjpeg_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mpeg1_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'mpeg1_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mpeg2_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'mpeg2_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mpeg2_mmal'];
const mapping = {
global: [],
local: ['-c:v', 'mpeg2_mmal'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mpeg4_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'mpeg4_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'mpeg4_mmal'];
const mapping = {
global: [],
local: ['-c:v', 'mpeg4_mmal'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-hwaccel', 'cuda'];
const mapping = {
global: [],
local: ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'vc1_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'vc1_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'vc1_mmal'];
const mapping = {
global: [],
local: ['-c:v', 'vc1_mmal'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'vp8_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'vp8_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-c:v', 'vp9_cuvid'];
const mapping = {
global: [],
local: ['-c:v', 'vp9_cuvid'],
};
return mapping;
}

View File

@ -9,7 +9,10 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = ['-hwaccel', 'videotoolbox'];
const mapping = {
global: [],
local: ['-hwaccel', 'videotoolbox'],
};
return mapping;
}

View File

@ -28,12 +28,17 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
if (stream.codec === 'aac') {
mapping.push('-bsf:a', 'aac_adtstoasc');
local.push('-bsf:a', 'aac_adtstoasc');
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -28,12 +28,17 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
if (stream.codec === 'aac') {
mapping.push('-bsf:a', 'aac_adtstoasc');
local.push('-bsf:a', 'aac_adtstoasc');
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -1,12 +1,18 @@
import React from 'react';
function createMapping(settings, stream) {
const mapping = ['-codec:a', 'copy'];
const local = ['-codec:a', 'copy'];
/*
if(stream.codec === 'aac') {
mapping.push('-bsf:a', 'aac_adtstoasc');
local.push('-bsf:a', 'aac_adtstoasc');
}
*/
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -28,7 +28,12 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -28,7 +28,12 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -29,7 +29,12 @@ function createMapping(settings, stream) {
}
// '-qscale:a', '6'
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -1,7 +1,12 @@
import React from 'react';
function createMapping(settings, stream) {
const mapping = ['-an'];
const local = ['-an'];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -34,12 +34,17 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
if (settings.delay !== 'auto') {
mapping.push('opus_delay', settings.delay);
local.push('opus_delay', settings.delay);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -28,7 +28,12 @@ function createMapping(settings, stream) {
layout = stream.layout;
}
const mapping = ['-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', '-af', `aresample=osr=${sampling}:ocl=${layout}`];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -1,7 +1,12 @@
import React from 'react';
function createMapping(settings) {
const mapping = ['-codec:v', 'copy', '-vsync', '0', '-copyts', '-start_at_zero'];
const local = ['-codec:v', 'copy', '-vsync', 'passthrough', '-copyts', '-start_at_zero'];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -24,7 +24,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'h264_nvenc',
'-preset:v',
@ -44,21 +44,26 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.level !== 'auto') {
mapping.push('-level:v', `${settings.level}`);
local.push('-level:v', `${settings.level}`);
}
if (settings.rc !== 'auto') {
mapping.push('-rc:v', `${settings.rc}`);
local.push('-rc:v', `${settings.rc}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -17,7 +17,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'h264_omx',
'-b:v',
@ -37,13 +37,18 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -65,7 +65,7 @@ Codec Controls
*/
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'h264_v4l2m2m',
'-b:v',
@ -83,13 +83,18 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -24,7 +24,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-vaapi_device',
'/dev/dri/renderD128',
'-vf',
@ -46,13 +46,18 @@ function createMapping(settings) {
'-g',
`${settings.gop}`,
'-vsync',
'1',
'cfr',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -22,7 +22,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'h264_videotoolbox',
'-b:v',
@ -42,17 +42,22 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.entropy !== 'default') {
mapping.push('-coder:v', `${settings.entropy}`);
local.push('-coder:v', `${settings.entropy}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -1,7 +1,12 @@
import React from 'react';
function createMapping(settings, stream) {
const mapping = ['-vn'];
const local = ['-vn'];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -1,7 +1,12 @@
import React from 'react';
function createMapping(settings) {
const mapping = ['-codec:v', 'rawvideo'];
const local = ['-codec:v', 'rawvideo'];
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -16,7 +16,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libvpx-vp9',
'-b:v',
@ -34,9 +34,14 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -23,7 +23,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libx264',
'-preset:v',
@ -43,17 +43,22 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.tune !== 'none') {
mapping.push('-tune:v', `${settings.tune}`);
local.push('-tune:v', `${settings.tune}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -23,7 +23,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libx265',
'-preset:v',
@ -43,17 +43,22 @@ function createMapping(settings) {
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.tune !== 'none') {
mapping.push('-tune:v', `${settings.tune}`);
local.push('-tune:v', `${settings.tune}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

118
src/misc/coders/README.md Normal file
View File

@ -0,0 +1,118 @@
# Decoders and Encoders
Implementations of various decoders and encoder for audio and video.
## Decoders
Each decoder exports the same variables:
```
export { coder, name, codecs, type, hwaccel, defaults, Coder as component };
```
| Variable | Description |
| -------- | -------------------------------------------------------------------- |
| coder | Name of the decoder in FFmpeg, e.g. `cuda`, `vc1_mmal`. |
| name | Name for the decoder as it will be displayed in the UI. |
| codecs | Array of codecs this coder supports, e.g. `['h264', 'h265']`. |
| type | Either `video` or `audio`. |
| hwaccel | Whether this codec uses hardware acceleration. |
| defaults | A function that returns the default settings and mapping. See below. |
| Coder | The React component. |
### defaults
The `defaults()` function returns the default settings and mappings for this decoder. It is an object of this shape:
```
{
settings: {},
mapping: {
global: [],
local: [],
},
}
```
The `settings` is an object private to a coder and contains its settings as required for rendering the component
with options for this coder. The `mapping` object contains the FFmpeg command line options according to the settings.
It has a `global` array which contains all global options for this coder. _Each option (with its value) has to be
an array of its own_. The `local` array is an array of options for that input, e.g.
```
{
settings: {
...
},
mapping: {
global: [
['-init_hw_device', 'vaapi=foo:/dev/dri/renderD128'],
],
local: [
'-hwaccel', 'vaapi',
'-hwaccel_output_format', 'vaapi',
'-hwaccel_device', 'foo',
],
},
}
```
Check out the existing decoders as examples for an implementation.
## Encoders
Each encoder exports the same variables:
```
export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as component };
```
| Variable | Description |
| --------- | ---------------------------------------------------------------------- |
| coder | Name of the encoder in FFmpeg, e.g. `libx264`. |
| name | Name for the encoder as it will be displayed in the UI. |
| codec | Name of the codec, e.g. `h264`. |
| type | Either `video` or `audio`. |
| hwaccel | Whether this codec uses hardware acceleration. |
| summarize | A function that returns a string that summarizes the current settings. |
| defaults | A function that returns the default settings and mapping. See below. |
| Coder | The React component. |
### defaults
The `defaults()` function returns the default settings and mappings for this encoder. It is an object of this shape:
```
{
settings: {},
mapping: {
global: [],
local: [],
},
}
```
The `settings` is an object private to a coder and contains its settings as required for rendering the component
with options for this coder. The `mapping` object contains the FFmpeg command line options according to the settings.
It has a `global` array which contains all global options for this coder. _Each option (with its value) has to be
an array of its own_. The `local` array is an array of options for that input, e.g.
```
{
settings: {
...
},
mapping: {
global: [
['-init_hw_device', 'vaapi=foo:/dev/dri/renderD128'],
],
local: [
'-filter_hw_device', 'foo',
'-filter:v', 'format=nv12|vaapi,hwupload',
'-codec:v', 'h264_vaapi',
],
},
}
```
Check out the existing encoders as examples for an implementation.

View File

@ -93,13 +93,16 @@ data = {
channels: '2',
sampling: '44100'
},
mapping: [
'-codec:a', 'aac',
'-b:a', '64k',
'-bsf:a', 'aac_adtstoasc',
'-shortest',
'-af', 'aresample=osr=44100:ocl=2'
]
mapping: {
global: [],
local: [
'-codec:a', 'aac',
'-b:a', '64k',
'-bsf:a', 'aac_adtstoasc',
'-shortest',
'-af', 'aresample=osr=44100:ocl=2'
]
}
},
decoder: null,
},
@ -110,12 +113,15 @@ data = {
coder: 'copy',
codec: 'h264',
settings: {},
mapping: [
'-codec:v', 'copy',
'-vsync 0',
'-copyts',
'-start_at_zero',
]
mapping: {
global: [],
local: [
'-codec:v', 'copy',
'-vsync 0',
'-copyts',
'-start_at_zero',
]
}
},
decoder: null,
},
@ -133,19 +139,22 @@ data = {
profile: 'auto',
tune: 'zerolatency',
},
mapping: [
'-codec:v', 'libx264',
'-preset:v', 'ultrafast',
'-b:v', '4096k',
'-maxrate', '4096k',
'-bufsize', '4096k',
'-r', '25',
'-g', '50',
'-pix_fmt', 'yuv420p',
'-vsync', '1',
'-profile:v', 'high',
'-tune:v', 'zerolatency',
]
mapping: {
global: [],
local: [
'-codec:v', 'libx264',
'-preset:v', 'ultrafast',
'-b:v', '4096k',
'-maxrate', '4096k',
'-bufsize', '4096k',
'-r', '25',
'-g', '50',
'-pix_fmt', 'yuv420p',
'-vsync', '1',
'-profile:v', 'high',
'-tune:v', 'zerolatency',
]
}
},
decoder: {
coder: 'h264_cuvid',
@ -483,11 +492,7 @@ const mergeEgressMetadata = (metadata, base) => {
const validateProfile = (sources, profile) => {
let validVideo = false;
if (!('video' in profile)) {
profile.video = initProfile({});
} else {
profile.video = initProfile(profile.video);
}
profile = initProfile(profile);
if (profile.video.source !== -1 && profile.video.source < sources.length) {
const source = sources[profile.video.source];
@ -505,12 +510,6 @@ const validateProfile = (sources, profile) => {
let validAudio = false;
if (!('audio' in profile)) {
profile.audio = initProfile({});
} else {
profile.audio = initProfile(profile.audio);
}
if (profile.audio.source !== -1 && profile.audio.source < sources.length) {
const source = sources[profile.audio.source];
@ -547,6 +546,7 @@ const validateProfile = (sources, profile) => {
const createInputsOutputs = (sources, profiles) => {
const source2inputMap = new Map();
let global = [];
const inputs = [];
const outputs = [];
@ -559,11 +559,13 @@ const createInputsOutputs = (sources, profiles) => {
let index = -1;
global = [...global, ...profile.video.decoder.mapping.global];
const source = sources[profile.video.source];
const stream = source.streams[profile.video.stream];
const input = source.inputs[stream.index];
input.options = [...profile.video.decoder.mapping, ...input.options];
input.options = [...profile.video.decoder.mapping.local, ...input.options];
const id = profile.video.source + ':' + stream.index;
@ -576,14 +578,18 @@ const createInputsOutputs = (sources, profiles) => {
index = source2inputMap.get(id);
const options = ['-map', index + ':' + stream.stream, ...profile.video.encoder.mapping];
global = [...global, ...profile.video.encoder.mapping.global];
const options = ['-map', index + ':' + stream.stream, ...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];
const source = sources[profile.audio.source];
const stream = source.streams[profile.audio.stream];
const input = source.inputs[stream.index];
input.options = [...profile.audio.decoder.mapping, ...input.options];
input.options = [...profile.audio.decoder.mapping.local, ...input.options];
const id = profile.audio.source + ':' + stream.index;
@ -594,7 +600,9 @@ const createInputsOutputs = (sources, profiles) => {
index = source2inputMap.get(id);
options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping);
global = [...global, ...profile.audio.encoder.mapping.global];
options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping.local);
} else {
options.push('-an');
}
@ -605,7 +613,16 @@ const createInputsOutputs = (sources, profiles) => {
});
}
return [inputs, outputs];
// https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array
const uniqBy = (a, key) => {
return [...new Map(a.map((x) => [key(x), x])).values()];
};
// global is an array of arrays. Here we remove duplicates and flatten it.
global = uniqBy(global, (x) => JSON.stringify(x.sort()));
global = global.reduce((acc, val) => acc.concat(val), []);
return [global, inputs, outputs];
};
const createOutputStreams = (sources, profiles) => {
@ -717,17 +734,44 @@ const initProfile = (initialProfile) => {
profile.video.encoder = {
coder: 'none',
settings: {},
mapping: [],
mapping: {},
...profile.video.encoder,
};
// mapping used to be an array for input/output specific options
if (Array.isArray(profile.video.encoder.mapping)) {
profile.video.encoder.mapping = {
global: [],
local: profile.video.encoder.mapping,
};
} else {
profile.video.encoder.mapping = {
global: [],
local: [],
...profile.video.encoder.mapping,
};
}
profile.video.decoder = {
coder: 'default',
settings: {},
mapping: [],
mapping: {},
...profile.video.decoder,
};
if (Array.isArray(profile.video.decoder.mapping)) {
profile.video.decoder.mapping = {
global: [],
local: profile.video.decoder.mapping,
};
} else {
profile.video.decoder.mapping = {
global: [],
local: [],
...profile.video.decoder.mapping,
};
}
profile.audio = {
source: -1,
stream: -1,
@ -739,17 +783,43 @@ const initProfile = (initialProfile) => {
profile.audio.encoder = {
coder: 'none',
settings: {},
mapping: [],
mapping: {},
...profile.audio.encoder,
};
if (Array.isArray(profile.audio.encoder.mapping)) {
profile.audio.encoder.mapping = {
global: [],
local: profile.audio.encoder.mapping,
};
} else {
profile.audio.encoder.mapping = {
global: [],
local: [],
...profile.audio.encoder.mapping,
};
}
profile.audio.decoder = {
coder: 'default',
settings: {},
mapping: [],
mapping: {},
...profile.audio.decoder,
};
if (Array.isArray(profile.audio.decoder.mapping)) {
profile.audio.decoder.mapping = {
global: [],
local: profile.audio.decoder.mapping,
};
} else {
profile.audio.decoder.mapping = {
global: [],
local: [],
...profile.audio.decoder.mapping,
};
}
profile.custom = {
selected: profile.audio.source === 1,
stream: profile.audio.source === 1 ? -2 : profile.audio.stream,

View File

@ -1427,7 +1427,7 @@ class Restreamer {
}
// Upsert the ingest process
async UpsertIngest(channelid, inputs, outputs, control) {
async UpsertIngest(channelid, global, inputs, outputs, control) {
const channel = this.GetChannel(channelid);
if (channel === null) {
return [null, { message: 'Unknown channel ID' }];
@ -1439,7 +1439,7 @@ class Restreamer {
reference: channel.channelid,
input: [],
output: [],
options: ['-err_detect', 'ignore_err'],
options: ['-err_detect', 'ignore_err', ...global],
autostart: control.process.autostart,
reconnect: control.process.reconnect,
reconnect_delay_seconds: parseInt(control.process.delay),
@ -2195,7 +2195,7 @@ class Restreamer {
}
// Update an egress process
async UpdateEgress(channelid, id, inputs, outputs, control) {
async UpdateEgress(channelid, id, global, inputs, outputs, control) {
const channel = this.GetChannel(channelid);
if (channel === null) {
return null;
@ -2226,7 +2226,7 @@ class Restreamer {
},
],
output: [],
options: ['-err_detect', 'ignore_err'],
options: ['-err_detect', 'ignore_err', ...global],
autostart: control.process.autostart,
reconnect: control.process.reconnect,
reconnect_delay_seconds: parseInt(control.process.delay),
@ -2252,7 +2252,7 @@ class Restreamer {
}
// Create an egress process
async CreateEgress(channelid, service, inputs, outputs, control) {
async CreateEgress(channelid, service, global, inputs, outputs, control) {
const channel = this.GetChannel(channelid);
if (channel === null) {
return ['', { message: 'Unknown channel ID' }];
@ -2269,7 +2269,7 @@ class Restreamer {
this.SetChannelEgress(channelid, egress.id, egress);
const [, err] = await this.UpdateEgress(channelid, egress.id, inputs, outputs, control);
const [, err] = await this.UpdateEgress(channelid, egress.id, global, inputs, outputs, control);
if (err !== null) {
this.DeleteChannelEgress(channelid, egress.id);
}

View File

@ -164,7 +164,7 @@ export default function Wizard(props) {
const profiles = data.profiles;
const control = data.control;
const [inputs, outputs] = M.createInputsOutputs(sources, profiles);
const [global, inputs, outputs] = M.createInputsOutputs(sources, profiles);
if (inputs.length === 0 || outputs.length === 0) {
notify.Dispatch('error', 'save:ingest', i18n._(t`The input profile is not complete. Please define a video and audio source.`));
@ -173,7 +173,7 @@ export default function Wizard(props) {
data.streams = M.createOutputStreams(sources, profiles);
const [, err] = await props.restreamer.UpsertIngest(_channelid, inputs, outputs, control);
const [, err] = await props.restreamer.UpsertIngest(_channelid, global, inputs, outputs, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', err.message);
return false;

View File

@ -284,7 +284,7 @@ export default function Edit(props) {
const profiles = $data.profiles;
const control = $data.control;
const [inputs, outputs] = M.createInputsOutputs(sources, profiles);
const [global, inputs, outputs] = M.createInputsOutputs(sources, profiles);
if (inputs.length === 0 || outputs.length === 0) {
notify.Dispatch('error', 'save:ingest', i18n._(t`The input profile is not complete. Please define a video and audio source.`));
@ -292,7 +292,7 @@ export default function Edit(props) {
}
// Create/update the ingest
const [, err] = await props.restreamer.UpsertIngest(_channelid, inputs, outputs, control);
const [, err] = await props.restreamer.UpsertIngest(_channelid, global, inputs, outputs, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest process (${err.message})`));
return false;

View File

@ -176,9 +176,9 @@ export default function Add(props) {
const handleServiceDone = async () => {
setSaving(true);
const [inputs, outputs] = helper.createInputsOutputs($sources, $settings.profiles, $settings.outputs);
const [global, inputs, outputs] = helper.createInputsOutputs($sources, $settings.profiles, $settings.outputs);
const [id, err] = await props.restreamer.CreateEgress(_channelid, $service, inputs, outputs, $settings.control);
const [id, err] = await props.restreamer.CreateEgress(_channelid, $service, global, inputs, outputs, $settings.control);
if (err !== null) {
setSaving(false);
notify.Dispatch('error', 'save:egress:' + $service, i18n._(t`Failed to create publication service (${err.message})`));
@ -380,20 +380,12 @@ export default function Add(props) {
<TabPanel value={$tab} index="general" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{service.name}
</Typography>
<Typography>v{service.version}</Typography>
</Stack>
</Stack>
@ -435,20 +427,12 @@ export default function Add(props) {
<TabPanel value={$tab} index="process" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{service.name}
</Typography>
<Typography>v{service.version}</Typography>
</Stack>
</Stack>
@ -480,20 +464,12 @@ export default function Add(props) {
<TabPanel value={$tab} index="encoding" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{service.name}
</Typography>
<Typography>v{service.version}</Typography>
</Stack>
</Stack>

View File

@ -218,9 +218,9 @@ export default function Edit(props) {
const handleServiceDone = async () => {
setSaving(true);
const [inputs, outputs] = helper.createInputsOutputs($sources, $settings.profiles, $settings.outputs);
const [global, inputs, outputs] = helper.createInputsOutputs($sources, $settings.profiles, $settings.outputs);
const [, err] = await props.restreamer.UpdateEgress(_channelid, id, inputs, outputs, $settings.control);
const [, err] = await props.restreamer.UpdateEgress(_channelid, id, global, inputs, outputs, $settings.control);
if (err !== null) {
setSaving(false);
notify.Dispatch('error', 'save:egress:' + _service, i18n._(t`Failed to store publication service (${err.message})`));
@ -394,20 +394,12 @@ export default function Edit(props) {
<TabPanel value={$tab} index="general" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{$service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{$service.name}
</Typography>
<Typography>v{$service.version}</Typography>
</Stack>
</Stack>
@ -443,20 +435,12 @@ export default function Edit(props) {
<TabPanel value={$tab} index="process" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{$service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{$service.name}
</Typography>
<Typography>v{$service.version}</Typography>
</Stack>
</Stack>
@ -516,20 +500,12 @@ export default function Edit(props) {
<TabPanel value={$tab} index="encoding" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ServiceIcon className={classes.serviceIcon} />
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={0}
>
<Typography variant="h1" className={classes.serviceName}>{$service.name}</Typography>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={0}>
<Typography variant="h1" className={classes.serviceName}>
{$service.name}
</Typography>
<Typography>v{$service.version}</Typography>
</Stack>
</Stack>

View File

@ -35,7 +35,7 @@ export function createSourcesFromStreams(streams) {
* @returns
*/
export function createInputsOutputs(sources, profiles, outputs) {
const [inpts, outpts] = M.createInputsOutputs(sources, profiles);
const [global, inpts, outpts] = M.createInputsOutputs(sources, profiles);
const out = [];
@ -58,7 +58,7 @@ export function createInputsOutputs(sources, profiles, outputs) {
out.push(o);
}
return [inpts, out];
return [global, inpts, out];
}
/**
* validateRequirements validates the requirements object the each