Merge branch 'dev'
This commit is contained in:
commit
2ac8bb5e87
3
.github/workflows/build_restreamer-ui.yaml
vendored
3
.github/workflows/build_restreamer-ui.yaml
vendored
@ -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: |
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
# RESTREAMER UI
|
||||
RELEASE=1.1.0
|
||||
RELEASE=1.2.0
|
||||
NODE_IMAGE=node:18.6.0-alpine3.15
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
22
README.md
22
README.md
@ -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.
|
||||
|
||||
28
package.json
28
package.json
@ -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": {
|
||||
|
||||
15
public/_player/videojs/dist/ic_airplay_white_24px.svg
vendored
Normal file
15
public/_player/videojs/dist/ic_airplay_white_24px.svg
vendored
Normal 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 |
BIN
public/_player/videojs/dist/ic_cast_connected_white_24dp.png
vendored
Normal file
BIN
public/_player/videojs/dist/ic_cast_connected_white_24dp.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 981 B |
BIN
public/_player/videojs/dist/ic_cast_white_24dp.png
vendored
Normal file
BIN
public/_player/videojs/dist/ic_cast_white_24dp.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 824 B |
1
public/_player/videojs/dist/videojs-airplay.min.css
vendored
Normal file
1
public/_player/videojs/dist/videojs-airplay.min.css
vendored
Normal 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")}
|
||||
1315
public/_player/videojs/dist/videojs-airplay.min.js
vendored
Normal file
1315
public/_player/videojs/dist/videojs-airplay.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
65
public/_player/videojs/dist/videojs-chromecast.css
vendored
Normal file
65
public/_player/videojs/dist/videojs-chromecast.css
vendored
Normal 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;
|
||||
}
|
||||
1
public/_player/videojs/dist/videojs-chromecast.min.css
vendored
Normal file
1
public/_player/videojs/dist/videojs-chromecast.min.css
vendored
Normal 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}
|
||||
3147
public/_player/videojs/dist/videojs-chromecast.min.js
vendored
Normal file
3147
public/_player/videojs/dist/videojs-chromecast.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
11
src/I18n.js
11
src/I18n.js
@ -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
1
src/locales/pl/messages.js
Normal file
1
src/locales/pl/messages.js
Normal file
File diff suppressed because one or more lines are too long
2936
src/locales/pl/messages.po
Normal file
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
@ -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',
|
||||
};
|
||||
|
||||
@ -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
134
src/misc/FilterSelect.js
Normal 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) {},
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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) {},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -9,7 +9,10 @@ function init(initialState) {
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
const mapping = {
|
||||
global: [],
|
||||
local: [],
|
||||
};
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@ -9,7 +9,10 @@ function init(initialState) {
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = [];
|
||||
const mapping = {
|
||||
global: [],
|
||||
local: [],
|
||||
};
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -9,7 +9,10 @@ function init(initialState) {
|
||||
}
|
||||
|
||||
function createMapping(settings) {
|
||||
const mapping = ['-hwaccel', 'videotoolbox'];
|
||||
const mapping = {
|
||||
global: [],
|
||||
local: ['-hwaccel', 'videotoolbox'],
|
||||
};
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
174
src/misc/coders/Encoders/video/HEVCVAAPI.js
Normal file
174
src/misc/coders/Encoders/video/HEVCVAAPI.js
Normal 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 };
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
174
src/misc/coders/Encoders/video/VP9VAAPI.js
Normal file
174
src/misc/coders/Encoders/video/VP9VAAPI.js
Normal 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 };
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
118
src/misc/coders/README.md
Normal 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.
|
||||
@ -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
83
src/misc/controls/RTMP.js
Normal 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
83
src/misc/controls/SRT.js
Normal 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) {},
|
||||
};
|
||||
89
src/misc/controls/Source.js
Normal file
89
src/misc/controls/Source.js
Normal 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) {},
|
||||
};
|
||||
95
src/misc/filters/audio/Loudnorm.js
Normal file
95
src/misc/filters/audio/Loudnorm.js
Normal 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 };
|
||||
128
src/misc/filters/audio/Pan.js
Normal file
128
src/misc/filters/audio/Pan.js
Normal 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 };
|
||||
226
src/misc/filters/audio/Resample.js
Normal file
226
src/misc/filters/audio/Resample.js
Normal 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 };
|
||||
161
src/misc/filters/audio/Volume.js
Normal file
161
src/misc/filters/audio/Volume.js
Normal 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
59
src/misc/filters/index.js
Normal 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 };
|
||||
93
src/misc/filters/video/HFlip.js
Normal file
93
src/misc/filters/video/HFlip.js
Normal 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 };
|
||||
117
src/misc/filters/video/Transpose.js
Normal file
117
src/misc/filters/video/Transpose.js
Normal 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 };
|
||||
93
src/misc/filters/video/VFlip.js
Normal file
93
src/misc/filters/video/VFlip.js
Normal 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 };
|
||||
11
src/theme/components/formHelperText.js
Normal file
11
src/theme/components/formHelperText.js
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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 },
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user