Merge branch 'dev'

This commit is contained in:
Ingo Oppermann 2022-11-24 18:25:38 +01:00
commit 8b402acbbe
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
90 changed files with 7209 additions and 4436 deletions

View File

@ -53,6 +53,7 @@ jobs:
build-args: |
PUBLIC_URL=./
NODE_IMAGE=${{ env.NODE_IMAGE }}
CADDY_IMAGE=${{ env.CADDY_IMAGE }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |

View File

@ -1,3 +1,4 @@
# RESTREAMER UI
RELEASE=1.5.0
NODE_IMAGE=node:19.0-alpine3.16
RELEASE=1.6.0
NODE_IMAGE=node:19.1-alpine3.16
CADDY_IMAGE=caddy:2.6.2-alpine

View File

@ -20,6 +20,7 @@
"pl",
"pt",
"es",
"ru"
"ru",
"ko"
]
}

View File

@ -1,8 +1,16 @@
# Restreamer-UI
## v1.5.1 > v1.6.0
- Add Bob Weaver Deinterlacing Filter ([#465](https://github.com/datarhei/restreamer/issues/465))
- Add tests for wizard, network source, and coders
- Add Korean translation (thanks to Jihaeng)
- Mod splitting wizard in components
- Fix wrong call to encoder defaults ([#467](https://github.com/datarhei/restreamer/issues/467))
## v1.5.0 > v1.5.1
- Fix FFmpeg version check for RTSP sources (datarhei/restreamer#455)
- Fix FFmpeg version check for RTSP sources ([#455](https://github.com/datarhei/restreamer/issues/455))
- Fix requires Core >= v16.11.0 and FFmpeg >= 5.1.0
## v1.4.0 > v1.5.0

View File

@ -1,7 +1,7 @@
{
"name": "restreamer-ui",
"version": "1.5.1",
"bundle": "restreamer-v2.4.1",
"version": "1.6.0",
"bundle": "restreamer-v2.4.2",
"private": false,
"license": "Apache-2.0",
"dependencies": {
@ -12,27 +12,27 @@
"@emotion/styled": "^11.10.5",
"@fontsource/dosis": "^4.5.10",
"@fontsource/roboto": "^4.5.8",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@lingui/core": "^3.15.0",
"@lingui/macro": "^3.15.0",
"@lingui/react": "^3.15.0",
"@mui/icons-material": "^5.10.9",
"@mui/lab": "^5.0.0-alpha.107",
"@mui/material": "^5.10.13",
"@mui/styles": "^5.10.10",
"@mui/icons-material": "^5.10.15",
"@mui/lab": "^5.0.0-alpha.109",
"@mui/material": "^5.10.15",
"@mui/styles": "^5.10.15",
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.25",
"babel-plugin-macros": "^3.1.0",
"eslint": "^8.27.0",
"eslint": "^8.28.0",
"handlebars": "^4.7.7",
"jwt-decode": "^3.1.2",
"make-plural": "^7.1.0",
"make-plural": "^7.2.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-device-detect": "^2.2.2",
@ -41,7 +41,7 @@
"react-router-dom": "^6.4.3",
"react-scripts": "5.0.1",
"semver": "^7.3.8",
"typescript": "^4.8.4",
"typescript": "^4.9.3",
"url-parse": "^1.5.10",
"uuid": "^9.0.0",
"video.js": "^7.20.3",
@ -51,6 +51,8 @@
"start": "react-scripts start",
"build": "react-scripts --optimize-for-size build",
"test": "react-scripts test",
"test-ci": "react-scripts test --watchAll=false --testTimeout 50000",
"test-coverage": "react-scripts test --watchAll=false --testTimeout 50000 --coverage",
"eject": "react-scripts eject",
"i18n-extract": "lingui extract",
"i18n-extract:clean": "lingui extract --clean",
@ -85,8 +87,8 @@
"@lingui/cli": "^3.15.0",
"babel-core": "^7.0.0-bridge.0",
"eslint-config-react-app": "^7.0.1",
"prettier": "^2.7.1",
"prettier": "^2.8.0",
"react-error-overlay": "^6.0.11"
},
"resolutions": {}
}
}

View File

@ -12,6 +12,7 @@ import { messages as IT } from './locales/it/messages.js';
import { messages as PL } from './locales/pl/messages.js';
import { messages as PT } from './locales/pt/messages.js';
import { messages as RU } from './locales/ru/messages.js';
import { messages as KO } from './locales/ko/messages.js';
import * as Storage from './utils/storage';
i18n.loadLocaleData('en', { plurals: plurals.en });
@ -22,6 +23,7 @@ i18n.loadLocaleData('it', { plurals: plurals.it });
i18n.loadLocaleData('pl', { plurals: plurals.pl });
i18n.loadLocaleData('pt', { plurals: plurals.pt });
i18n.loadLocaleData('ru', { plurals: plurals.ru });
i18n.loadLocaleData('ko', { plurals: plurals.ko });
i18n.load({
en: EN,
de: DE,
@ -31,6 +33,7 @@ i18n.load({
pl: PL,
pt: PT,
ru: RU,
ko: KO,
});
const getLanguage = (defaultLanguage, supportedLanguages) => {
@ -59,7 +62,7 @@ const getBrowserLanguage = (defaultLanguage) => {
return match[0].toLowerCase();
};
i18n.activate(getLanguage('en', ['en', 'de', 'es', 'fr', 'it', 'pl', 'pt', 'ru']));
i18n.activate(getLanguage('en', ['en', 'de', 'es', 'fr', 'it', 'pl', 'pt', 'ru', 'ko']));
export default function Provider(props) {
return <I18nProvider i18n={i18n}>{props.children}</I18nProvider>;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2933
src/locales/ko/messages.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ export default function EncodingSelect(props) {
const handleDecoderChange = (event) => {
const decoder = profile.decoder;
const stream = props.streams[profile.stream];
decoder.coder = event.target.value;
// If the coder changes, use the coder's default settings
@ -39,7 +40,7 @@ export default function EncodingSelect(props) {
}
if (c !== null) {
const defaults = c.defaults(props.skills);
const defaults = c.defaults(stream, props.skills);
decoder.settings = defaults.settings;
decoder.mapping = defaults.mapping;
}
@ -58,6 +59,7 @@ export default function EncodingSelect(props) {
const handleEncoderChange = (event) => {
const encoder = profile.encoder;
const stream = props.streams[profile.stream];
encoder.coder = event.target.value;
// If the coder changes, use the coder's default settings
@ -69,7 +71,7 @@ export default function EncodingSelect(props) {
}
if (c !== null) {
const defaults = c.defaults(props.skills);
const defaults = c.defaults(stream, props.skills);
encoder.settings = defaults.settings;
encoder.mapping = defaults.mapping;
}
@ -198,6 +200,8 @@ export default function EncodingSelect(props) {
}
}
// TODO: in case there's no decoder for a codec it should be mentioned.
return (
<Grid container spacing={2}>
<Grid item xs={12}>

View File

@ -42,14 +42,15 @@ export default function LanguageSelect(props) {
return (
<Select className={classes.root} variant="standard" displayEmpty value={i18n.locale} onChange={handleChange}>
<MenuItem value="en">English </MenuItem>
<MenuItem value="de">Deutsch </MenuItem>
<MenuItem value="es">Español </MenuItem>
<MenuItem value="fr">Français </MenuItem>
<MenuItem value="it">Italiano </MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="es">Español</MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="pl">Polski</MenuItem>
<MenuItem value="pt">Português </MenuItem>
<MenuItem value="ru">Русский </MenuItem>
<MenuItem value="pt">Português</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="ko">한국어</MenuItem>
</Select>
);
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: [],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = [];
const type = 'audio';
const hwaccel = false;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: [],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = [];
const type = 'video';
const hwaccel = false;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'h264_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -53,12 +60,12 @@ const codecs = ['h264'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'h264_mmal'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['h264'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'hevc_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['hevc'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mjpeg_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mjpeg'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mpeg1_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mpeg1'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mpeg2_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mpeg2'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mpeg2_mmal'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mpeg2'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mpeg4_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mpeg4'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'mpeg4_mmal'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['mpeg4'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -53,12 +60,12 @@ const codecs = ['h264', 'hevc', 'mpeg1', 'mpeg2', 'mpeg4', 'vp8', 'vp9', 'vc1'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'vc1_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['vc1'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'vc1_mmal'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['vc1'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'vp8_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['vp8'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-c:v', 'vp9_cuvid'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['vp9'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import Helper from '../../helper';
function init(initialState) {
const state = {
...initialState,
@ -8,7 +10,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const mapping = {
global: [],
local: ['-hwaccel', 'videotoolbox'],
@ -19,6 +24,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -27,7 +34,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -51,12 +58,12 @@ const codecs = ['h264'];
const type = 'video';
const hwaccel = true;
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'aac', '-b:a', `${settings.bitrate}k`, '-shortest'];
if (stream.codec === 'aac') {
@ -30,7 +34,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -39,7 +44,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -84,12 +89,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'aac_at', '-b:a', `${settings.bitrate}k`, '-shortest'];
if (stream.codec === 'aac') {
@ -30,7 +34,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -39,7 +44,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -84,12 +89,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,6 +1,11 @@
import React from 'react';
function createMapping(settings, stream) {
import Helper from '../../helper';
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'copy'];
//if (stream.codec === 'aac') {
@ -17,7 +22,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = {};
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -26,7 +32,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -54,12 +60,12 @@ function summarize(settings) {
return `${name}`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = {};
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'libopus', '-b:a', `${settings.bitrate}k`, '-shortest'];
const mapping = {
@ -26,7 +30,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -35,7 +40,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -80,12 +85,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'libvorbis', '-b:a', `${settings.bitrate}k`, '-shortest'];
const mapping = {
@ -26,7 +30,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -35,7 +40,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -80,12 +85,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
// '-qscale:a', '6'
const local = ['-codec:a', 'libmp3lame', '-b:a', `${settings.bitrate}k`, '-shortest'];
@ -27,7 +31,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -36,7 +41,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -81,12 +86,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,6 +1,11 @@
import React from 'react';
function createMapping(settings, stream) {
import Helper from '../../helper';
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-an'];
const mapping = {
@ -13,7 +18,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = {};
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -22,7 +28,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -50,12 +56,12 @@ function summarize(settings) {
return `${name}`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = {};
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans, t } from '@lingui/macro';
import Audio from '../../settings/Audio';
import SelectCustom from '../../../../misc/SelectCustom';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -19,7 +20,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
let sampling = settings.sampling;
let layout = settings.layout;
@ -90,7 +94,8 @@ Delay.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -99,7 +104,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -147,12 +152,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Audio from '../../settings/Audio';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -13,7 +14,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, stream) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:a', 'vorbis', '-b:a', `${settings.bitrate}k`, '-qscale:a', '3', '-shortest'];
const mapping = {
@ -26,7 +30,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = init(props.settings);
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -35,7 +40,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -80,12 +85,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,6 +1,11 @@
import React from 'react';
function createMapping(settings) {
import Helper from '../../helper';
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:v', 'copy'];
const mapping = {
@ -13,6 +18,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = {};
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -21,7 +28,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -43,10 +50,10 @@ function summarize(settings) {
return `${name}`;
}
function defaults() {
function defaults(stream, skills) {
return {
settings: {},
mapping: createMapping({}),
mapping: createMapping({}, stream, skills),
};
}

View File

@ -7,6 +7,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -23,7 +24,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = [
'-codec:v',
'h264_nvenc',
@ -163,6 +167,8 @@ RateControl.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -171,7 +177,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -232,12 +238,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -3,6 +3,7 @@ import React from 'react';
import Grid from '@mui/material/Grid';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -16,7 +17,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = [
'-codec:v',
'h264_omx',
@ -50,6 +54,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -58,7 +64,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -110,12 +116,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -9,6 +9,7 @@ import { Trans } from '@lingui/macro';
import BoxText from '../../../BoxText';
import TextField from '../../../TextField';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -75,7 +76,10 @@ Codec Controls
4: High
*/
function createMapping(settings, skills) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
let ffversion = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
@ -131,8 +135,11 @@ function createMapping(settings, skills) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
let ffversion = 4;
if (SemverSatisfies(props.skills.ffmpeg.version, '^5.0.0')) {
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
}
@ -143,7 +150,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, props.skills), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -216,12 +223,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults(skills) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, skills),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -23,7 +24,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const global = [];
const local = [];
@ -97,6 +101,8 @@ Profile.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -105,7 +111,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -163,12 +169,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -7,6 +7,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -21,7 +22,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = [
'-codec:v',
'h264_videotoolbox',
@ -76,6 +80,8 @@ Entropy.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -84,7 +90,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -139,12 +145,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -23,7 +24,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const global = [];
const local = [];
@ -97,6 +101,8 @@ Profile.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -105,7 +111,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -163,12 +169,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,6 +1,11 @@
import React from 'react';
function createMapping(settings, stream) {
import Helper from '../../helper';
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-vn'];
const mapping = {
@ -13,7 +18,8 @@ function createMapping(settings, stream) {
function Coder(props) {
const settings = {};
const stream = props.stream;
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -22,7 +28,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, stream), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -50,12 +56,12 @@ function summarize(settings) {
return `${name}`;
}
function defaults(stream) {
function defaults(stream, skills) {
const settings = {};
return {
settings: settings,
mapping: createMapping(settings, stream),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -1,6 +1,11 @@
import React from 'react';
function createMapping(settings) {
import Helper from '../../helper';
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const local = ['-codec:v', 'rawvideo'];
const mapping = {
@ -13,6 +18,8 @@ function createMapping(settings) {
function Coder(props) {
const settings = {};
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -21,7 +28,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
React.useEffect(() => {
@ -43,10 +50,10 @@ function summarize(settings) {
return `${name}`;
}
function defaults() {
function defaults(stream, skills) {
return {
settings: {},
mapping: createMapping({}),
mapping: createMapping({}, stream, skills),
};
}

View File

@ -4,6 +4,7 @@ import SemverSatisfies from 'semver/functions/satisfies';
import Grid from '@mui/material/Grid';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -17,7 +18,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, skills) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
let ffversion = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
@ -63,8 +67,11 @@ function createMapping(settings, skills) {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
let ffversion = 4;
if (SemverSatisfies(props.skills.ffmpeg.version, '^5.0.0')) {
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
}
@ -75,7 +82,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, props.skills), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -129,12 +136,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Preset: ${settings.preset}, Profile: ${settings.profile}`;
}
function defaults(skills) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, skills),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -23,7 +24,10 @@ function init(initialState) {
return state;
}
function createMapping(settings) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
const global = [];
const local = [];
@ -97,6 +101,8 @@ Profile.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
const handleChange = (newSettings) => {
let automatic = false;
@ -105,7 +111,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -163,12 +169,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -24,7 +25,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, skills) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
let ffversion = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
@ -120,8 +124,11 @@ Tune.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
let ffversion = 4;
if (SemverSatisfies(props.skills.ffmpeg.version, '^5.0.0')) {
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
}
@ -132,7 +139,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, props.skills), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -195,12 +202,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Preset: ${settings.preset}, Profile: ${settings.profile}`;
}
function defaults(skills) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, skills),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -8,6 +8,7 @@ import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
import Helper from '../../helper';
function init(initialState) {
const state = {
@ -24,7 +25,10 @@ function init(initialState) {
return state;
}
function createMapping(settings, skills) {
function createMapping(settings, stream, skills) {
stream = Helper.InitStream(stream);
skills = Helper.InitSkills(skills);
let ffversion = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
@ -120,8 +124,11 @@ Tune.defaultProps = {
function Coder(props) {
const settings = init(props.settings);
const stream = Helper.InitStream(props.stream);
const skills = Helper.InitSkills(props.skills);
let ffversion = 4;
if (SemverSatisfies(props.skills.ffmpeg.version, '^5.0.0')) {
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffversion = 5;
}
@ -132,7 +139,7 @@ function Coder(props) {
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings, props.skills), automatic);
props.onChange(newSettings, createMapping(newSettings, stream, skills), automatic);
};
const update = (what) => (event) => {
@ -195,12 +202,12 @@ function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Preset: ${settings.preset}, Profile: ${settings.profile}`;
}
function defaults(skills) {
function defaults(stream, skills) {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings, skills),
mapping: createMapping(settings, stream, skills),
};
}

View File

@ -22,7 +22,7 @@ export { coder, name, codecs, type, hwaccel, defaults, Coder as component };
### defaults
The `defaults()` function returns the default settings and mappings for this decoder. It is an object of this shape:
The `defaults(stream, skills)` function returns the default settings and mappings for this decoder. It is an object of this shape:
```
{
@ -80,7 +80,7 @@ export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as compon
### defaults
The `defaults()` function returns the default settings and mappings for this encoder. It is an object of this shape:
The `defaults(stream, skills)` function returns the default settings and mappings for this encoder. It is an object of this shape:
```
{

View File

@ -0,0 +1,35 @@
import React from 'react';
import { render, act } from '../../utils/testing';
import '@testing-library/jest-dom';
import * as Decoders from './Decoders';
import * as Encoders from './Encoders';
const audiodecoders = Decoders.Audio.List();
const videodecoders = Decoders.Video.List();
const audioencoders = Encoders.Audio.List();
const videoencoders = Encoders.Video.List();
const testfunc = async (coder) => {
const defaults = coder.defaults();
let $settings = {};
let $mapping = {};
const handleChange = (settings, mapping) => {
$settings = settings;
$mapping = mapping;
};
await act(async () => {
render(<coder.component onChange={handleChange} />);
});
expect($settings).toStrictEqual(defaults.settings);
expect($mapping).toStrictEqual(defaults.mapping);
};
test.each(audiodecoders)('coder:decoder:$type $coder', testfunc);
test.each(videodecoders)('coder:decoder:$type $coder', testfunc);
test.each(audioencoders)('coder:encoder:$type $coder', testfunc);
test.each(videoencoders)('coder:encoder:$type $coder', testfunc);

View File

@ -0,0 +1,35 @@
function InitStream(initialStream) {
if (!initialStream) {
initialStream = {};
}
let stream = {
codec: '',
...initialStream,
};
return stream;
}
function InitSkills(initialSkills) {
if (!initialSkills) {
initialSkills = {};
}
let skills = {
ffmpeg: {},
...initialSkills,
};
skills.ffmpeg = {
version: '5.0.0',
...skills.ffmpeg,
};
return skills;
}
export default {
InitStream,
InitSkills,
};

View File

@ -9,6 +9,7 @@ import * as Scale from './video/Scale';
import * as Transpose from './video/Transpose';
import * as HFlip from './video/HFlip';
import * as VFlip from './video/VFlip';
import * as Bwdif from './video/Bwdif';
// Register filters type: audio/video
class Registry {
@ -56,6 +57,7 @@ videoRegistry.Register(Scale);
videoRegistry.Register(Transpose);
videoRegistry.Register(HFlip);
videoRegistry.Register(VFlip);
videoRegistry.Register(Bwdif);
// Export registrys for ../SelectFilters.js
export { audioRegistry as Audio, videoRegistry as Video };

View File

@ -0,0 +1,170 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Checkbox from '../../Checkbox';
import Select from '../../Select';
// Deinterlace the input video ("bwdif" stands for "Bob Weaver Deinterlacing Filter").
// http://ffmpeg.org/ffmpeg-all.html#bwdif
function init(initialState) {
const state = {
enabled: false,
mode: '1',
parity: '-1',
deint: '0',
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
if (settings.enabled) {
mapping.push(`bwdif=mode=${settings.mode}:parity=${settings.parity}:deint=${settings.deint}`);
}
return mapping.join(',');
}
function Mode(props) {
return (
<Select label={<Trans>Deinterlace mode</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="0">
<Trans>Each frames</Trans>
</MenuItem>
<MenuItem value="1">
<Trans>Each field</Trans>
</MenuItem>
</Select>
);
}
Mode.defaultProps = {
value: '',
onChange: function (event) {},
};
function Parity(props) {
return (
<Select label={<Trans>Deinterlace parity</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="0">
<Trans>Top field</Trans>
</MenuItem>
<MenuItem value="1">
<Trans>Bottom field</Trans>
</MenuItem>
<MenuItem value="-1">
<Trans>Auto</Trans>
</MenuItem>
</Select>
);
}
Parity.defaultProps = {
value: '',
onChange: function (event) {},
};
function Deint(props) {
return (
<Select label={<Trans>Deinterlace deint</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="0">
<Trans>All frames</Trans>
</MenuItem>
<MenuItem value="1">
<Trans>Marked frames</Trans>
</MenuItem>
</Select>
);
}
Deint.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,
};
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>Deinterlace (bwdif)</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
{settings.enabled && (
<React.Fragment>
<Grid item xs={12}>
<Mode value={settings.mode} onChange={update('mode')}></Mode>
</Grid>
<Grid item xs={6}>
<Parity value={settings.parity} onChange={update('parity')}></Parity>
</Grid>
<Grid item xs={6}>
<Deint value={settings.deint} onChange={update('deint')}></Deint>
</Grid>
</React.Fragment>
)}
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, mapping) {},
};
const filter = 'bwdif';
const name = 'Deinterlacing Filter';
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, createGraph, Filter as component };

View File

@ -92,7 +92,7 @@ function Filter(props) {
return (
<React.Fragment>
<Grid item xs={settings.mode === 'none' ? 12 : 4}>
<Mode allowNone allowCustom label={<Trans>Scale</Trans>} value={settings.mode} onChange={update('mode')}></Mode>
<Mode value={settings.mode} onChange={update('mode')}></Mode>
</Grid>
{settings.mode === 'fix' && (
<Grid item xs={8}>

View File

@ -17,11 +17,11 @@ const topics = {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/edit-livestream/license',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/streameinstellungen/lizenz',
},
'login': {
login: {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/login',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/dashboard',
},
'main': {
main: {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/main-screen',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/hauptbildschirm',
},
@ -73,7 +73,7 @@ const topics = {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/process-report',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/prozess-details',
},
'publication': {
publication: {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/publications',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/publication-services',
},

92
src/utils/testing.js Normal file
View File

@ -0,0 +1,92 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import theme from '../theme';
import I18n from '../I18n';
/*
This is wrapper for the render method in order to provide all required providers and
not to repeat them for each test.
If you want to debug a test by having a look at the current output of the component,
simply import the "screen" object from this file and use the "debug()" method:
test('displays the header and paragraph text', () => {
render(<Travel />)
screen.debug()
})
or of a specific element in the component:
test('displays the header and paragraph text', () => {
render(<Travel />)
const header = screen.getByRole('heading', { name: /travel anywhere/i })
screen.debug(header)
})
More about "render" and "screen" on https://testing-library.com/docs
For testing certain conditions, check out https://jestjs.io/docs/expect and for
running tests, check out https://jestjs.io/docs/api.
As an alternative to fireEvent there is the userEvent which may simulate better
a user clicking around. Example:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("radio", () => {
const user = userEvent.setup();
render(
<form>
<label>
First <input type="radio" name="radio1" value="first" />
</label>
<label>
Second <input type="radio" name="radio1" value="second" />
</label>
</form>
)
await user.click(screen.getByLabelText("Second"));
});
*/
const NoRoute = (props) => {
return null;
};
const AllTheProviders =
(initialEntries, path) =>
({ children }) => {
if (typeof initialEntries === 'undefined') {
initialEntries = '/';
}
if (typeof path === 'undefined') {
path = '/';
}
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<I18n>
<MemoryRouter initialEntries={[initialEntries]}>
<Routes>
<Route path={path} element={children} />
<Route path="*" element={<NoRoute />} />
</Routes>
</MemoryRouter>
</I18n>
</ThemeProvider>
</StyledEngineProvider>
);
};
const customRender = (ui, options, initialEntries, path) => render(ui, { wrapper: AllTheProviders(initialEntries, path), ...options });
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };

View File

@ -108,7 +108,7 @@ const initConfig = (initialConfig) => {
local: 'localhost',
token: '',
passphrase: '',
name: '',
name: 'external',
...config.srt,
};
@ -152,7 +152,9 @@ const initSkills = (initialSkills) => {
};
if (skills.formats.demuxers.includes('rtsp')) {
skills.protocols.input.push('rtsp');
if (!skills.protocols.input.includes('rtsp')) {
skills.protocols.input.push('rtsp');
}
}
return skills;
@ -181,8 +183,10 @@ const createInputs = (settings, config, skills) => {
if (settings.mode === 'push') {
if (settings.push.type === 'hls') {
input.address = getLocalHLS(config);
input.options.push('-analyzeduration', '20000000');
} else if (settings.push.type === 'rtmp') {
input.address = getLocalRTMP(config);
input.options.push('-analyzeduration', '3000000');
} else if (settings.push.type === 'srt') {
input.address = getLocalSRT(config);
} else {
@ -315,14 +319,14 @@ const isAuthProtocol = (url) => {
const isSupportedProtocol = (url, supportedProtocols) => {
const protocol = getProtocol(url);
if (protocol.length === 0) {
return 0;
return false;
}
if (!supportedProtocols.includes(protocol)) {
return -1;
return false;
}
return 1;
return true;
};
const getHLSAddress = (host, credentials, name, secure) => {
@ -406,6 +410,7 @@ function Pull(props) {
const settings = props.settings;
const protocolClass = getProtocolClass(settings.address);
const authProtocol = isAuthProtocol(settings.address);
const validURL = isValidURL(settings.address);
const supportedProtocol = isSupportedProtocol(settings.address, props.skills.protocols.input);
return (
@ -428,69 +433,133 @@ function Pull(props) {
<Trans>Supports HTTP (HLS, DASH), RTP, RTSP, RTMP, SRT and more.</Trans>
</Typography>
</Grid>
{supportedProtocol === -1 && (
<Grid item xs={12} align="center">
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography>
<Trans>This protocol is unknown or not supported by the available FFmpeg binary.</Trans>
</Typography>
</BoxText>
</Grid>
)}
{supportedProtocol === 1 && (
{validURL === true && (
<React.Fragment>
{authProtocol && (
<React.Fragment>
<Grid item md={6} xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Username</Trans>}
value={settings.username}
onChange={props.onChange('', 'username')}
/>
<Typography variant="caption">
<Trans>Username for the device.</Trans>
</Typography>
</Grid>
<Grid item md={6} xs={12}>
<Password
variant="outlined"
fullWidth
label={<Trans>Password</Trans>}
value={settings.password}
onChange={props.onChange('', 'password')}
/>
<Typography variant="caption">
<Trans>Password for the device.</Trans>
</Typography>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<Accordion className="accordion">
<AccordionSummary elevation={0} expandIcon={<ArrowDropDownIcon />}>
{!supportedProtocol ? (
<Grid item xs={12} align="center">
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography>
<Trans>Advanced settings</Trans>
<Trans>This protocol is unknown or not supported by the available FFmpeg binary.</Trans>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{protocolClass === 'rtsp' && (
<React.Fragment>
</BoxText>
</Grid>
) : (
<React.Fragment>
{authProtocol && (
<React.Fragment>
<Grid item md={6} xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Username</Trans>}
value={settings.username}
onChange={props.onChange('', 'username')}
/>
<Typography variant="caption">
<Trans>Username for the device.</Trans>
</Typography>
</Grid>
<Grid item md={6} xs={12}>
<Password
variant="outlined"
fullWidth
label={<Trans>Password</Trans>}
value={settings.password}
onChange={props.onChange('', 'password')}
/>
<Typography variant="caption">
<Trans>Password for the device.</Trans>
</Typography>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<Accordion className="accordion">
<AccordionSummary elevation={0} expandIcon={<ArrowDropDownIcon />}>
<Typography>
<Trans>Advanced settings</Trans>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{protocolClass === 'rtsp' && (
<React.Fragment>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>RTSP</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>UDP transport</Trans>}
checked={settings.rtsp.udp}
onChange={props.onChange('rtsp', 'udp')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
type="number"
min="0"
step="1"
fullWidth
label={<Trans>Socket timeout (microseconds)</Trans>}
value={settings.rtsp.stimeout}
onChange={props.onChange('rtsp', 'stimeout')}
/>
</Grid>
</React.Fragment>
)}
{protocolClass === 'http' && (
<React.Fragment>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>HTTP and HTTPS</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Read input at native speed</Trans>}
checked={settings.http.readNative}
onChange={props.onChange('http', 'readNative')}
/>
<Checkbox
label={<Trans>Force input framerate</Trans>}
checked={settings.http.forceFramerate}
onChange={props.onChange('http', 'forceFramerate')}
/>
</Grid>
{settings.http.forceFramerate === true && (
<Grid item xs={12}>
<TextField
variant="outlined"
type="number"
min="0"
step="1"
fullWidth
label={<Trans>Framerate</Trans>}
value={settings.http.framerate}
onChange={props.onChange('http', 'framerate')}
/>
</Grid>
)}
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label="User-Agent"
value={settings.http.userAgent}
onChange={props.onChange('http', 'userAgent')}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<Typography variant="h3">
<Trans>RTSP</Trans>
<Trans>General</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>UDP transport</Trans>}
checked={settings.rtsp.udp}
onChange={props.onChange('rtsp', 'udp')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
@ -498,95 +567,39 @@ function Pull(props) {
min="0"
step="1"
fullWidth
label={<Trans>Socket timeout (microseconds)</Trans>}
value={settings.rtsp.stimeout}
onChange={props.onChange('rtsp', 'stimeout')}
label="thread_queue_size"
value={settings.general.thread_queue_size}
onChange={props.onChange('general', 'thread_queue_size')}
/>
</Grid>
</React.Fragment>
)}
{protocolClass === 'http' && (
<React.Fragment>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>HTTP and HTTPS</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Read input at native speed</Trans>}
checked={settings.http.readNative}
onChange={props.onChange('http', 'readNative')}
/>
<Checkbox
label={<Trans>Force input framerate</Trans>}
checked={settings.http.forceFramerate}
onChange={props.onChange('http', 'forceFramerate')}
/>
<MultiSelect
type="select"
label="flags"
value={settings.general.fflags}
onChange={props.onChange('general', 'fflags')}
>
<MultiSelectOption value="discardcorrupt" name="discardcorrupt" />
<MultiSelectOption value="fastseek" name="fastseek" />
<MultiSelectOption value="genpts" name="genpts" />
<MultiSelectOption value="igndts" name="igndts" />
<MultiSelectOption value="ignidx" name="ignidx" />
<MultiSelectOption value="nobuffer" name="nobuffer" />
<MultiSelectOption value="nofillin" name="nofillin" />
<MultiSelectOption value="noparse" name="noparse" />
<MultiSelectOption value="sortdts" name="sortdts" />
</MultiSelect>
</Grid>
{settings.http.forceFramerate === true && (
<Grid item xs={12}>
<TextField
variant="outlined"
type="number"
min="0"
step="1"
fullWidth
label={<Trans>Framerate</Trans>}
value={settings.http.framerate}
onChange={props.onChange('http', 'framerate')}
/>
</Grid>
)}
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label="User-Agent"
value={settings.http.userAgent}
onChange={props.onChange('http', 'userAgent')}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<Typography variant="h3">
<Trans>General</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
type="number"
min="0"
step="1"
fullWidth
label="thread_queue_size"
value={settings.general.thread_queue_size}
onChange={props.onChange('general', 'thread_queue_size')}
/>
</Grid>
<Grid item xs={12}>
<MultiSelect type="select" label="flags" value={settings.general.fflags} onChange={props.onChange('general', 'fflags')}>
<MultiSelectOption value="discardcorrupt" name="discardcorrupt" />
<MultiSelectOption value="fastseek" name="fastseek" />
<MultiSelectOption value="genpts" name="genpts" />
<MultiSelectOption value="igndts" name="igndts" />
<MultiSelectOption value="ignidx" name="ignidx" />
<MultiSelectOption value="nobuffer" name="nobuffer" />
<MultiSelectOption value="nofillin" name="nofillin" />
<MultiSelectOption value="noparse" name="noparse" />
<MultiSelectOption value="sortdts" name="sortdts" />
</MultiSelect>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
</React.Fragment>
)}
</React.Fragment>
)}
<Grid item xs={12}>
<FormInlineButton disabled={!isValidURL(settings.address) || supportedProtocol <= 0} onClick={props.onProbe}>
<FormInlineButton disabled={!validURL || !supportedProtocol} onClick={props.onProbe}>
<Trans>Probe</Trans>
</FormInlineButton>
</Grid>
@ -602,7 +615,7 @@ function Push(props) {
const supportsRTMP = isSupportedProtocol('rtmp://', props.skills.protocols.input);
const supportsSRT = isSupportedProtocol('srt://', props.skills.protocols.input);
if (!supportsRTMP && supportsSRT) {
if (!supportsRTMP && !supportsSRT) {
return (
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12} align="center">

View File

@ -0,0 +1,330 @@
import React from 'react';
import { render, fireEvent } from '../../../utils/testing';
import '@testing-library/jest-dom';
import * as Network from './Network';
const $skills_ffmpeg5 = {
ffmpeg: {
version: '5.1.2',
},
formats: {
demuxers: ['rtsp'],
},
protocols: {
input: ['http', 'https', 'rtmp', 'rtmps', 'srt'],
},
};
const $skills_ffmpeg4 = {
ffmpeg: {
version: '4.4.1',
},
formats: {
demuxers: ['rtsp'],
},
protocols: {
input: ['http', 'https', 'rtmp', 'rtmps', 'srt'],
},
};
const $config = {
rtmp: {
enabled: true,
app: '/live',
token: 'foobar',
},
srt: {
enabled: true,
token: 'foobar',
passphrase: 'bazfoobazfoo',
},
};
test('source:network pull', async () => {
let $settings = {
mode: 'pull',
};
const handleChange = (settings) => {
$settings = settings;
};
const Source = Network.component;
let { getByLabelText, queryByText, rerender } = render(<Source onChange={handleChange} />);
expect(queryByText(`This protocol is unknown or not supported by the available FFmpeg binary.`)).toBe(null);
const input = getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/stream' } });
expect($settings.mode).toBe('pull');
expect($settings.address).toBe('rtsp://127.0.0.1/live/stream');
rerender(<Source settings={$settings} onChange={handleChange} />);
expect(queryByText(`This protocol is unknown or not supported by the available FFmpeg binary.`)).toBeInTheDocument();
rerender(<Source settings={$settings} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect(queryByText(`This protocol is unknown or not supported by the available FFmpeg binary.`)).toBe(null);
});
const pullmatrix = {
settings: {
mode: 'pull',
address: '',
username: 'admin',
password: 'foobar',
rtsp: {
udp: false,
stimeout: 5000000,
},
http: {
readNative: true,
forceFramerate: true,
framerate: 25,
userAgent: 'foobaz/1',
},
general: {
fflags: ['genpts'],
thread_queue_size: 512,
},
},
tests: [],
};
pullmatrix.tests = [
{
name: 'RTSP',
settings: { ...pullmatrix.settings, address: 'rtsp://127.0.0.1/live/stream' },
skills: $skills_ffmpeg4,
input: {
address: 'rtsp://admin:foobar@127.0.0.1/live/stream',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-stimeout', 5000000, '-rtsp_transport', 'tcp'],
},
},
{
name: 'RTMP',
settings: { ...pullmatrix.settings, address: 'rtmp://127.0.0.1/live/stream' },
skills: $skills_ffmpeg4,
input: {
address: 'rtmp://admin:foobar@127.0.0.1/live/stream',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '3000000'],
},
},
{
name: 'HTTP',
settings: { ...pullmatrix.settings, address: 'http://127.0.0.1/live/stream.m3u8' },
skills: $skills_ffmpeg4,
input: {
address: 'http://admin:foobar@127.0.0.1/live/stream.m3u8',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '20000000', '-re', '-r', 25, '-user_agent', 'foobaz/1'],
},
},
{
name: 'SRT',
settings: { ...pullmatrix.settings, address: 'srt://127.0.0.1?mode=caller&streamid=foobar' },
skills: $skills_ffmpeg4,
input: {
address: 'srt://127.0.0.1?mode=caller&streamid=foobar',
options: ['-fflags', '+genpts', '-thread_queue_size', 512],
},
},
{
name: 'RTSP',
settings: { ...pullmatrix.settings, address: 'rtsp://127.0.0.1/live/stream' },
skills: $skills_ffmpeg5,
input: {
address: 'rtsp://admin:foobar@127.0.0.1/live/stream',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-timeout', 5000000, '-rtsp_transport', 'tcp'],
},
},
{
name: 'RTMP',
settings: { ...pullmatrix.settings, address: 'rtmp://127.0.0.1/live/stream' },
skills: $skills_ffmpeg5,
input: {
address: 'rtmp://admin:foobar@127.0.0.1/live/stream',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '3000000'],
},
},
{
name: 'HTTP',
settings: { ...pullmatrix.settings, address: 'http://127.0.0.1/live/stream.m3u8' },
skills: $skills_ffmpeg5,
input: {
address: 'http://admin:foobar@127.0.0.1/live/stream.m3u8',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '20000000', '-re', '-r', 25, '-user_agent', 'foobaz/1'],
},
},
{
name: 'SRT',
settings: { ...pullmatrix.settings, address: 'srt://127.0.0.1?mode=caller&streamid=foobar' },
skills: $skills_ffmpeg5,
input: {
address: 'srt://127.0.0.1?mode=caller&streamid=foobar',
options: ['-fflags', '+genpts', '-thread_queue_size', 512],
},
},
];
test.each(pullmatrix.tests)('source:network pull $name input with ffmpeg $skills.ffmpeg.version', async (data) => {
let $inputs = [];
const handleProbe = (_, inputs) => {
$inputs = inputs;
};
const Source = Network.component;
let { getByText, getByRole } = render(<Source settings={data.settings} skills={data.skills} onProbe={handleProbe} />);
expect(getByText('Probe')).toBeInTheDocument();
const button = getByRole('button', { name: 'Probe' });
fireEvent.click(button, { bubbles: true });
expect($inputs.length).toBe(1);
expect($inputs[0]).toStrictEqual(data.input);
});
test('source:network push', async () => {
let $settings = {
mode: 'push',
push: {
type: 'rtmp',
},
};
const handleChange = (settings) => {
$settings = settings;
};
const Source = Network.component;
let { queryByText, rerender } = render(<Source settings={$settings} onChange={handleChange} />);
expect($settings.mode).toBe('push');
expect(queryByText(`The available FFmpeg binary doesn't support any of the required protocols.`)).toBeInTheDocument();
rerender(<Source settings={$settings} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect(queryByText(`The available FFmpeg binary doesn't support any of the required protocols.`)).toBe(null);
});
test('source:network push RTMP', async () => {
let $settings = {
mode: 'push',
push: {
type: 'rtmp',
},
};
const handleChange = (settings) => {
$settings = settings;
};
const Source = Network.component;
let { getByText, queryByText, rerender } = render(<Source settings={$settings} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect($settings.mode).toBe('push');
expect($settings.push.type).toBe('rtmp');
expect(queryByText(`Enable RTMP server ...`)).toBeInTheDocument();
rerender(<Source settings={$settings} config={$config} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect(getByText('Probe')).toBeInTheDocument();
});
test('source:network push SRT', async () => {
let $settings = {
mode: 'push',
push: {
type: 'srt',
},
};
const handleChange = (settings) => {
$settings = settings;
};
const Source = Network.component;
let { getByText, queryByText, rerender } = render(<Source settings={$settings} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect($settings.mode).toBe('push');
expect($settings.push.type).toBe('srt');
expect(queryByText(`Enable SRT server ...`)).toBeInTheDocument();
rerender(<Source settings={$settings} config={$config} skills={$skills_ffmpeg5} onChange={handleChange} />);
expect(getByText('Probe')).toBeInTheDocument();
});
const pushmatrix = {
settings: {
mode: 'push',
push: {
type: '',
},
},
tests: [],
};
pushmatrix.tests = [
{
name: 'RTMP',
settings: { ...pushmatrix.settings, push: { ...pushmatrix.push, type: 'rtmp' } },
skills: $skills_ffmpeg4,
config: $config,
input: {
address: 'rtmp://localhost/live/external.stream?token=foobar',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '3000000'],
},
},
{
name: 'SRT',
settings: { ...pushmatrix.settings, push: { ...pushmatrix.push, type: 'srt' } },
skills: $skills_ffmpeg4,
config: $config,
input: {
address: 'srt://localhost?mode=caller&transtype=live&streamid=external,mode:request,token:foobar&passphrase=bazfoobazfoo',
options: ['-fflags', '+genpts', '-thread_queue_size', 512],
},
},
{
name: 'RTMP',
settings: { ...pushmatrix.settings, push: { ...pushmatrix.push, type: 'rtmp' } },
skills: $skills_ffmpeg5,
config: $config,
input: {
address: 'rtmp://localhost/live/external.stream?token=foobar',
options: ['-fflags', '+genpts', '-thread_queue_size', 512, '-analyzeduration', '3000000'],
},
},
{
name: 'SRT',
settings: { ...pushmatrix.settings, push: { ...pushmatrix.push, type: 'srt' } },
skills: $skills_ffmpeg5,
config: $config,
input: {
address: 'srt://localhost?mode=caller&transtype=live&streamid=external,mode:request,token:foobar&passphrase=bazfoobazfoo',
options: ['-fflags', '+genpts', '-thread_queue_size', 512],
},
},
];
test.each(pushmatrix.tests)('source:network push $name input with ffmpeg $skills.ffmpeg.version', async (data) => {
let $inputs = [];
const handleProbe = (_, inputs) => {
$inputs = inputs;
};
const Source = Network.component;
let { getByText, getByRole } = render(<Source settings={data.settings} config={data.config} skills={data.skills} onProbe={handleProbe} />);
expect(getByText('Probe')).toBeInTheDocument();
const button = getByRole('button', { name: 'Probe' });
fireEvent.click(button, { bubbles: true });
expect($inputs.length).toBe(1);
expect($inputs[0]).toStrictEqual(data.input);
});

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Abort(props) {
return (
<Paper xs={12} sm={8} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={3} variant="h1" title={<Trans>Abort</Trans>} onHelp={props.onHelp} />
<Grid container spacing={3}>
{props.nchannels <= 1 ? (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>You can't abort the wizard because at least one input must be defined.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Button variant="outlined" color="primary" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>Are you sure you want to abort the wizard?</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<Button variant="outlined" color="default" fullWidth onClick={props.onNext}>
<Trans>Yes</Trans>
</Button>
</Grid>
<Grid item xs={6}>
<Button variant="outlined" color="primary" fullWidth onClick={props.onBack}>
<Trans>No</Trans>
</Button>
</Grid>
</React.Fragment>
)}
</Grid>
</Paper>
);
}
Abort.defaultProps = {
onHelp: () => {},
onBack: () => {},
onNext: () => {},
nchannels: 0,
};

View File

@ -0,0 +1,136 @@
import React from 'react';
import { Trans, t } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import WarningIcon from '@mui/icons-material/Warning';
import BoxText from '../../../misc/BoxText';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
import Select from '../../../misc/Select';
export default function Audio(props) {
const { i18n } = useLingui();
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Audio setup</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
{props.status === 'error' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>Failed to verify the source. Please check the address.</Trans>
</Typography>
</BoxText>
)}
{props.status === 'nostream' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any audio streams.</Trans>
</Typography>
</BoxText>
)}
{props.status === 'nocoder' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any compatible audio streams.</Trans>
</Typography>
</BoxText>
)}
</Grid>
<Grid item xs={12}>
<RadioGroup row value={props.source} onChange={props.onSource}>
<Grid container spacing={2}>
{props.streamList.length === 0 && (
<Grid item xs={12}>
<Typography>
<Trans>
The video source doesn't provide any compatible audio stream. <strong>Silence audio</strong> is recommended.
Services e.g. YouTube, Facebook &amp; Co. require an audio channel.
</Trans>
</Typography>
</Grid>
)}
{props.streamList.length !== 0 && (
<React.Fragment>
<Grid item xs={12}>
<FormControlLabel value="video" control={<Radio />} label={i18n._(t`Audio from device`)} />
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Stream</Trans>} value={props.stream} onChange={props.onAudioStreamChange}>
{props.streamList}
</Select>
</Grid>
</React.Fragment>
)}
{props.deviceList.length !== 0 && (
<React.Fragment>
<Grid item xs={12}>
<FormControlLabel value="alsa" control={<Radio />} label={i18n._(t`Audio from device`)} />
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Device</Trans>} value={props.address} onChange={props.onAudioDeviceChange}>
{props.deviceList}
</Select>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<div>
<FormControlLabel value="silence" control={<Radio />} label={i18n._(t`Silence Audio`)} />
</div>
<div>
<FormControlLabel value="none" control={<Radio />} label={i18n._(t`No audio`)} />
</div>
</Grid>
</Grid>
</RadioGroup>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" onClick={props.onNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
Audio.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onBack: () => {},
onNext: () => {},
onSource: () => {},
source: '',
onAudioStreamChange: () => {},
onAudioDeviceChange: () => {},
streamList: [],
deviceList: [],
status: '',
stream: 0,
address: {},
};

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import WarningIcon from '@mui/icons-material/Warning';
import BoxText from '../../../misc/BoxText';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Error(props) {
return (
<Paper xs={12} sm={8} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={3} variant="h1" title={<Trans>Error</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={3}>
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>There was an error setting up the stream.</Trans>
</Typography>
</BoxText>
<Grid item xs={12}>
<Button variant="outlined" fullWidth color="primary" onClick={props.onNext}>
<Trans>Retry</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
Error.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onNext: () => {},
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import LicenseControl from '../../../misc/controls/License';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function License(props) {
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>License</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>
Use your copyright and choose the correct image license. Whether free for all or highly restricted. Briefly discuss what others are
allowed to do with your image.
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<LicenseControl license={props.license} onChange={props.onChange} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" color="primary" fullWidth onClick={props.onNext}>
<Trans>Save</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
License.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onBack: () => {},
onNext: () => {},
onChange: (license) => {},
license: '',
};

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import MetadataControl from '../../../misc/controls/Metadata';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Metadata(props) {
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Metadata</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Briefly describe what the audience will see during the live stream.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<MetadataControl settings={props.metadata} onChange={props.onChange} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" onClick={props.onNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
Metadata.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onBack: () => {},
onNext: () => {},
onChange: (metadata) => {},
metadata: {},
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Probe(props) {
return (
<Paper xs={12} md={5} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" onAbort={props.onAbort} />
<Grid container justifyContent="center" spacing={2} align="center">
<Grid item xs={12}>
<CircularProgress color="inherit" />
</Grid>
<Grid item xs={12}>
<Typography textAlign="center">
<Trans>Please wait. Probe stream data ...</Trans>
</Typography>
</Grid>
</Grid>
</Paper>
);
}
Probe.defaultProps = {
onAbort: () => {},
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Saving(props) {
return (
<Paper xs={12} md={5} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" onAbort={props.onAbort} />
<Grid container justifyContent="center" spacing={2} align="center">
<Grid item xs={12}>
<CircularProgress color="inherit" />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Please wait. Setting up the stream ...</Trans>
</Typography>
</Grid>
</Grid>
</Paper>
);
}
Saving.defaultProps = {
onAbort: () => {},
};

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Source(props) {
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={props.Abort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>
Select whether you pull the stream from a <strong>network source</strong> (such as a network camera) or the{' '}
<strong>internal RTMP server</strong> (e.g., OBS streams to the Restreamer).
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{props.sources}
</Grid>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Button variant="outlined" fullWidth color="default" onClick={props.onAdvanced}>
<Trans>Advanced setup</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
Source.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onAdvanced: () => {},
sources: [],
};

View File

@ -0,0 +1,87 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import BoxText from '../../../misc/BoxText';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
export default function Video(props) {
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
{props.children}
<Grid item xs={12}>
{props.status === 'error' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
{props.sourceid === 'rtmp' || props.sourceid === 'hls' ? (
<Trans>No live stream was detected. Please check the software that sends the stream.</Trans>
) : (
<Trans>Failed to verify the source. Please check the address.</Trans>
)}
</Typography>
</BoxText>
)}
{props.status === 'nostream' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any video streams. Please check the device.</Trans>
</Typography>
</BoxText>
)}
{props.status === 'nocoder' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>
The source doesn't provide any compatible video streams. Please check the{' '}
<Link color="secondary" target="_blank" href="https://github.com/datarhei/restreamer">
requirements
</Link>
.
</Trans>
</Typography>
</BoxText>
)}
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" disabled={!props.ready} onClick={props.onNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
Video.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onBack: () => {},
onNext: () => {},
sourceid: '',
status: '',
ready: false,
};

View File

@ -0,0 +1,101 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
import Select from '../../../misc/Select';
export default function VideoProfile(props) {
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={props.onAbort} onHelp={props.onHelp} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>The video source is compatible. Select the desired resolution:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Profile</Trans>} value={props.stream} onChange={props.onStreamChange}>
{props.streamList}
</Select>
</Grid>
{props.compatible === false && (
<React.Fragment>
{props.encodersList.length === 0 ? (
<Grid item xs={12}>
<Typography>
<Trans>Your stream needs to be encoded, but there's no suitable encoder available.</Trans>
</Typography>
</Grid>
) : (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>Your stream needs to be encoded. Choose the desired encoder:</Trans>
</Typography>
</Grid>
{props.decodersList.length >= 2 && (
<Grid item xs={12}>
<Select label={<Trans>Decoder</Trans>} value={props.decoder} onChange={props.onDecoderChange}>
{props.decodersList}
</Select>
</Grid>
)}
<Grid item xs={12}>
<Select label={<Trans>Encoder</Trans>} value={props.encoder} onChange={props.onEncoderChange}>
{props.encodersList}
</Select>
</Grid>
</React.Fragment>
)}
</React.Fragment>
)}
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={props.onBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button
variant="outlined"
fullWidth
color="primary"
disabled={props.compatible === false && props.encodersList.length === 0}
onClick={props.onNext}
>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
}
VideoProfile.defaultProps = {
onAbort: () => {},
onHelp: () => {},
onBack: () => {},
onNext: () => {},
compatible: false,
onStreamChange: () => {},
streamList: [],
stream: 0,
onDecoderChange: () => {},
decodersList: [],
decoder: '',
onEncoderChange: () => {},
encodersList: [],
encoder: '',
};

View File

@ -2,33 +2,32 @@ import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import { t } from '@lingui/macro';
import Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import Typography from '@mui/material/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import * as Decoders from '../../../misc/coders/Decoders';
import * as Encoders from '../../../misc/coders/Encoders';
import * as M from '../../../utils/metadata';
import BoxText from '../../../misc/BoxText';
import FullSources from '../Sources';
import H from '../../../utils/help';
import LicenseControl from '../../../misc/controls/License';
import MetadataControl from '../../../misc/controls/Metadata';
import NotifyContext from '../../../contexts/Notify';
import Paper from '../../../misc/Paper';
import PaperHeader from '../../../misc/PaperHeader';
import Sources from './Sources';
import Select from '../../../misc/Select';
import Source from './Source';
import Video from './Video';
import VideoProfile from './VideoProfile';
import Audio from './Audio';
import Abort from './Abort';
import Error from './Error';
import Saving from './Saving';
import Probe from './Probe';
import License from './License';
import Metadata from './Metadata';
export default function Wizard(props) {
const { i18n } = useLingui();
@ -281,37 +280,7 @@ export default function Wizard(props) {
}
// STEP 1 - Source Type Selection
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={handleAbort} onHelp={handleHelp('video-setup')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>
Select whether you pull the stream from a <strong>network source</strong> (such as a network camera) or the{' '}
<strong>internal RTMP server</strong> (e.g., OBS streams to the Restreamer).
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{availableSources}
</Grid>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Button variant="outlined" fullWidth color="default" onClick={handleAdvanced}>
<Trans>Advanced setup</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
return <Source onAbort={handleAbort} onHelp={handleHelp('video-setup')} onAdvanced={handleAdvanced} sources={availableSources} />;
} else if ($step === 'VIDEO SETTINGS') {
handleNext = async () => {
// probing ...
@ -372,106 +341,45 @@ export default function Wizard(props) {
// STEP 2 - Source Settings
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={handleAbort} onHelp={handleHelp('video-settings')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Component
knownDevices={$skills.sources[s.type]}
config={$config.source[s.type]}
settings={$sources.video.settings}
skills={$skills}
onChange={handleChange}
onRefresh={handleRefresh}
/>
<Grid item xs={12}>
{$probe.status === 'error' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
{$sourceid === 'rtmp' || $sourceid === 'hls' ? (
<Trans>No live stream was detected. Please check the software that sends the stream.</Trans>
) : (
<Trans>Failed to verify the source. Please check the address.</Trans>
)}
</Typography>
</BoxText>
)}
{$probe.status === 'nostream' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any video streams. Please check the device.</Trans>
</Typography>
</BoxText>
)}
{$probe.status === 'nocoder' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>
The source doesn't provide any compatible video streams. Please check the{' '}
<Link color="secondary" target="_blank" href="https://github.com/datarhei/restreamer">
requirements
</Link>
.
</Trans>
</Typography>
</BoxText>
)}
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" disabled={!$sources.video.ready} onClick={handleNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
<Video
onAbort={handleAbort}
onHelp={handleHelp('video-settings')}
onBack={handleBack}
onNext={handleNext}
status={$probe.status}
sourceid={$sourceid}
ready={$sources.video.ready}
>
<Component
knownDevices={$skills.sources[s.type]}
config={$config.source[s.type]}
settings={$sources.video.settings}
skills={$skills}
onChange={handleChange}
onRefresh={handleRefresh}
/>
<Backdrop open={$skillsRefresh}>
<CircularProgress color="inherit" />
</Backdrop>
</Paper>
</Video>
);
}
// STEP 3 - Source Probe
else if ($step === 'VIDEO PROBE') {
return (
<Paper xs={12} md={5} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" onAbort={handleAbort} />
<Grid container justifyContent="center" spacing={2} align="center">
<Grid item xs={12}>
<CircularProgress color="inherit" />
</Grid>
<Grid item xs={12}>
<Typography textAlign="center">
<Trans>Please wait. Probe stream data ...</Trans>
</Typography>
</Grid>
</Grid>
</Paper>
);
return <Probe onAbort={handleAbort} />;
} else if ($step === 'VIDEO RESULT') {
handleNext = () => {
const streams = $sources.video.streams;
const videoprofile = $profile.video;
const encoder = Encoders.Video.Get(videoprofile.encoder.coder);
let defaults = encoder.defaults();
let defaults = encoder.defaults({}, $skills);
videoprofile.encoder.settings = defaults.settings;
videoprofile.encoder.mapping = defaults.mapping;
const decoder = Decoders.Video.Get(videoprofile.decoder.coder);
defaults = decoder.defaults();
defaults = decoder.defaults({}, $skills);
videoprofile.decoder.settings = defaults.settings;
videoprofile.decoder.mapping = defaults.mapping;
@ -613,68 +521,22 @@ export default function Wizard(props) {
// STEP 4 - Video Profile Selection
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Video setup</Trans>} onAbort={handleAbort} onHelp={handleHelp('video-result')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>The video source is compatible. Select the desired resolution:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Profile</Trans>} value={$profile.video.stream} onChange={handleStreamChange}>
{streamList}
</Select>
</Grid>
{compatible === false && (
<React.Fragment>
{encodersList.length === 0 ? (
<Grid item xs={12}>
<Typography>
<Trans>Your stream needs to be encoded, but there's no suitable encoder available.</Trans>
</Typography>
</Grid>
) : (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>Your stream needs to be encoded. Choose the desired encoder:</Trans>
</Typography>
</Grid>
{decodersList.length >= 2 && (
<Grid item xs={12}>
<Select label={<Trans>Decoder</Trans>} value={$profile.video.decoder.coder} onChange={handleDecoderChange}>
{decodersList}
</Select>
</Grid>
)}
<Grid item xs={12}>
<Select label={<Trans>Encoder</Trans>} value={$profile.video.encoder.coder} onChange={handleEncoderChange}>
{encodersList}
</Select>
</Grid>
</React.Fragment>
)}
</React.Fragment>
)}
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" disabled={compatible === false && encodersList.length === 0} onClick={handleNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
<VideoProfile
onAbort={handleAbort}
onHelp={handleHelp('video-result')}
onBack={handleBack}
onNext={handleNext}
compatible={compatible}
stream={$profile.video.stream}
streamList={streamList}
onStreamChange={handleStreamChange}
decoder={$profile.video.decoder.coder}
decodersList={decodersList}
onDecoderChange={handleDecoderChange}
encoder={$profile.video.encoder.coder}
encodersList={encodersList}
onEncoderChange={handleEncoderChange}
/>
);
} else if ($step === 'AUDIO SETTINGS') {
handleNext = async () => {
@ -845,118 +707,24 @@ export default function Wizard(props) {
}
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Audio setup</Trans>} onAbort={handleAbort} onHelp={handleHelp('audio-settings')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
{$probe.status === 'error' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>Failed to verify the source. Please check the address.</Trans>
</Typography>
</BoxText>
)}
{$probe.status === 'nostream' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any audio streams.</Trans>
</Typography>
</BoxText>
)}
{$probe.status === 'nocoder' && (
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>The source doesn't provide any compatible audio streams.</Trans>
</Typography>
</BoxText>
)}
</Grid>
<Grid item xs={12}>
<RadioGroup row value={radioValue} onChange={handleStream}>
<Grid container spacing={2}>
{streamList.length === 0 && (
<Grid item xs={12}>
<Typography>
<Trans>
The video source doesn't provide any compatible audio stream. <strong>Silence audio</strong> is recommended.
Services e.g. YouTube, Facebook &amp; Co. require an audio channel.
</Trans>
</Typography>
</Grid>
)}
{streamList.length !== 0 && (
<React.Fragment>
<Grid item xs={12}>
<FormControlLabel value="video" control={<Radio />} label={i18n._(t`Audio from device`)} />
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Stream</Trans>} value={profile.stream} onChange={handleAudioStreamChange}>
{streamList}
</Select>
</Grid>
</React.Fragment>
)}
{deviceList.length !== 0 && (
<React.Fragment>
<Grid item xs={12}>
<FormControlLabel value="alsa" control={<Radio />} label={i18n._(t`Audio from device`)} />
</Grid>
<Grid item xs={12}>
<Select label={<Trans>Device</Trans>} value={source.settings.address} onChange={handleAudioDeviceChange}>
{deviceList}
</Select>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<div>
<FormControlLabel value="silence" control={<Radio />} label={i18n._(t`Silence Audio`)} />
</div>
<div>
<FormControlLabel value="none" control={<Radio />} label={i18n._(t`No audio`)} />
</div>
</Grid>
</Grid>
</RadioGroup>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" onClick={handleNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
<Audio
onAbort={handleAbort}
onHelp={handleHelp('audio-settings')}
onBack={handleBack}
onNext={handleNext}
status={$probe.status}
source={radioValue}
onSource={handleStream}
streamList={streamList}
deviceList={deviceList}
stream={profile.stream}
onAudioStreamChange={handleAudioStreamChange}
address={source.settings.address}
onAudioDeviceChange={handleAudioDeviceChange}
/>
);
} else if ($step === 'AUDIO PROBE') {
return (
<Paper xs={12} md={5} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" onAbort={handleAbort} />
<Grid container justifyContent="center" spacing={2} align="center">
<Grid item xs={12}>
<CircularProgress color="inherit" />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Please wait. Probe stream data ...</Trans>
</Typography>
</Grid>
</Grid>
</Paper>
);
return <Probe onAbort={handleAbort} />;
} else if ($step === 'AUDIO RESULT') {
handleNext = () => {
let stream = null;
@ -981,7 +749,7 @@ export default function Wizard(props) {
}
const encoder = Encoders.Audio.Get(profile.coder);
const defaults = encoder.defaults(stream);
const defaults = encoder.defaults(stream, $skills);
profile.encoder.settings = defaults.settings;
profile.encoder.mapping = defaults.mapping;
@ -1023,35 +791,14 @@ export default function Wizard(props) {
};
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>Metadata</Trans>} onAbort={handleAbort} onHelp={handleHelp('audio-result')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Briefly describe what the audience will see during the live stream.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<MetadataControl settings={$data.meta} onChange={handleMetadataChange} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" fullWidth color="primary" onClick={handleNext}>
<Trans>Next</Trans>
</Button>
</Grid>
</Grid>
</Paper>
<Metadata
onAbort={handleAbort}
onHelp={handleHelp('audio-result')}
onBack={handleBack}
onNext={handleNext}
onChange={handleMetadataChange}
metadata={$data.meta}
/>
);
} else if ($step === 'LICENSE') {
handleNext = async () => {
@ -1078,55 +825,17 @@ export default function Wizard(props) {
};
return (
<Paper xs={12} sm={9} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" title={<Trans>License</Trans>} onAbort={handleAbort} onHelp={handleHelp('license')} />
<Grid container spacing={2}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>
Use your copyright and choose the correct image license. Whether free for all or highly restricted. Briefly discuss what others
are allowed to do with your image.
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<LicenseControl license={$data.license} onChange={handleLicenseChange} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={3}>
<Button variant="outlined" color="default" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
<Grid item xs={9}>
<Button variant="outlined" color="primary" fullWidth onClick={handleNext}>
<Trans>Save</Trans>
</Button>
</Grid>
</Grid>
</Paper>
<License
onAbort={handleAbort}
onHelp={handleHelp('license')}
onBack={handleBack}
onNext={handleNext}
onChange={handleLicenseChange}
license={$data.license}
/>
);
} else if ($step === 'SAVING') {
return (
<Paper xs={12} md={5} marginBottom="6em" className="PaperM">
<PaperHeader spacing={2} variant="h1" onAbort={handleAbort} />
<Grid container justifyContent="center" spacing={2} align="center">
<Grid item xs={12}>
<CircularProgress color="inherit" />
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Please wait. Setting up the stream ...</Trans>
</Typography>
</Grid>
</Grid>
</Paper>
);
return <Saving onAbort={handleAbort} />;
} else if ($step === 'DONE') {
return null;
} else if ($step === 'ERROR') {
@ -1134,24 +843,7 @@ export default function Wizard(props) {
setStep('TYPE');
};
return (
<Paper xs={12} sm={8} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={3} variant="h1" title={<Trans>Error</Trans>} onAbort={handleAbort} onHelp={handleHelp('error')} />
<Grid container spacing={3}>
<BoxText color="dark">
<WarningIcon fontSize="large" color="error" />
<Typography textAlign="center">
<Trans>There was an error setting up the stream.</Trans>
</Typography>
</BoxText>
<Grid item xs={12}>
<Button variant="outlined" fullWidth color="primary" onClick={handleNext}>
<Trans>Retry</Trans>
</Button>
</Grid>
</Grid>
</Paper>
);
return <Error onAbort={handleAbort} onHelp={handleHelp('error')} />;
} else if ($step === 'ABORT') {
const nchannels = props.restreamer.ListChannels().length;
@ -1169,46 +861,12 @@ export default function Wizard(props) {
navigate(`/`);
};
return (
<Paper xs={12} sm={8} md={6} marginBottom="6em" className="PaperM">
<PaperHeader spacing={3} variant="h1" title={<Trans>Abort</Trans>} onHelp={handleHelp('abort')} />
<Grid container spacing={3}>
{nchannels <= 1 ? (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>You can't abort the wizard because at least one input must be defined.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Button variant="outlined" color="primary" fullWidth onClick={handleBack}>
<Trans>Back</Trans>
</Button>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<Typography>
<Trans>Are you sure you want to abort the wizard?</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<Button variant="outlined" color="default" fullWidth onClick={handleNext}>
<Trans>Yes</Trans>
</Button>
</Grid>
<Grid item xs={6}>
<Button variant="outlined" color="primary" fullWidth onClick={handleBack}>
<Trans>No</Trans>
</Button>
</Grid>
</React.Fragment>
)}
</Grid>
</Paper>
);
return <Abort onHelp={handleHelp('abort')} onBack={handleBack} onNext={handleNext} nchannels={nchannels} />;
}
return null;
}
Wizard.defaultProps = {
restreamer: null,
};

View File

@ -0,0 +1,906 @@
import React from 'react';
import { render, fireEvent, act, screen } from '../../../utils/testing';
import '@testing-library/jest-dom';
import Wizard from './index';
const restreamer = {
SelectChannel: () => {
return 'test';
},
GetChannel: () => {
return {
id: 'test',
name: 'test',
};
},
Skills: () => {
return {
ffmpeg: {
version: '5.1.2',
},
formats: {
demuxers: ['rtsp'],
},
protocols: {
input: ['http', 'https', 'rtmp', 'rtmps', 'srt'],
},
sources: {
network: {},
},
encoders: {
audio: ['copy', 'none', 'aac'],
video: ['copy', 'none', 'libx264'],
},
decoders: {
audio: ['default'],
video: ['default'],
},
};
},
RefreshSkills: () => {},
ConfigActive: () => {
return {
source: {
network: {
rtmp: {
enabled: true,
app: '/live',
token: 'foobar',
},
srt: {
enabled: true,
token: 'foobar',
passphrase: 'bazfoobazfoo',
},
},
},
};
},
Probe: (id, inputs) => {
let streams = [];
if (inputs[0].address === 'rtmp://localhost/live/external.stream?token=foobar') {
streams.push({
url: inputs[0].address,
format: 'rtmp',
index: 0,
stream: 0,
language: 'und',
type: 'video',
codec: 'h264',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 0,
layout: '',
channels: 0,
});
streams.push({
url: inputs[0].address,
format: 'rtmp',
index: 0,
stream: 1,
language: 'und',
type: 'audio',
codec: 'aac',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
} else if (inputs[0].address === 'srt://localhost?mode=caller&transtype=live&streamid=external,mode:request,token:foobar&passphrase=bazfoobazfoo') {
streams.push({
url: inputs[0].address,
format: 'srt',
index: 0,
stream: 0,
language: 'und',
type: 'video',
codec: 'h264',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 0,
layout: '',
channels: 0,
});
streams.push({
url: inputs[0].address,
format: 'srt',
index: 0,
stream: 1,
language: 'und',
type: 'audio',
codec: 'aac',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
} else if (inputs[0].address === 'anullsrc=r=44100:cl=stereo') {
streams.push({
url: inputs[0].address,
format: 'lavfi',
index: 0,
stream: 0,
language: 'und',
type: 'audio',
codec: 'pcm',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
} else {
const name = inputs[0].address.split('/').pop();
const [video, audio] = name.split('-');
switch (video) {
case 'none':
break;
case 'h264':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 0,
language: 'und',
type: 'video',
codec: 'h264',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 0,
layout: '',
channels: 0,
});
break;
case 'other':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 0,
language: 'und',
type: 'video',
codec: 'mjpeg',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 0,
layout: '',
channels: 0,
});
break;
case 'unknown':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 0,
language: 'und',
type: 'video',
codec: 'xxx',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 0,
layout: '',
channels: 0,
});
break;
default:
break;
}
switch (audio) {
case 'none':
break;
case 'aac':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 1,
language: 'und',
type: 'audio',
codec: 'aac',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
break;
case 'ogg':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 1,
language: 'und',
type: 'audio',
codec: 'ogg',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
break;
case 'unknown':
streams.push({
url: 'rtsp://127.0.0.1/live/stream',
format: 'rtsp',
index: 0,
stream: 1,
language: 'und',
type: 'audio',
codec: 'xxx',
coder: '',
bitrate_kbps: 0,
duration_sec: 0,
fps: 0,
pix_fmt: 'yuvj420p',
width: 1280,
height: 720,
sampling_hz: 44100,
layout: 'stereo',
channels: 2,
});
break;
default:
break;
}
}
return [{ streams: streams }, null];
},
UpsertIngest: (_channelid, global, inputs, outputs, control) => {
return [{}, null];
},
SetIngestMetadata: (_channelid, data) => {},
UpsertIngestSnapshot: (_channelid, control) => {},
UpdatePlayer: (_channelid) => {},
UpdatePlayersite: () => {},
};
test('wizard', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
expect(await screen.findByText(/Select whether you pull the stream from a/)).toBeInTheDocument();
expect(screen.queryByText('Network source')).toBeInTheDocument();
expect(screen.queryByText('RTMP server')).toBeInTheDocument();
expect(screen.queryByText('SRT server')).toBeInTheDocument();
});
test('wizard: rtmp source video h264-aac', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'RTMP server' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source is compatible. Select the desired resolution:/)).toBeInTheDocument();
expect(screen.queryByText(/Your stream needs to be encoded. Choose the desired encoder:/)).not.toBeInTheDocument();
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
// Confirm metadata
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/License/)).toBeInTheDocument();
// Confirm license
button = screen.getByRole('button', { name: 'Save' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
});
test('wizard: srt source video h264-aac', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'SRT server' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source is compatible. Select the desired resolution:/)).toBeInTheDocument();
expect(screen.queryByText(/Your stream needs to be encoded. Choose the desired encoder:/)).not.toBeInTheDocument();
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
// Confirm metadata
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/License/)).toBeInTheDocument();
// Confirm license
button = screen.getByRole('button', { name: 'Save' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
});
test('wizard: network source error', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/none-none' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Failed to verify the source. Please check the address./)).toBeInTheDocument();
// Add a stream address
input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/none-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The source doesn't provide any video streams. Please check the device./)).toBeInTheDocument();
/*
// TODO: This message appears only if there's no h264 encoder. However it should appear if there's no
// suitable decoder for the detected codec. The question is, if this is actually possible, because
// if FFmpeg doesn't know the codec, how can it detect it?
input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/unknown-none' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The source doesn't provide any compatible video streams./)).toBeInTheDocument();
*/
});
test('wizard: network source video h264', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-none' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source is compatible. Select the desired resolution:/)).toBeInTheDocument();
expect(screen.queryByText(/Your stream needs to be encoded. Choose the desired encoder:/)).not.toBeInTheDocument();
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source doesn't provide any compatible audio stream./)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
});
test('wizard: network source video non-h264', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/other-none' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source is compatible. Select the desired resolution:/)).toBeInTheDocument();
expect(screen.queryByText(/Your stream needs to be encoded. Choose the desired encoder:/)).toBeInTheDocument();
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/The video source doesn't provide any compatible audio stream./)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
});
test('wizard: network source audio aac', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
// Select suggested audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
});
test('wizard: network source audio non-aac', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-ogg' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
// Select suggested audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
});
test('wizard: network source silence audio', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
expect(screen.queryByLabelText('Audio from device')).toBeChecked();
expect(screen.queryByLabelText('Silence Audio')).not.toBeChecked();
expect(screen.queryByLabelText('No audio')).not.toBeChecked();
input = screen.queryByLabelText('Silence Audio');
await act(async () => {
fireEvent.click(input);
});
expect(screen.queryByLabelText('Audio from device')).not.toBeChecked();
expect(screen.queryByLabelText('Silence Audio')).toBeChecked();
expect(screen.queryByLabelText('No audio')).not.toBeChecked();
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
});
test('wizard: network source no audio', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Audio from device/)).toBeInTheDocument();
expect(screen.queryByText(/Silence Audio/)).toBeInTheDocument();
expect(screen.queryByText(/No audio/)).toBeInTheDocument();
expect(screen.queryByLabelText('Audio from device')).toBeChecked();
expect(screen.queryByLabelText('Silence Audio')).not.toBeChecked();
expect(screen.queryByLabelText('No audio')).not.toBeChecked();
input = screen.queryByLabelText('No audio');
await act(async () => {
fireEvent.click(input);
});
expect(screen.queryByLabelText('Audio from device')).not.toBeChecked();
expect(screen.queryByLabelText('Silence Audio')).not.toBeChecked();
expect(screen.queryByLabelText('No audio')).toBeChecked();
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
});
test('wizard: metadata', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/Metadata/)).toBeInTheDocument();
expect(screen.queryByLabelText('Name')).toHaveValue('test');
expect(screen.queryByLabelText('Description')).toHaveValue('Live from earth. Powered by datarhei Restreamer.');
// Confirm metadata
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/License/)).toBeInTheDocument();
});
test('wizard: license', async () => {
await act(async () => {
render(<Wizard restreamer={restreamer} />, {}, '/wizard/test', '/wizard/:channelid');
});
// Choose network source
let button = screen.getByRole('button', { name: 'Network source' });
fireEvent.click(button);
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeDisabled();
// Add a stream address
let input = screen.getByLabelText('Address');
fireEvent.change(input, { target: { value: 'rtsp://127.0.0.1/live/h264-aac' } });
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Select suggested video stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Confirm selected audio stream
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
// Confirm metadata
button = screen.getByRole('button', { name: 'Next' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
expect(screen.queryByText(/License/)).toBeInTheDocument();
// Confirm license
button = screen.getByRole('button', { name: 'Save' });
expect(button).toBeEnabled();
await act(async () => {
fireEvent.click(button);
});
});

View File

@ -139,8 +139,8 @@ export default function Add(props) {
const serviceSkills = helper.conflateServiceSkills(s.requires, $skills);
const profiles = $settings.profiles;
profiles[0].video = helper.preselectProfile(profiles[0].video, 'video', $sources[0].streams, serviceSkills.codecs.video, $skills.encoders.video);
profiles[0].audio = helper.preselectProfile(profiles[0].audio, 'audio', $sources[0].streams, serviceSkills.codecs.audio, $skills.encoders.audio);
profiles[0].video = helper.preselectProfile(profiles[0].video, 'video', $sources[0].streams, serviceSkills.codecs.video, $skills);
profiles[0].audio = helper.preselectProfile(profiles[0].audio, 'audio', $sources[0].streams, serviceSkills.codecs.audio, $skills);
setSettings({
...$settings,

View File

@ -162,8 +162,8 @@ export default function Edit(props) {
const settings = await props.restreamer.GetEgressMetadata(_channelid, id);
const profiles = settings.profiles;
profiles[0].video = helper.preselectProfile(profiles[0].video, 'video', ingest.streams, serviceSkills.codecs.video, skills.encoders.video);
profiles[0].audio = helper.preselectProfile(profiles[0].audio, 'audio', ingest.streams, serviceSkills.codecs.audio, skills.encoders.audio);
profiles[0].video = helper.preselectProfile(profiles[0].video, 'video', ingest.streams, serviceSkills.codecs.video, skills);
profiles[0].audio = helper.preselectProfile(profiles[0].audio, 'audio', ingest.streams, serviceSkills.codecs.audio, skills);
settings.profiles = profiles;
settings.streams = M.createOutputStreams(sources, profiles);

View File

@ -237,10 +237,12 @@ export function conflateServiceSkills(requires, skills) {
* @param {*} type Either 'audio' or 'video'
* @param {*} streams List of available streams
* @param {*} codecs List of target codecs
* @param {*} encoders list of available (to ffmpeg) encoders
* @param {*} skills FFmpeg skills
* @returns {boolean} Whether the provided profile is valid
*/
export function preselectProfile(profile, type, streams, codecs, encoders) {
export function preselectProfile(profile, type, streams, codecs, skills) {
const encoders = skills.encoders[type];
/**
* Checks if the given profile makes sense, i.e. matches to the available
* streams and codecs.
@ -325,7 +327,7 @@ export function preselectProfile(profile, type, streams, codecs, encoders) {
if (coder === null) {
profile.encoder.coder = 'none';
} else {
const defaults = coder.defaults();
const defaults = coder.defaults(streams[i], skills);
profile.encoder.coder = coder.coder;
profile.encoder.settings = defaults.settings;
profile.encoder.mapping = defaults.mapping;
@ -338,7 +340,7 @@ export function preselectProfile(profile, type, streams, codecs, encoders) {
let coder = type === 'audio' ? Coders.Audio.Get('copy') : Coders.Video.Get('copy');
const defaults = coder.defaults();
const defaults = coder.defaults(streams[i], skills);
profile.encoder.settings = defaults.settings;
profile.encoder.mapping = defaults.mapping;