Merge branch 'dev'

This commit is contained in:
Ingo Oppermann 2022-07-21 22:18:02 +02:00
commit 2ac8bb5e87
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
142 changed files with 17812 additions and 4693 deletions

View File

@ -51,7 +51,8 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
PUBLIC_URL=/ui
PUBLIC_URL=./
NODE_IMAGE=${{ env.NODE_IMAGE }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |

View File

@ -1,2 +1,3 @@
# RESTREAMER UI
RELEASE=1.1.0
RELEASE=1.2.0
NODE_IMAGE=node:18.6.0-alpine3.15

View File

@ -17,6 +17,7 @@
"de",
"fr",
"it",
"pl",
"pt",
"es",
"ru"

View File

@ -1,5 +1,36 @@
# Restreamer-UI
#### v1.1.0 > v1.2.0
- Add allow writing HLS to disk
- Add audio pan filter
- Add video rotation filter ([#347](https://github.com/datarhei/restreamer/discussions/347))
- Add video h/v flip filter
- Add audio volume filter ([#313](https://github.com/datarhei/restreamer/issues/313))
- Add audio loudness normalization filter
- Add audio resample filter, that was before part of the encoders
- Add HLS Master playlist (requires FFmpeg hlsbitrate.patch) (thx Dwaynarang, Electra Player compatibility)
- Add LinkedIn & Azure Media Services to publication services (thx kalashnikov)
- Add AirPlay support with silvermine videojs plugin
- Add Chromecast support (thx badincite, [#10](https://github.com/datarhei/restreamer-ui/pull/10))
- Add stream distribution across multiple internal servers
- Add SRT settings
- Add HLS version selection (thx Dwaynarang, Electra Player compatibility)
- Add Owncast to publication services ([#369](https://github.com/datarhei/restreamer/issues/369))
- Add Telegram to publication services (thx Martin Held)
- Add Polish translations (thx Robert Rykała)
- Mod extends the datarhei Core publication service with srt streaming
- Mod allow decoders and encoders to set global options
- Mod allow trailing slash on Core address
- Fix player problem with different stream formats (9:16)
- Fix process report naming
- Fix publication service icon styles
- Fix VAAPI encoder
Dependency:
- datarhei Core v16.9.0+
#### v1.0.0 > v1.1.0
- Add compatibility list for encoders
@ -9,8 +40,9 @@
- Add missed VAAPI encoder
- Add missed V4L2_M2M encoder
- Add missed Raspberry Pi 64bit Docker image
- Mod updates VideoJS
- Add option to disable playersites share-button (thx Anders Mellgren)
- Add security pr
- Mod updates VideoJS
- Fix hides unset content license on playersite (thx Anders Mellgren)
- Fix updates V4L2 device-list on select
- Fix snapshot interval ([#341](https://github.com/datarhei/restreamer/issues/340))
@ -21,7 +53,6 @@
- Fix datarhei Core publication service
- Fix dependabot alerts
- Fix code scanning alerts
- Merge security pr
Preparation for FFmpeg v5.0 (migration will not work)

View File

@ -1,9 +1,11 @@
FROM node:17.9.0-alpine3.15
ARG NODE_IMAGE=node:18.6.0-alpine3.15
FROM $NODE_IMAGE
ARG NODE_SPACE_SIZE=10240
ENV NODE_OPTIONS="--openssl-legacy-provider --max-old-space-size=$NODE_SPACE_SIZE"
ARG PUBLIC_URL "/"
ENV PUBLIC_URL "./"
COPY . /ui

View File

@ -1,6 +1,6 @@
# Restreamer-UI
The user interface of the Restreamer for the connection to the Core application.
The user interface of the Restreamer for the connection to the [datarhei Core](https://github.com/datarhei/core)application.
- React
- Material-UI (MUI)
@ -17,7 +17,7 @@ $ npm run start
```
Connect the UI with a [datarhei Core](https://github.com/datarhei/core):
http://localhost:3000?address=http://core-ip:core-port/
http://localhost:3000?address=http://core-ip:core-port
### To add/fix translations:
Locales are located in `src/locals`
@ -26,5 +26,23 @@ $ npm run i18n-extract:clean
$ npm run i18n-compile
```
### Known outdated dependencies
Requires MUI 5.2+ & React 18 compatibility. Clappr-Player upgrade (or removal).
```sh
@mui/material 5.1.1 → 5.9.0
@mui/styles ^5.1.1 → ^5.9.0
@testing-library/dom ^8.13.0 → ^8.16.0
@testing-library/jest-dom ^4.2.4 → ^5.16.4
@testing-library/react ^12.1.5 → ^13.3.0
@testing-library/user-event ^13.5.0 → ^14.2.5
eslint ^7.32.0 → ^8.19.0
hls.js ^0.14.17 → ^1.1.5
react ^17.0.2 → ^18.2.0
react-dom ^17.0.2 → ^18.2.0
react-scripts ^4.0.3 → ^5.0.1
typescript ^3.9.7 → ^4.7.4
```
## License
See the [LICENSE](./LICENSE) file for licensing information.

View File

@ -1,28 +1,28 @@
{
"name": "restreamer-ui",
"version": "1.1.0",
"bundle": "restreamer-v2.1.0",
"version": "1.2.0",
"bundle": "restreamer-v2.2.0",
"private": false,
"license": "Apache-2.0",
"dependencies": {
"@auth0/auth0-spa-js": "^1.22.0",
"@auth0/auth0-spa-js": "^1.22.1",
"@clappr/core": "^0.4.21",
"@clappr/hlsjs-playback": "^0.6.0",
"@clappr/plugins": "^0.4.16",
"@clappr/stats-plugin": "^0.2.0",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@fontsource/dosis": "^4.5.8",
"@fontsource/roboto": "^4.5.7",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"@lingui/core": "^3.13.3",
"@lingui/macro": "^3.13.3",
"@lingui/react": "^3.13.3",
"@mui/icons-material": "^5.8.2",
"@mui/lab": "^5.0.0-alpha.84",
"@fortawesome/react-fontawesome": "^0.2.0",
"@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.90",
"@mui/material": "5.1.1",
"@mui/styles": "^5.1.1",
"@testing-library/dom": "^8.13.0",
@ -82,10 +82,10 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@lingui/cli": "^3.13.3",
"@babel/core": "^7.18.6",
"@lingui/cli": "^3.14.0",
"babel-core": "^7.0.0-bridge.0",
"prettier": "^2.6.2",
"prettier": "^2.7.1",
"react-error-overlay": "^6.0.11"
},
"resolutions": {

View File

@ -0,0 +1,15 @@
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0V0z" id="a"/>
</defs>
<defs>
<path d="M0 0h24v24H0V0z" id="c"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<clipPath clip-path="url(#b)" id="d">
<use overflow="visible" xlink:href="#c"/>
</clipPath>
<path clip-path="url(#d)" d="M6 22h12l-6-6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View File

@ -0,0 +1 @@
.vjs-airplay-button .vjs-icon-placeholder{background:url("ic_airplay_white_24px.svg") center center no-repeat;background-size:contain;display:inline-block;width:20px;height:20px}.vjs-airplay-button:hover{cursor:pointer}.vjs-airplay-button:hover .vjs-icon-placeholder{background-image:url("ic_airplay_white_24px.svg")}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
/** Silvermine Chromecast **/
.vjs-chromecast-button .vjs-icon-placeholder {
background: url('ic_cast_white_24dp.png') center center no-repeat;
background-size: contain;
display: inline-block;
width: 20px;
height: 20px;
}
.vjs-chromecast-button:hover {
cursor: pointer;
}
.vjs-chromecast-button:hover .vjs-icon-placeholder {
background-image: url('ic_cast_white_24dp.png');
}
.vjs-chromecast-button.vjs-chromecast-casting-state .vjs-icon-placeholder {
background-image: url('ic_cast_connected_white_24dp.png');
}
.vjs-chromecast-button.vjs-chromecast-casting-state:hover .vjs-icon-placeholder {
background-image: url('ic_cast_connected_white_24dp.png');
}
.vjs-tech-chromecast {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
.vjs-tech-chromecast .vjs-tech-chromecast-poster::after {
content: ' ';
display: block;
height: 2px;
width: 100px;
background-color: #cccccc;
position: absolute;
left: calc(50% - 50px);
}
.vjs-tech-chromecast .vjs-tech-chromecast-poster-img {
max-height: 180px;
width: auto;
border: 2px solid #cccccc;
}
.vjs-tech-chromecast .vjs-tech-chromecast-poster-img.vjs-tech-chromecast-poster-img-empty {
width: 160px;
height: 90px;
}
.vjs-tech-chromecast .vjs-tech-chromecast-title-container {
position: absolute;
bottom: 50%;
margin-bottom: 100px;
color: #cccccc;
text-align: center;
}
.vjs-tech-chromecast .vjs-tech-chromecast-title {
font-size: 22px;
}
.vjs-tech-chromecast .vjs-tech-chromecast-title.vjs-tech-chromecast-title-empty {
display: none;
}
.vjs-tech-chromecast .vjs-tech-chromecast-subtitle {
font-size: 18px;
padding-top: 0.5em;
}
.vjs-tech-chromecast .vjs-tech-chromecast-subtitle.vjs-tech-chromecast-subtitle-empty {
display: none;
}

View File

@ -0,0 +1 @@
.vjs-chromecast-button .vjs-icon-placeholder{background:url(ic_cast_white_24dp.png) center center no-repeat;background-size:contain;display:inline-block;width:20px;height:20px}.vjs-chromecast-button:hover{cursor:pointer}.vjs-chromecast-button:hover .vjs-icon-placeholder{background-image:url(ic_cast_white_24dp.png)}.vjs-chromecast-button.vjs-chromecast-casting-state .vjs-icon-placeholder{background-image:url(ic_cast_connected_white_24dp.png)}.vjs-chromecast-button.vjs-chromecast-casting-state:hover .vjs-icon-placeholder{background-image:url(ic_cast_connected_white_24dp.png)}.vjs-tech-chromecast{display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden}.vjs-tech-chromecast .vjs-tech-chromecast-poster::after{content:' ';display:block;height:2px;width:100px;background-color:#ccc;position:absolute;left:calc(50% - 50px)}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img{max-height:180px;width:auto;border:2px solid #ccc}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img.vjs-tech-chromecast-poster-img-empty{width:160px;height:90px}.vjs-tech-chromecast .vjs-tech-chromecast-title-container{position:absolute;bottom:50%;margin-bottom:100px;color:#ccc;text-align:center}.vjs-tech-chromecast .vjs-tech-chromecast-title{font-size:22px}.vjs-tech-chromecast .vjs-tech-chromecast-title.vjs-tech-chromecast-title-empty{display:none}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle{font-size:18px;padding-top:.5em}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle.vjs-tech-chromecast-subtitle-empty{display:none}

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,10 @@ dist/videojs-overlay.min.css
dist/video-js-skin.min.css
dist/videojs-license.min.js
dist/videojs-license.min.css
dist/videojs-chromecast.min.js
dist/videojs-chromecast.min.css
dist/ic_cast_connected_white_24dp.png
dist/ic_cast_white_24dp.png
dist/videojs-airplay.min.js
dist/videojs-airplay.min.css
dist/ic_airplay_white_24px.svg

View File

@ -15,11 +15,12 @@
<link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet">
<style>
.player-poster[data-poster] .poster-background[data-poster] {
height: initial !important;
}
</style>
{{#if airplay}}
<link href="player/videojs/dist/videojs-airplay.min.css" rel="stylesheet">
{{/if}}
{{#if chromecast}}
<link href="player/videojs/dist/videojs-chromecast.min.css" rel="stylesheet">
{{/if}}
</head>
<body>
<div style="position:absolute;top:0;right:0;bottom:0;left:0">
@ -28,6 +29,13 @@
<script src="player/videojs/dist/video.min.js"></script>
<script src="player/videojs/dist/videojs-overlay.min.js"></script>
<script src="player/videojs/dist/videojs-license.min.js"></script>
{{#if airplay}}
<script src='player/videojs/dist/videojs-airplay.min.js'></script>
{{/if}}
{{#if chromecast}}
<script src='player/videojs/dist/videojs-chromecast.min.js'></script>
<script src='https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'></script>
{{/if}}
<script>
function getQueryParam(key, defaultValue) {
var query = window.location.search.substring(1);
@ -80,6 +88,15 @@
}
};
if (playerConfig.chromecast) {
config.techOrder = ["chromecast", "html5"];
config.plugins.chromecast = {};
}
if (playerConfig.airplay) {
config.plugins.airPlay = {};
}
var player = videojs('player', config);
player.ready(function() {
if(playerConfig.logo.image.length != 0) {

View File

@ -44,6 +44,12 @@
<link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet">
{{#if airplay}}
<link href="player/videojs/dist/videojs-airplay.min.css" rel="stylesheet">
{{/if}}
{{#if chromecast}}
<link href="player/videojs/dist/videojs-chromecast.min.css" rel="stylesheet">
{{/if}}
{{/ifEquals}}
<style>
/** flexboxgrid 6.3.1 **/
@ -373,7 +379,7 @@
<div class="col-xs-12 player-l2">
<div class="player-l3">
{{#ifEquals player "videojs"}}
<video id="player" class="vjs-public video-js player-l4" playsinline></video>
<video id="player" class="vjs-public video-js vjs-16-9 player-l4" playsinline></video>
{{else}}
<div id="player" class="player-l4"></div>
{{/ifEquals}}
@ -782,6 +788,13 @@
<script src="player/videojs/dist/video.min.js"></script>
<script src="player/videojs/dist/videojs-overlay.min.js"></script>
<script src="player/videojs/dist/videojs-license.min.js"></script>
{{#if airplay}}
<script src='player/videojs/dist/videojs-airplay.min.js'></script>
{{/if}}
{{#if chromecast}}
<script src='player/videojs/dist/videojs-chromecast.min.js'></script>
<script src='https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'></script>
{{/if}}
{{else}}
<script src="player/clappr/clappr.min.js"></script>
<script src="player/clappr/clappr-stats.min.js"></script>
@ -824,6 +837,8 @@
var mute = convertBoolParam("mute", playerConfig.mute);
var statistics = convertBoolParam("stats", playerConfig.statistics);
var color = convertColorParam("color", playerConfig.color.buttons);
var chromecast = {{#if chromecast}}true{{else}}false{{/if}};
var airplay = {{#if airplay}}true{{else}}false{{/if}};
</script>
<script src="playersite/player.js"></script>

View File

@ -2,17 +2,28 @@ var config = {
controls: true,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
autoplay: autoplay ? 'muted' : false,
muted: mute,
muted: true,
liveui: true,
responsive: true,
fluid: true,
sources: [{ src: playerConfig.source, type: 'application/x-mpegURL' }],
// Needed to append the url orgin in order for the source to properly pass to the cast device
sources: [{ src: window.location.origin + '/' + playerConfig.source, type: 'application/x-mpegURL' }],
plugins: {
license: playerConfig.license,
},
};
if (chromecast) {
config.techOrder = ['chromecast', 'html5'];
config.plugins.chromecast = {};
}
if (airplay) {
config.plugins.airPlay = {};
}
var player = videojs('player', config);
player.ready(function () {
if (playerConfig.logo.image.length != 0) {
var overlay = null;

View File

@ -6,27 +6,30 @@ import * as plurals from 'make-plural/plurals';
import { messages as EN } from './locales/en/messages.js';
import { messages as DE } from './locales/de/messages.js';
import { messages as ES } from './locales/es/messages.js';
import { messages as FR } from './locales/fr/messages.js';
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 ES } from './locales/es/messages.js';
import { messages as RU } from './locales/ru/messages.js';
import * as Storage from './utils/storage';
i18n.loadLocaleData('en', { plurals: plurals.en });
i18n.loadLocaleData('de', { plurals: plurals.de });
i18n.loadLocaleData('es', { plurals: plurals.es });
i18n.loadLocaleData('fr', { plurals: plurals.fr });
i18n.loadLocaleData('it', { plurals: plurals.it });
i18n.loadLocaleData('pl', { plurals: plurals.pl });
i18n.loadLocaleData('pt', { plurals: plurals.pt });
i18n.loadLocaleData('es', { plurals: plurals.es });
i18n.loadLocaleData('ru', { plurals: plurals.ru });
i18n.load({
en: EN,
de: DE,
es: ES,
fr: FR,
it: IT,
pl: PL,
pt: PT,
es: ES,
ru: RU,
});
@ -56,7 +59,7 @@ const getBrowserLanguage = (defaultLanguage) => {
return match[0].toLowerCase();
};
i18n.activate(getLanguage('en', ['en', 'de', 'fr', 'it', 'pt', 'es', 'ru']));
i18n.activate(getLanguage('en', ['en', 'de', 'es', 'fr', 'it', 'pl', 'pt', 'ru']));
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

2936
src/locales/pl/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

View File

@ -49,6 +49,7 @@ export default function Component(props) {
direction="column"
justifyContent="center"
alignItems="center"
textAlign={props.textAlign}
spacing={1}
className={
props.color === 'dark' ? classes.dark : props.color === 'success' ? classes.success : props.color === 'danger' ? classes.danger : classes.light
@ -62,4 +63,5 @@ export default function Component(props) {
Component.defaultProps = {
color: 'light',
textAlign: 'left',
};

View File

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

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

@ -0,0 +1,134 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
// Import all filters (audio/video)
import * as Filters from './filters';
// Import all encoders (audio/video)
import * as Encoders from './coders/Encoders';
export default function FilterSelect(props) {
const profile = props.profile;
// handleFilterChange
// what: Filter name
// settings (component settings): {Key: Value}
// mapping (FFmpeg -af/-vf args): ['String', ...]
const handleFilterSettingsChange = (what) => (settings, graph, automatic) => {
const filter = profile.filter;
// Store mapping/settings per component
filter.settings[what] = {
settings: settings,
graph: graph,
};
// Get the order of the filters
let filterOrder = [];
if (props.type === 'video') {
filterOrder = Filters.Video.Filters();
} else {
filterOrder = Filters.Audio.Filters();
}
// Create the filter graph in the order as the filters are registered
const graphs = [];
for (let f of filterOrder) {
if (!(f in filter.settings)) {
continue;
}
if (filter.settings[f].graph.length !== 0) {
graphs.push(filter.settings[f].graph);
}
}
filter.graph = graphs.join(',');
props.onChange(filter, automatic);
};
// Set filterRegistry by type
let filterRegistry = null;
if (props.type === 'video') {
filterRegistry = Filters.Video;
} else if (props.type === 'audio') {
filterRegistry = Filters.Audio;
} else {
return null;
}
// Checks the state of hwaccel (gpu encoding)
let encoderRegistry = null;
let hwaccel = false;
if (props.type === 'video') {
encoderRegistry = Encoders.Video;
for (let encoder of encoderRegistry.List()) {
if (encoder.codec === props.profile.encoder.coder && encoder.hwaccel) {
hwaccel = true;
}
}
}
// Creates filter components
let filterSettings = [];
if (!hwaccel) {
for (let c of filterRegistry.List()) {
// Checks FFmpeg skills (filter is available)
if (props.availableFilters.includes(c.filter)) {
const Settings = c.component;
if (!(c.filter in profile.filter.settings)) {
profile.filter.settings[c.filter] = c.defaults();
} else {
profile.filter.settings[c.filter] = {
...c.defaults(),
...profile.filter.settings[c.filter],
};
}
filterSettings.push(
<Settings key={c.filter} settings={profile.filter.settings[c.filter].settings} onChange={handleFilterSettingsChange(c.filter)} />
);
}
}
}
// No suitable filter found
if (filterSettings === null && !hwaccel) {
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>No suitable filter found.</Trans>
</Typography>
</Grid>
</Grid>
);
// hwaccel requires further settings
} else if (hwaccel) {
return false;
}
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>Select your filter settings (optional):</Trans>
</Typography>
</Grid>
{filterSettings}
</Grid>
);
}
FilterSelect.defaultProps = {
type: '',
profile: {},
availableFilters: [],
onChange: function (filter, automatic) {},
};

View File

@ -44,10 +44,11 @@ export default function LanguageSelect(props) {
<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="pl">Polski</MenuItem>
<MenuItem value="pt">Português </MenuItem>
<MenuItem value="es">Español </MenuItem>
<MenuItem value="ru">Русский </MenuItem>
</Select>
);

View File

@ -53,6 +53,8 @@ export default function Password(props) {
endAdornment={adornment}
label={props.label}
autoComplete={props.autoComplete}
inputProps={props.inputProps}
error={props.error}
/>
{props.helperText && <FormHelperText>{props.helperText}</FormHelperText>}
</FormControl>
@ -67,6 +69,8 @@ Password.defaultProps = {
autoComplete: 'current-password',
env: false,
show: false,
helperText: null,
helperText: false,
inputProps: {},
error: false,
onChange: function (value) {},
};

View File

@ -31,6 +31,7 @@ export default function VideoJS(props) {
player.addClass('vjs-internal');
}
player.addClass('video-js');
player.addClass('vjs-16-9');
} else {
// you can update player here [update player through props]
// const player = playerRef.current;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,8 @@ import * as H264NVENC from './video/H264NVENC';
import * as H264OMX from './video/H264OMX';
import * as H264V4L2M2M from './video/H264V4L2M2M';
import * as H264VAAPI from './video/H264VAAPI';
import * as HEVCVAAPI from './video/HEVCVAAPI';
import * as VP9VAAPI from './video/VP9VAAPI';
import * as VideoCopy from './video/Copy';
import * as VideoNone from './video/None';
import * as VideoRaw from './video/Raw';
@ -127,6 +129,8 @@ videoRegistry.Register(H264NVENC);
videoRegistry.Register(H264OMX);
videoRegistry.Register(H264V4L2M2M);
videoRegistry.Register(H264VAAPI);
videoRegistry.Register(HEVCVAAPI);
videoRegistry.Register(VP9VAAPI);
videoRegistry.Register(VP9);
videoRegistry.Register(VideoRaw);

View File

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

View File

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

View File

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

View File

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

View File

@ -24,9 +24,13 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
'-vaapi_device',
'/dev/dri/renderD128',
const global = [];
const local = [];
// https://trac.ffmpeg.org/wiki/Hardware/VAAPI
global.push(['-vaapi_device', '/dev/dri/renderD128']);
local.push(
'-vf',
'format=nv12,hwupload',
'-codec:v',
@ -35,8 +39,6 @@ function createMapping(settings) {
`${settings.profile}`,
'-quality',
`${settings.quality}`,
'-level',
`${settings.level}`,
'-b:v',
`${settings.bitrate}k`,
'-maxrate',
@ -46,16 +48,17 @@ function createMapping(settings) {
'-r',
`${settings.fps}`,
'-g',
`${settings.gop}`,
'-vsync',
'1',
];
`${settings.gop}`
);
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
return mapping;
return {
global: global,
local: local,
};
}
function RateControl(props) {

View File

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

View File

@ -0,0 +1,174 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
function init(initialState) {
const state = {
bitrate: '4096',
fps: '25',
gop: '2',
profile: '77',
rc_mode: '1',
quality: '-1',
...initialState,
};
return state;
}
function createMapping(settings) {
const global = [];
const local = [];
// https://trac.ffmpeg.org/wiki/Hardware/VAAPI
global.push(['-vaapi_device', '/dev/dri/renderD128']);
local.push(
'-vf',
'format=nv12,hwupload',
'-codec:v',
'hevc_vaapi',
'-profile:v',
`${settings.profile}`,
'-quality',
`${settings.quality}`,
'-b:v',
`${settings.bitrate}k`,
'-maxrate',
`${settings.bitrate}k`,
'-bufsize',
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-g',
`${settings.gop}`
);
if (settings.gop !== 'auto') {
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
return {
global: global,
local: local,
};
}
function RateControl(props) {
return (
<Select label={<Trans>Rate control</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="0">auto</MenuItem>
<MenuItem value="1">Constant-quality</MenuItem>
<MenuItem value="2">Constant-bitrate</MenuItem>
<MenuItem value="3">Variable-bitrate</MenuItem>
<MenuItem value="4">Intelligent constant-quality</MenuItem>
<MenuItem value="5">Quality-defined variable-bitrate</MenuItem>
<MenuItem value="6">Average variable-bitrate</MenuItem>
</Select>
);
}
RateControl.defaultProps = {
value: '',
onChange: function (event) {},
};
function Profile(props) {
return (
<Select label={<Trans>Profile</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="578">baseline (constrained)</MenuItem>
<MenuItem value="77">main</MenuItem>
<MenuItem value="100">high</MenuItem>
</Select>
);
}
Profile.defaultProps = {
value: '',
onChange: function (event) {},
};
function Coder(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
[what]: event.target.value,
};
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<RateControl value={settings.rc_mode} onChange={update('rc_mode')} />
</Grid>
<Grid item xs={12} md={6}>
<Video.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12} md={6}>
<Video.Framerate value={settings.fps} onChange={update('fps')} allowCustom />
</Grid>
<Grid item xs={12} md={6}>
<Video.GOP value={settings.gop} onChange={update('gop')} allowAuto allowCustom />
</Grid>
<Grid item xs={6}>
<TextField variant="outlined" fullWidth label={<Trans>Quality</Trans>} type="number" value={settings.quality} onChange={update('quality')} />
</Grid>
<Grid item xs={6}>
<Profile value={settings.profile} onChange={update('profile')} />
</Grid>
</Grid>
);
}
Coder.defaultProps = {
stream: {},
settings: {},
onChange: function (settings, mapping) {},
};
const coder = 'hevc_vaapi';
const name = 'HEVC (Intel VAAPI)';
const codec = 'hevc';
const type = 'video';
const hwaccel = true;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
};
}
export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as component };

View File

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

View File

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

View File

@ -16,7 +16,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libvpx-vp9',
'-b:v',
@ -27,16 +27,26 @@ function createMapping(settings) {
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-sc_threshold',
'0',
'-pix_fmt',
'yuv420p',
'-vsync',
'1',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push(
'-g',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`,
'-keyint_min',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`
);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -0,0 +1,174 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import { Trans } from '@lingui/macro';
import Select from '../../../Select';
import Video from '../../settings/Video';
function init(initialState) {
const state = {
bitrate: '4096',
fps: '25',
gop: '2',
profile: '77',
rc_mode: '1',
quality: '-1',
...initialState,
};
return state;
}
function createMapping(settings) {
const global = [];
const local = [];
// https://trac.ffmpeg.org/wiki/Hardware/VAAPI
global.push(['-vaapi_device', '/dev/dri/renderD128']);
local.push(
'-vf',
'format=nv12,hwupload',
'-codec:v',
'vp9_vaapi',
'-profile:v',
`${settings.profile}`,
'-quality',
`${settings.quality}`,
'-b:v',
`${settings.bitrate}k`,
'-maxrate',
`${settings.bitrate}k`,
'-bufsize',
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-g',
`${settings.gop}`
);
if (settings.gop !== 'auto') {
local.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
return {
global: global,
local: local,
};
}
function RateControl(props) {
return (
<Select label={<Trans>Rate control</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="0">auto</MenuItem>
<MenuItem value="1">Constant-quality</MenuItem>
<MenuItem value="2">Constant-bitrate</MenuItem>
<MenuItem value="3">Variable-bitrate</MenuItem>
<MenuItem value="4">Intelligent constant-quality</MenuItem>
<MenuItem value="5">Quality-defined variable-bitrate</MenuItem>
<MenuItem value="6">Average variable-bitrate</MenuItem>
</Select>
);
}
RateControl.defaultProps = {
value: '',
onChange: function (event) {},
};
function Profile(props) {
return (
<Select label={<Trans>Profile</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="578">baseline (constrained)</MenuItem>
<MenuItem value="77">main</MenuItem>
<MenuItem value="100">high</MenuItem>
</Select>
);
}
Profile.defaultProps = {
value: '',
onChange: function (event) {},
};
function Coder(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createMapping(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
[what]: event.target.value,
};
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<RateControl value={settings.rc_mode} onChange={update('rc_mode')} />
</Grid>
<Grid item xs={12} md={6}>
<Video.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12} md={6}>
<Video.Framerate value={settings.fps} onChange={update('fps')} allowCustom />
</Grid>
<Grid item xs={12} md={6}>
<Video.GOP value={settings.gop} onChange={update('gop')} allowAuto allowCustom />
</Grid>
<Grid item xs={6}>
<TextField variant="outlined" fullWidth label={<Trans>Quality</Trans>} type="number" value={settings.quality} onChange={update('quality')} />
</Grid>
<Grid item xs={6}>
<Profile value={settings.profile} onChange={update('profile')} />
</Grid>
</Grid>
);
}
Coder.defaultProps = {
stream: {},
settings: {},
onChange: function (settings, mapping) {},
};
const coder = 'vp9_vaapi';
const name = 'VP9 (Intel VAAPI)';
const codec = 'vp9';
const type = 'video';
const hwaccel = true;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
};
}
export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as component };

View File

@ -23,7 +23,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libx264',
'-preset:v',
@ -36,24 +36,34 @@ function createMapping(settings) {
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-sc_threshold',
'0',
'-pix_fmt',
'yuv420p',
'-vsync',
'1',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push(
'-g',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`,
'-keyint_min',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`
);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.tune !== 'none') {
mapping.push('-tune:v', `${settings.tune}`);
local.push('-tune:v', `${settings.tune}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

View File

@ -23,7 +23,7 @@ function init(initialState) {
}
function createMapping(settings) {
const mapping = [
const local = [
'-codec:v',
'libx265',
'-preset:v',
@ -36,24 +36,34 @@ function createMapping(settings) {
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-sc_threshold',
'0',
'-pix_fmt',
'yuv420p',
'-vsync',
'1',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
local.push(
'-g',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`,
'-keyint_min',
`${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`
);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
local.push('-profile:v', `${settings.profile}`);
}
if (settings.tune !== 'none') {
mapping.push('-tune:v', `${settings.tune}`);
local.push('-tune:v', `${settings.tune}`);
}
const mapping = {
global: [],
local: local,
};
return mapping;
}

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

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

View File

@ -2,10 +2,12 @@ import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Checkbox from '../Checkbox';
import Select from '../Select';
function init(settings) {
const initSettings = {
@ -13,6 +15,9 @@ function init(settings) {
segmentDuration: 2,
listSize: 6,
cleanup: true,
version: 3,
storage: 'memfs',
master_playlist: true,
...settings,
};
@ -31,7 +36,7 @@ export default function Control(props) {
const handleChange = (what) => (event) => {
const value = event.target.value;
if (['lhls', 'cleanup'].includes(what)) {
if (['lhls', 'cleanup', 'master_playlist'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
@ -39,6 +44,7 @@ export default function Control(props) {
props.onChange(settings, false);
};
return (
<Grid container spacing={2}>
{/*
@ -55,6 +61,33 @@ export default function Control(props) {
</Typography>
</Grid>
*/}
<Grid item xs={12}>
<Select label={<Trans>Storage</Trans>} value={settings.storage} onChange={handleChange('storage')}>
<MenuItem value="memfs">
<Trans>In-memory</Trans>
</MenuItem>
<MenuItem value="diskfs">
<Trans>Disk</Trans>
</MenuItem>
</Select>
<Typography variant="caption">
<Trans>Where to store the HLS playlist and segments. In-Memory is recommended.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Select label={<Trans>EXT-X-VERSION</Trans>} value={settings.version} onChange={handleChange('version')}>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={6}>
<Trans>6 (+ guaranteed to start with a Key frame)</Trans>
</MenuItem>
<MenuItem value={7}>
<Trans>7 (+ fragmented MP4 format)</Trans>
</MenuItem>
</Select>
<Typography variant="caption">
<Trans>Playlist version (M3U8). Version 3 has the best browser/client compatibility.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
variant="outlined"
@ -79,6 +112,13 @@ export default function Control(props) {
<Trans>The maximum number of playlist segments. 0 will contain all the segments. 6 is recommended.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Master playlist (increases browser/client compatibility)</Trans>}
checked={settings.master_playlist}
onChange={handleChange('master_playlist')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox label={<Trans>Automatic cleanup of all media data</Trans>} checked={settings.cleanup} onChange={handleChange('cleanup')} />
</Grid>

83
src/misc/controls/RTMP.js Normal file
View File

@ -0,0 +1,83 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import BoxText from '../BoxText';
import Checkbox from '../Checkbox';
function init(settings) {
const initSettings = {
enable: false,
...settings,
};
return initSettings;
}
export default function Control(props) {
const navigate = useNavigate();
const settings = init(props.settings);
// Set the defaults
React.useEffect(() => {
props.onChange(settings, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (what) => (event) => {
const value = event.target.value;
if (['enable'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
}
props.onChange(settings, false);
};
return (
<Grid container spacing={2}>
{props.enabled && (
<Grid item xs={12}>
<Checkbox
label={<Trans>Enable</Trans>}
checked={settings.enable}
disabled={!props.enabled && settings.enable !== true}
onChange={handleChange('enable')}
/>
<Typography variant="caption">
<Trans>Make the channel available as an RTMP stream (experimental).</Trans>
</Typography>
</Grid>
)}
{!props.enabled && (
<Grid item xs={12}>
<BoxText textAlign="center">
<Trans>The RTMP output requires the RTMP Server.</Trans>
<Button
variant="outlined"
size="small"
style={{ marginTop: 10, marginBottom: 3 }}
fullWidth
color="primary"
onClick={() => navigate('/settings/rtmp')}
>
<Trans>Enable now</Trans>
</Button>
</BoxText>
</Grid>
)}
</Grid>
);
}
Control.defaulProps = {
settings: {},
enabled: false,
onChange: function (settings, automatic) {},
};

83
src/misc/controls/SRT.js Normal file
View File

@ -0,0 +1,83 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Trans } from '@lingui/macro';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import BoxText from '../BoxText';
import Checkbox from '../Checkbox';
function init(settings) {
const initSettings = {
enable: false,
...settings,
};
return initSettings;
}
export default function Control(props) {
const navigate = useNavigate();
const settings = init(props.settings);
// Set the defaults
React.useEffect(() => {
props.onChange(settings, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (what) => (event) => {
const value = event.target.value;
if (['enable'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
}
props.onChange(settings, false);
};
return (
<Grid container spacing={2}>
{props.enabled && (
<Grid item xs={12}>
<Checkbox
label={<Trans>Enable</Trans>}
checked={settings.enable}
disabled={!props.enabled && settings.enable !== true}
onChange={handleChange('enable')}
/>
<Typography variant="caption">
<Trans>Make the channel available as an SRT stream (experimental).</Trans>
</Typography>
</Grid>
)}
{!props.enabled && (
<Grid item xs={12}>
<BoxText textAlign="center">
<Trans>The SRT output requires the SRT Server.</Trans>
<Button
variant="outlined"
size="small"
style={{ marginTop: 10, marginBottom: 3 }}
fullWidth
color="primary"
onClick={() => navigate('/settings/srt')}
>
<Trans>Enable now</Trans>
</Button>
</BoxText>
</Grid>
)}
</Grid>
);
}
Control.defaulProps = {
settings: {},
enabled: false,
onChange: function (settings, automatic) {},
};

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import Select from '../Select';
function init(settings) {
const initSettings = {
source: 'hls+memfs',
...settings,
};
switch (initSettings.source) {
case 'hls+diskfs':
case 'rtmp':
case 'srt':
break;
default:
initSettings.source = 'hls+memfs';
}
return initSettings;
}
export default function Control(props) {
const settings = init(props.settings);
// Set the defaults
React.useEffect(() => {
props.onChange(settings, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (what) => (event) => {
const value = event.target.value;
settings[what] = value;
props.onChange(settings, false);
};
const items = [];
items.push(
<MenuItem key="hls+memfs" value="hls+memfs" disabled={!props.sources.includes('hls+memfs')}>
HLS (memfs)
</MenuItem>
);
items.push(
<MenuItem key="hls+diskfs" value="hls+diskfs" disabled={!props.sources.includes('hls+diskfs')}>
HLS (diskfs)
</MenuItem>
);
items.push(
<MenuItem key="rtmp" value="rtmp" disabled={!props.sources.includes('rtmp')}>
RTMP
</MenuItem>
);
items.push(
<MenuItem key="srt" value="srt" disabled={!props.sources.includes('srt')}>
SRT
</MenuItem>
);
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Select label={<Trans>Source</Trans>} value={settings.source} onChange={handleChange('source')}>
{items}
</Select>
<Typography variant="caption">
<Trans>Stream source for publication service (experimental).</Trans>
</Typography>
</Grid>
</Grid>
);
}
Control.defaulProps = {
settings: {},
sources: [],
onChange: function (settings, automatic) {},
};

View File

@ -0,0 +1,95 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Checkbox from '../../Checkbox';
// Loudnorm Filter
// http://ffmpeg.org/ffmpeg-all.html#loudnorm
function init(initialState) {
const state = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
if (settings.enabled) {
mapping.push('loudnorm');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} else {
newSettings[what] = event.target.value;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<React.Fragment>
<Grid item>
<Checkbox label={<Trans>Loudness Normalization</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'loudnorm';
const name = 'Loudness Normalization';
const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

View File

@ -0,0 +1,128 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import Select from '../../Select';
// Pan Filter
// https://ffmpeg.org/ffmpeg-filters.html#pan-1
function init(initialState) {
const state = {
value: 'inherit',
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
switch (settings.value) {
case 'mute_left':
mapping.push('pan=stereo|c1=c1');
break;
case 'mute_right':
mapping.push('pan=stereo|c0=c0');
break;
default:
break;
}
return mapping;
}
// filter
function Pan(props) {
return (
<React.Fragment>
<Select label={<Trans>Pan</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="inherit">
<Trans>Inherit</Trans>
</MenuItem>
<MenuItem value="mute_left">
<Trans>Mute left</Trans>
</MenuItem>
<MenuItem value="mute_right">
<Trans>Mute right</Trans>
</MenuItem>
</Select>
<Typography variant="caption">
<Trans>Mute a channel.</Trans>
</Typography>
</React.Fragment>
);
}
Pan.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,
};
newSettings[what] = event.target.value;
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<React.Fragment>
<Grid item xs={12}>
<Pan value={settings.value} onChange={update('value')} allowCustom />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'pan';
const name = 'Pan';
const type = 'audio';
const hwaccel = false;
function summarize(settings) {
return `${name} (${settings.value.replace(/_/i, ' ')})`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

View File

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

View File

@ -0,0 +1,161 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import Select from '../../Select';
// Volume Filter
// http://ffmpeg.org/ffmpeg-all.html#volume
function init(initialState) {
const state = {
level: 'inherit',
db: 0,
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
switch (settings.level) {
case 'inherit':
break;
case 'custom':
mapping.push(`volume=volume=${settings.db}dB`);
break;
default:
mapping.push(`volume=volume=${parseInt(settings.level) / 100}`);
break;
}
return mapping.join(',');
}
function VolumeLevel(props) {
return (
<Select label={<Trans>Volume</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="inherit">
<Trans>Inherit</Trans>
</MenuItem>
<MenuItem value="10">10%</MenuItem>
<MenuItem value="20">20%</MenuItem>
<MenuItem value="30">30%</MenuItem>
<MenuItem value="40">40%</MenuItem>
<MenuItem value="50">50%</MenuItem>
<MenuItem value="60">60%</MenuItem>
<MenuItem value="70">70%</MenuItem>
<MenuItem value="80">80%</MenuItem>
<MenuItem value="90">90%</MenuItem>
<MenuItem value="custom">
<Trans>Custom ...</Trans>
</MenuItem>
</Select>
);
}
VolumeLevel.defaultProps = {
value: '',
onChange: function (event) {},
};
function VolumeDB(props) {
return (
<TextField
variant="outlined"
fullWidth
label={<Trans>Decibels (dB)</Trans>}
type="number"
value={props.value}
disabled={props.disabled}
onChange={props.onChange}
/>
);
}
VolumeDB.defaultProps = {
value: '',
disabled: false,
onChange: function (event) {},
};
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
[what]: event.target.value,
};
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<React.Fragment>
<Grid item xs={6}>
<VolumeLevel value={settings.level} onChange={update('level')} />
</Grid>
<Grid item xs={6}>
<VolumeDB value={settings.db} onChange={update('db')} disabled={settings.level !== 'custom'} />
</Grid>
</React.Fragment>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'volume';
const name = 'Volume';
const type = 'audio';
const hwaccel = false;
function summarize(settings) {
let summary = `${name} (`;
if (settings.level === 'custom') {
summary += `${settings.db}dB`;
} else {
summary += `${settings.level}%`;
}
summary += ')';
return summary;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

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

@ -0,0 +1,59 @@
// Audio Filter
import * as AResample from './audio/Resample';
import * as Pan from './audio/Pan';
import * as Volume from './audio/Volume';
import * as Loudnorm from './audio/Loudnorm';
// Video Filter
import * as Transpose from './video/Transpose';
import * as HFlip from './video/HFlip';
import * as VFlip from './video/VFlip';
// Register filters type: audio/video
class Registry {
constructor(type) {
this.type = type;
this.services = new Map();
}
Register(service) {
if (service.type !== this.type) {
return;
}
this.services.set(service.filter, service);
}
Get(filter) {
const service = this.services.get(filter);
if (service) {
return service;
}
return null;
}
Filters() {
return Array.from(this.services.keys());
}
List() {
return Array.from(this.services.values());
}
}
// Audio Filters
const audioRegistry = new Registry('audio');
audioRegistry.Register(AResample);
audioRegistry.Register(Pan);
audioRegistry.Register(Volume);
audioRegistry.Register(Loudnorm);
// Video Filters
const videoRegistry = new Registry('video');
videoRegistry.Register(Transpose);
videoRegistry.Register(HFlip);
videoRegistry.Register(VFlip);
// Export registrys for ../SelectFilters.js
export { audioRegistry as Audio, videoRegistry as Video };

View File

@ -0,0 +1,93 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Checkbox from '../../Checkbox';
// HFlip Filter
// http://ffmpeg.org/ffmpeg-all.html#hflip
function init(initialState) {
const state = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
if (settings.enabled) {
mapping.push('hflip');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} else {
newSettings[what] = event.target.value;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid item>
<Checkbox label={<Trans>Horizontal Flip</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'hflip';
const name = 'Horizonal Flip';
const type = 'video';
const hwaccel = false;
function summarize(settings) {
return `${name}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

View File

@ -0,0 +1,117 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Select from '../../Select';
// Transpose Filter
// http://ffmpeg.org/ffmpeg-all.html#transpose-1
function init(initialState) {
const state = {
value: 'none',
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
switch (settings.value) {
case '90':
mapping.push('transpose=dir=clock:passthrough=none');
break;
case '180':
mapping.push('transpose=dir=clock:passthrough=none', 'transpose=dir=clock:passthrough=none');
break;
case '270':
mapping.push('transpose=dir=cclock:passthrough=none');
break;
default:
break;
}
return mapping.join(',');
}
// filter
function Rotate(props) {
return (
<Select label={<Trans>Rotate</Trans>} value={props.value} onChange={props.onChange}>
<MenuItem value="none">None</MenuItem>
<MenuItem value="90">90°</MenuItem>
<MenuItem value="180">180°</MenuItem>
<MenuItem value="270">270°</MenuItem>
</Select>
);
}
Rotate.defaultProps = {
value: '',
onChange: function (event) {},
};
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
[what]: event.target.value,
};
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid item xs={12}>
<Rotate value={settings.value} onChange={update('value')} allowCustom />
</Grid>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, mapping) {},
};
const filter = 'transpose';
const name = 'Transpose';
const type = 'video';
const hwaccel = false;
function summarize(settings) {
return `${name} (${settings.value}° clockwise)`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

View File

@ -0,0 +1,93 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Checkbox from '../../Checkbox';
// VFlip Filter
// http://ffmpeg.org/ffmpeg-all.html#vflip
function init(initialState) {
const state = {
enabled: false,
...initialState,
};
return state;
}
function createGraph(settings) {
settings = init(settings);
const mapping = [];
if (settings.enabled) {
mapping.push('vflip');
}
return mapping.join(',');
}
function Filter(props) {
const settings = init(props.settings);
const handleChange = (newSettings) => {
let automatic = false;
if (!newSettings) {
newSettings = settings;
automatic = true;
}
props.onChange(newSettings, createGraph(newSettings), automatic);
};
const update = (what) => (event) => {
const newSettings = {
...settings,
};
if (['enabled'].includes(what)) {
newSettings[what] = !settings.enabled;
} else {
newSettings[what] = event.target.value;
}
handleChange(newSettings);
};
React.useEffect(() => {
handleChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Grid item>
<Checkbox label={<Trans>Vertical Flip</Trans>} checked={settings.enabled} onChange={update('enabled')} />
</Grid>
);
}
Filter.defaultProps = {
settings: {},
onChange: function (settings, graph, automatic) {},
};
const filter = 'vflip';
const name = 'Vertical Flip';
const type = 'video';
const hwaccel = false;
function summarize(settings) {
return `${name}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
graph: createGraph(settings),
};
}
export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };

View File

@ -0,0 +1,11 @@
/* eslint-disable import/no-anonymous-default-export */
import base from '../base';
export default {
styleOverrides: {
root: {
color: base.palette.error.main,
marginLeft: 0,
},
},
};

View File

@ -16,6 +16,7 @@ import dialog from './components/dialog';
import divider from './components/divider';
import fab from './components/fab';
import formControlLabel from './components/formControlLabel';
import formHelperText from './components/formHelperText';
import formLabel from './components/formLabel';
import iconButton from './components/iconButton';
import inputLabel from './components/inputLabel';
@ -61,6 +62,7 @@ const theme = createTheme({
MuiDivider: { ...divider },
MuiFab: { ...fab },
MuiFormControlLabel: { ...formControlLabel },
MuiFormHelperText: { ...formHelperText },
MuiFormLabel: { ...formLabel },
MuiIconButton: { ...iconButton },
MuiInputLabel: { ...inputLabel },

View File

@ -122,9 +122,13 @@ const topics = {
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/restreamer-einstellungen/speicherplatz',
},
'settings-rtmp': {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/system-settings/rtmp ',
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/system-settings/rtmp',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/restreamer-einstellungen/rtmp',
},
'settings-srt': {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/system-settings/srt',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/restreamer-einstellungen/srt',
},
'settings-logging': {
en: 'https://docs.datarhei.com/restreamer/knowledge-base/manual/system-settings/logging',
de: 'https://docs.datarhei.com/restreamer/v/de/wissensdatenbank/user-guides/restreamer-einstellungen/protokollierung-and-fehlersuche',

View File

@ -3,7 +3,7 @@
Ingest Metadata Layout:
data = {
version: 1,
version: "1.2.0",
meta: {
name: 'Livestream 1',
description: 'Live from earth. Powered by datarhei/restreamer.',
@ -93,15 +93,30 @@ data = {
channels: '2',
sampling: '44100'
},
mapping: [
'-codec:a', 'aac',
'-b:a', '64k',
'-bsf:a', 'aac_adtstoasc',
'-shortest',
'-af', 'aresample=osr=44100:ocl=2'
]
mapping: {
global: [],
local: [
'-codec:a', 'aac',
'-b:a', '64k',
'-bsf:a', 'aac_adtstoasc',
'-shortest'
]
}
},
decoder: null,
filter: {
graph: 'aresample=osr=44100:ocl=stereo',
settings: {
aresample: {
graph: 'aresample=osr=44100:ocl=stereo',
settings: {
channels: 2,
layout: 'stereo',
sampling: 44100
}
}
}
},
},
video: {
source: 0,
@ -110,14 +125,15 @@ data = {
coder: 'copy',
codec: 'h264',
settings: {},
mapping: [
'-codec:v', 'copy',
'-vsync 0',
'-copyts',
'-start_at_zero',
]
mapping: {
global: [],
local: [
'-codec:v', 'copy',
]
}
},
decoder: null,
filter: null,
},
"or": {},
"video": {
@ -133,19 +149,21 @@ data = {
profile: 'auto',
tune: 'zerolatency',
},
mapping: [
'-codec:v', 'libx264',
'-preset:v', 'ultrafast',
'-b:v', '4096k',
'-maxrate', '4096k',
'-bufsize', '4096k',
'-r', '25',
'-g', '50',
'-pix_fmt', 'yuv420p',
'-vsync', '1',
'-profile:v', 'high',
'-tune:v', 'zerolatency',
]
mapping: {
global: [],
local: [
'-codec:v', 'libx264',
'-preset:v', 'ultrafast',
'-b:v', '4096k',
'-maxrate', '4096k',
'-bufsize', '4096k',
'-r', '25',
'-g', '50',
'-pix_fmt', 'yuv420p',
'-profile:v', 'high',
'-tune:v', 'zerolatency',
]
}
},
decoder: {
coder: 'h264_cuvid',
@ -201,7 +219,7 @@ data = {
Egress Metadata Layout:
data = {
version: 1,
version: "1.2.0",
name: "foobar",
control: {
process: {
@ -222,15 +240,20 @@ data = {
*/
import SemverGt from 'semver/functions/gt';
import SemverCompare from 'semver/functions/compare';
import * as Coders from '../misc/coders/Encoders';
import * as Filters from '../misc/filters';
import * as version from '../version';
const defaultMetadata = {
version: 1,
version: version.Version,
playersite: {},
};
const defaultIngestMetadata = {
version: 1,
version: version.Version,
sources: [],
profiles: [{}],
streams: [],
@ -239,6 +262,16 @@ const defaultIngestMetadata = {
lhls: false,
segmentDuration: 2,
listSize: 6,
cleanup: true,
version: 3,
storage: 'memfs',
master_playlist: true,
},
rtmp: {
enable: false,
},
srt: {
enable: false,
},
process: {
autostart: true,
@ -264,7 +297,7 @@ const defaultIngestMetadata = {
};
const defaultEgressMetadata = {
version: 1,
version: version.Version,
name: '',
control: {
process: {
@ -273,6 +306,9 @@ const defaultEgressMetadata = {
delay: 15,
staleTimeout: 30,
},
source: {
source: 'hls+memfs',
},
},
outputs: [],
settings: {},
@ -295,6 +331,12 @@ const getDefaultEgressMetadata = () => {
return JSON.parse(JSON.stringify(defaultEgressMetadata));
};
const initMetadata = (initialMetadata) => {
return mergeMetadata(initialMetadata);
};
const transformers = {};
const mergeMetadata = (metadata, base) => {
if (!metadata) {
metadata = {};
@ -311,28 +353,57 @@ const mergeMetadata = (metadata, base) => {
...metadata,
};
if (metadata.version !== defaultMetadata.version) {
metadata = {
...defaultMetadata,
};
}
metadata.playersite = {
...base.playersite,
...metadata.playersite,
};
return metadata;
};
metadata = transformMetadata(metadata, defaultMetadata.version, transformers);
const initMetadata = (initialMetadata) => {
return mergeMetadata(initialMetadata);
return metadata;
};
const initIngestMetadata = (initialMetadata) => {
return mergeIngestMetadata(initialMetadata);
};
const ingestTransformers = {
'1.2.0': (metadata) => {
for (let p = 0; p < metadata.profiles.length; p++) {
const profile = metadata.profiles[p];
if (profile.audio.encoder.coder === 'copy' || profile.audio.encoder.coder === 'none') {
continue;
}
const settings = profile.audio.encoder.settings;
profile.audio.filter = {
settings: {
aresample: {
settings: {
channels: settings.channels,
layout: settings.layout,
sampling: settings.sampling,
},
},
},
};
delete profile.audio.encoder.settings.channels;
delete profile.audio.encoder.settings.layout;
delete profile.audio.encoder.settings.sampling;
profile.audio.filter.settings.aresample.graph = Filters.Audio.Get('aresample').createGraph(profile.audio.filter.settings.aresample.settings);
profile.audio.filter.graph = profile.audio.filter.settings.aresample.graph;
}
metadata.version = '1.2.0';
return metadata;
},
};
const mergeIngestMetadata = (metadata, base) => {
if (!metadata) {
metadata = {};
@ -349,12 +420,6 @@ const mergeIngestMetadata = (metadata, base) => {
...metadata,
};
if (metadata.version !== defaultMetadata.version) {
metadata = {
...defaultMetadata,
};
}
metadata.meta = {
...base.meta,
...metadata.meta,
@ -414,6 +479,8 @@ const mergeIngestMetadata = (metadata, base) => {
}
}
metadata = transformMetadata(metadata, defaultMetadata.version, ingestTransformers);
return metadata;
};
@ -421,6 +488,8 @@ const initEgressMetadata = (initialMetadata) => {
return mergeEgressMetadata(initialMetadata);
};
const egressTransformers = {};
const mergeEgressMetadata = (metadata, base) => {
if (!metadata) {
metadata = {};
@ -437,12 +506,6 @@ const mergeEgressMetadata = (metadata, base) => {
...metadata,
};
if (metadata.version !== defaultMetadata.version) {
metadata = {
...defaultMetadata,
};
}
metadata.control = {
...base.control,
...metadata.control,
@ -453,6 +516,11 @@ const mergeEgressMetadata = (metadata, base) => {
...metadata.control.process,
};
metadata.control.source = {
...base.control.source,
...metadata.control.source,
};
if (!Array.isArray(metadata.outputs)) {
metadata.outputs = [];
} else {
@ -477,17 +545,15 @@ const mergeEgressMetadata = (metadata, base) => {
}
}
metadata = transformMetadata(metadata, defaultMetadata.version, egressTransformers);
return metadata;
};
const validateProfile = (sources, profile) => {
let validVideo = false;
if (!('video' in profile)) {
profile.video = initProfile({});
} else {
profile.video = initProfile(profile.video);
}
profile = initProfile(profile);
if (profile.video.source !== -1 && profile.video.source < sources.length) {
const source = sources[profile.video.source];
@ -505,12 +571,6 @@ const validateProfile = (sources, profile) => {
let validAudio = false;
if (!('audio' in profile)) {
profile.audio = initProfile({});
} else {
profile.audio = initProfile(profile.audio);
}
if (profile.audio.source !== -1 && profile.audio.source < sources.length) {
const source = sources[profile.audio.source];
@ -547,6 +607,7 @@ const validateProfile = (sources, profile) => {
const createInputsOutputs = (sources, profiles) => {
const source2inputMap = new Map();
let global = [];
const inputs = [];
const outputs = [];
@ -559,11 +620,13 @@ const createInputsOutputs = (sources, profiles) => {
let index = -1;
global = [...global, ...profile.video.decoder.mapping.global];
const source = sources[profile.video.source];
const stream = source.streams[profile.video.stream];
const input = source.inputs[stream.index];
input.options = [...profile.video.decoder.mapping, ...input.options];
input.options = [...profile.video.decoder.mapping.local, ...input.options];
const id = profile.video.source + ':' + stream.index;
@ -576,14 +639,30 @@ const createInputsOutputs = (sources, profiles) => {
index = source2inputMap.get(id);
const options = ['-map', index + ':' + stream.stream, ...profile.video.encoder.mapping];
global = [...global, ...profile.video.encoder.mapping.global];
const local = profile.video.encoder.mapping.local.slice();
if (profile.video.encoder.coder !== 'copy' && profile.video.filter.graph.length !== 0) {
// Check if there's already a video filter in the local mapping
let filterIndex = local.indexOf('-filter:v');
if (filterIndex !== -1) {
local[filterIndex + 1] += ',' + profile.video.filter.graph;
} else {
local.unshift('-filter:v', profile.video.filter.graph);
}
}
const options = ['-map', index + ':' + stream.stream, ...local];
if (profile.audio.encoder.coder !== 'none' && profile.audio.source !== -1 && profile.audio.stream !== -1) {
global = [...global, ...profile.audio.decoder.mapping.global];
const source = sources[profile.audio.source];
const stream = source.streams[profile.audio.stream];
const input = source.inputs[stream.index];
input.options = [...profile.audio.decoder.mapping, ...input.options];
input.options = [...profile.audio.decoder.mapping.local, ...input.options];
const id = profile.audio.source + ':' + stream.index;
@ -594,7 +673,21 @@ const createInputsOutputs = (sources, profiles) => {
index = source2inputMap.get(id);
options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping);
global = [...global, ...profile.audio.encoder.mapping.global];
const local = profile.audio.encoder.mapping.local.slice();
if (profile.audio.encoder.coder !== 'copy' && profile.audio.filter.graph.length !== 0) {
// Check if there's already a audio filter in the local mapping
let filterIndex = local.indexOf('-filter:a');
if (filterIndex !== -1) {
local[filterIndex + 1] += ',' + profile.audio.filter.graph;
} else {
local.unshift('-filter:a', profile.audio.filter.graph);
}
}
options.push('-map', index + ':' + stream.stream, ...local);
} else {
options.push('-an');
}
@ -605,7 +698,16 @@ const createInputsOutputs = (sources, profiles) => {
});
}
return [inputs, outputs];
// https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array
const uniqBy = (a, key) => {
return [...new Map(a.map((x) => [key(x), x])).values()];
};
// global is an array of arrays. Here we remove duplicates and flatten it.
global = uniqBy(global, (x) => JSON.stringify(x.sort()));
global = global.reduce((acc, val) => acc.concat(val), []);
return [global, inputs, outputs];
};
const createOutputStreams = (sources, profiles) => {
@ -711,45 +813,112 @@ const initProfile = (initialProfile) => {
stream: -1,
encoder: {},
decoder: {},
filter: {},
...profile.video,
};
profile.video.encoder = {
coder: 'none',
settings: {},
mapping: [],
mapping: {},
...profile.video.encoder,
};
// mapping used to be an array for input/output specific options
if (Array.isArray(profile.video.encoder.mapping)) {
profile.video.encoder.mapping = {
global: [],
local: profile.video.encoder.mapping,
};
} else {
profile.video.encoder.mapping = {
global: [],
local: [],
...profile.video.encoder.mapping,
};
}
profile.video.decoder = {
coder: 'default',
settings: {},
mapping: [],
mapping: {},
...profile.video.decoder,
};
if (Array.isArray(profile.video.decoder.mapping)) {
profile.video.decoder.mapping = {
global: [],
local: profile.video.decoder.mapping,
};
} else {
profile.video.decoder.mapping = {
global: [],
local: [],
...profile.video.decoder.mapping,
};
}
profile.video.filter = {
graph: '',
settings: {},
...profile.video.filter,
};
profile.audio = {
source: -1,
stream: -1,
encoder: {},
decoder: {},
filter: {},
...profile.audio,
};
profile.audio.encoder = {
coder: 'none',
settings: {},
mapping: [],
mapping: {},
...profile.audio.encoder,
};
if (Array.isArray(profile.audio.encoder.mapping)) {
profile.audio.encoder.mapping = {
global: [],
local: profile.audio.encoder.mapping,
};
} else {
profile.audio.encoder.mapping = {
global: [],
local: [],
...profile.audio.encoder.mapping,
};
}
profile.audio.decoder = {
coder: 'default',
settings: {},
mapping: [],
mapping: {},
...profile.audio.decoder,
};
if (Array.isArray(profile.audio.decoder.mapping)) {
profile.audio.decoder.mapping = {
global: [],
local: profile.audio.decoder.mapping,
};
} else {
profile.audio.decoder.mapping = {
global: [],
local: [],
...profile.audio.decoder.mapping,
};
}
profile.audio.filter = {
graph: '',
settings: {},
...profile.audio.filter,
};
profile.custom = {
selected: profile.audio.source === 1,
stream: profile.audio.source === 1 ? -2 : profile.audio.stream,
@ -1025,6 +1194,39 @@ const cleanupProfile = (profile) => {
};
};
const transformMetadata = (metadata, targetVersion, transformers) => {
if (metadata.version === 1) {
metadata.version = '1.0.0';
}
if (targetVersion === 1) {
targetVersion = '1.0.0';
}
if (metadata.version === targetVersion) {
return metadata;
}
// Create a list of all transformers that are greater than the current version
// and sort them in ascending order.
const tlist = [];
for (let v in transformers) {
if (SemverGt(v, metadata.version)) {
tlist.push(v);
}
}
tlist.sort(SemverCompare);
// Apply all found transformers
for (let t of tlist) {
metadata = transformers[t](metadata);
}
return metadata;
};
export {
getDefaultMetadata,
getDefaultIngestMetadata,

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More