Add v1.1.0

This commit is contained in:
Jan Stabenow 2022-06-03 17:14:06 +02:00
parent 110a36a60f
commit 0bc412c633
No known key found for this signature in database
GPG Key ID: 9C22DD65A9AAF133
101 changed files with 8485 additions and 4984 deletions

View File

@ -3,6 +3,7 @@ Dockerfile*
.editorconfig
.gitignore
README.md
CHANGELOG.md
node_modules/
.yarn/cache
.eslintcache

View File

@ -1,10 +1,24 @@
{
"catalogs": [{
"catalogs": [
{
"path": "src/locales/{locale}/messages",
"include": ["src/"],
"exclude": ["**/node_modules/**"]
}],
"include": [
"src/"
],
"exclude": [
"**/node_modules/**"
]
}
],
"format": "po",
"sourceLocale": "en",
"locales": ["en", "de", "fr", "it", "pt", "es"]
"locales": [
"en",
"de",
"fr",
"it",
"pt",
"es",
"ru"
]
}

28
CHANGELOG.md Normal file
View File

@ -0,0 +1,28 @@
# Restreamer-UI
#### v1.0.0 > v1.1.0
- Add "HLS cleanup" as an optional function ([Philipp Trenz](https://github.com/philipptrenz))
- Add /ui info to / ([#326](https://github.com/datarhei/restreamer/issues/326))
- Add Russian translation (thx Inthegamelp)
- 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)
- 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))
- Fix reverse proxy issue ([#340](https://github.com/datarhei/restreamer/issues/340))
- Fix double escape failer ([#336](https://github.com/datarhei/restreamer/issues/336))
- Fix type in player plugin ([#336](https://github.com/datarhei/restreamer/issues/336))
- Fix deletes processes with dependencies (thx Patron Ramakrishna Chillara)
- Fix datarhei Core publication service
- Fix dependabot alerts
- Fix code scanning alerts
- Merge security pr
Preparation for FFmpeg v5.0 (migration will not work)
- Add FFmpeg v5.0 commands (preparation)
- Mod allows FFmpeg v5.0 (preparation)

View File

@ -1,11 +1,23 @@
FROM node:17-alpine3.15
FROM node:17.9.0-alpine3.15
WORKDIR /ui
ARG NODE_SPACE_SIZE=10240
ENV NODE_OPTIONS="--openssl-legacy-provider --max-old-space-size=$NODE_SPACE_SIZE"
ENV PUBLIC_URL "/"
COPY . /ui
RUN yarn install && \
WORKDIR /ui
RUN cd /ui && \
npm config set fetch-retries 10 && \
npm config set fetch-retry-mintimeout 100000 && \
npm config set fetch-retry-maxtimeout 600000 && \
npm config set cache-min 3600 && \
npm config ls -l && \
npm install && \
npm run build
EXPOSE 3000
CMD [ "npm", "run", "start" ]

View File

@ -178,7 +178,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2022 FOSS GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,39 +1,37 @@
{
"name": "restreamer-ui",
"version": "1.0.0",
"bundle": "restreamer-v2.0.0",
"version": "1.1.0",
"bundle": "restreamer-v2.1.0",
"private": false,
"license": "Apache-2.0",
"dependencies": {
"@auth0/auth0-spa-js": "^1.21.1",
"@clappr/core": "^0.4.17",
"@clappr/hlsjs-playback": "^0.5.3",
"@clappr/plugins": "^0.4.10",
"@auth0/auth0-spa-js": "^1.22.0",
"@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.5.0",
"@emotion/styled": "^11.3.0",
"@fontsource/dosis": "^4.5.1",
"@fontsource/roboto": "^4.5.5",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@fontsource/dosis": "^4.5.8",
"@fontsource/roboto": "^4.5.7",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"@lingui/core": "^3.13.2",
"@lingui/macro": "^3.4.0",
"@lingui/react": "^3.4.0",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@mui/icons-material": "^5.0.4",
"@mui/lab": "^5.0.0-alpha.51",
"@mui/material": "^5.0.4",
"@mui/styles": "^5.7.0",
"@testing-library/dom": ">=5",
"@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",
"@mui/material": "5.1.1",
"@mui/styles": "^5.1.1",
"@testing-library/dom": "^8.13.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"babel-plugin-macros": "2 || 3",
"eslint": "^7.19.0",
"handlebars": "^4.7.6",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^13.5.0",
"babel-plugin-macros": "^3.1.0",
"eslint": "^7.32.0",
"handlebars": "^4.7.7",
"hls.js": "^0.14.17",
"jwt-decode": "^3.1.2",
"make-plural": "^7.1.0",
@ -41,9 +39,9 @@
"react-colorful": "^5.5.1",
"react-device-detect": "^2.2.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",
"react-scripts": "4.0.3",
"semver": "^7.3.4",
"react-router-dom": "^6.3.0",
"react-scripts": "^4.0.3",
"semver": "^7.3.7",
"typescript": "^3.9.7",
"url-parse": "^1.5.10",
"uuid": "^8.3.2",
@ -84,10 +82,10 @@
]
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@lingui/cli": "^3.4.0",
"@babel/core": "^7.18.2",
"@lingui/cli": "^3.13.3",
"babel-core": "^7.0.0-bridge.0",
"prettier": "2.2.1",
"prettier": "^2.6.2",
"react-error-overlay": "^6.0.11"
},
"resolutions": {

View File

@ -384,26 +384,33 @@
.video-js.vjs-1-1 {
width: 100%;
max-width: 100%;
}
.video-js.vjs-fluid:not(.vjs-audio-only-mode),
.video-js.vjs-16-9:not(.vjs-audio-only-mode),
.video-js.vjs-4-3:not(.vjs-audio-only-mode),
.video-js.vjs-9-16:not(.vjs-audio-only-mode),
.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
height: 0;
}
.video-js.vjs-16-9 {
.video-js.vjs-16-9:not(.vjs-audio-only-mode) {
padding-top: 56.25%;
}
.video-js.vjs-4-3 {
.video-js.vjs-4-3:not(.vjs-audio-only-mode) {
padding-top: 75%;
}
.video-js.vjs-9-16 {
.video-js.vjs-9-16:not(.vjs-audio-only-mode) {
padding-top: 177.7777777778%;
}
.video-js.vjs-1-1 {
.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
padding-top: 100%;
}
.video-js.vjs-fill {
.video-js.vjs-fill:not(.vjs-audio-only-mode) {
width: 100%;
height: 100%;
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
/**
* @license
* Video.js 7.19.0 <http://videojs.com/>
* Video.js 7.19.2 <http://videojs.com/>
* Copyright Brightcove, Inc. <https://www.brightcove.com/>
* Available under Apache License Version 2.0
* <https://github.com/videojs/video.js/blob/main/LICENSE>
@ -16,7 +16,7 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
}(this, (function () { 'use strict';
var version$5 = "7.19.0";
var version$5 = "7.19.2";
/**
* An Object that contains lifecycle hooks as keys which point to an array
@ -18587,8 +18587,10 @@
if (this.items && this.items.length <= this.hideThreshold_) {
this.hide();
this.menu.contentEl_.removeAttribute('role');
} else {
this.show();
this.menu.contentEl_.setAttribute('role', 'menu');
}
}
/**
@ -25588,7 +25590,7 @@
this.addClass(idClass);
setTextContent(this.styleEl_, "\n ." + idClass + " {\n width: " + width + "px;\n height: " + height + "px;\n }\n\n ." + idClass + ".vjs-fluid {\n padding-top: " + ratioMultiplier * 100 + "%;\n }\n ");
setTextContent(this.styleEl_, "\n ." + idClass + " {\n width: " + width + "px;\n height: " + height + "px;\n }\n\n ." + idClass + ".vjs-fluid:not(.vjs-audio-only-mode) {\n padding-top: " + ratioMultiplier * 100 + "%;\n }\n ");
}
/**
* Load/Create an instance of playback {@link Tech} including element
@ -30889,7 +30891,7 @@
head.insertBefore(style, head.firstChild);
}
setTextContent(style, "\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid {\n padding-top: 56.25%\n }\n ");
setTextContent(style, "\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid:not(.vjs-audio-only-mode) {\n padding-top: 56.25%\n }\n ");
}
} // Run Auto-load players
// You have to wait at least once in case this script is loaded after your
@ -31515,7 +31517,7 @@
return array;
}
/*! @name m3u8-parser @version 4.7.0 @license Apache-2.0 */
/*! @name m3u8-parser @version 4.7.1 @license Apache-2.0 */
/**
* A stream that buffers string input and generates a `data` event for each
* line.
@ -32577,6 +32579,15 @@
attributes: entry.attributes
};
return;
}
if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
this.manifest.contentProtection['com.microsoft.playready'] = {
uri: entry.attributes.URI
};
return;
} // check if the content is encrypted for Widevine
// Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
@ -33290,6 +33301,193 @@
return null;
};
// const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2));
// we used to do this with log2 but BigInt does not support builtin math
// Math.ceil(log2(x));
var countBits = function countBits(x) {
return x.toString(2).length;
}; // count the number of whole bytes it would take to represent a number
var countBytes = function countBytes(x) {
return Math.ceil(countBits(x) / 8);
};
var isArrayBufferView = function isArrayBufferView(obj) {
if (ArrayBuffer.isView === 'function') {
return ArrayBuffer.isView(obj);
}
return obj && obj.buffer instanceof ArrayBuffer;
};
var isTypedArray = function isTypedArray(obj) {
return isArrayBufferView(obj);
};
var toUint8 = function toUint8(bytes) {
if (bytes instanceof Uint8Array) {
return bytes;
}
if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) {
// any non-number or NaN leads to empty uint8array
// eslint-disable-next-line
if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) {
bytes = 0;
} else {
bytes = [bytes];
}
}
return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0);
};
var BigInt = window.BigInt || Number;
var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
var bytesToNumber = function bytesToNumber(bytes, _temp) {
var _ref = _temp === void 0 ? {} : _temp,
_ref$signed = _ref.signed,
signed = _ref$signed === void 0 ? false : _ref$signed,
_ref$le = _ref.le,
le = _ref$le === void 0 ? false : _ref$le;
bytes = toUint8(bytes);
var fn = le ? 'reduce' : 'reduceRight';
var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn];
var number = obj.call(bytes, function (total, _byte, i) {
var exponent = le ? i : Math.abs(i + 1 - bytes.length);
return total + BigInt(_byte) * BYTE_TABLE[exponent];
}, BigInt(0));
if (signed) {
var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1);
number = BigInt(number);
if (number > max) {
number -= max;
number -= max;
number -= BigInt(2);
}
}
return Number(number);
};
var numberToBytes = function numberToBytes(number, _temp2) {
var _ref2 = _temp2 === void 0 ? {} : _temp2,
_ref2$le = _ref2.le,
le = _ref2$le === void 0 ? false : _ref2$le; // eslint-disable-next-line
if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) {
number = 0;
}
number = BigInt(number);
var byteCount = countBytes(number);
var bytes = new Uint8Array(new ArrayBuffer(byteCount));
for (var i = 0; i < byteCount; i++) {
var byteIndex = le ? i : Math.abs(i + 1 - bytes.length);
bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF));
if (number < 0) {
bytes[byteIndex] = Math.abs(~bytes[byteIndex]);
bytes[byteIndex] -= i === 0 ? 1 : 2;
}
}
return bytes;
};
var stringToBytes = function stringToBytes(string, stringIsBytes) {
if (typeof string !== 'string' && string && typeof string.toString === 'function') {
string = string.toString();
}
if (typeof string !== 'string') {
return new Uint8Array();
} // If the string already is bytes, we don't have to do this
// otherwise we do this so that we split multi length characters
// into individual bytes
if (!stringIsBytes) {
string = unescape(encodeURIComponent(string));
}
var view = new Uint8Array(string.length);
for (var i = 0; i < string.length; i++) {
view[i] = string.charCodeAt(i);
}
return view;
};
var concatTypedArrays = function concatTypedArrays() {
for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) {
buffers[_key] = arguments[_key];
}
buffers = buffers.filter(function (b) {
return b && (b.byteLength || b.length) && typeof b !== 'string';
});
if (buffers.length <= 1) {
// for 0 length we will return empty uint8
// for 1 length we return the first uint8
return toUint8(buffers[0]);
}
var totalLen = buffers.reduce(function (total, buf, i) {
return total + (buf.byteLength || buf.length);
}, 0);
var tempBuffer = new Uint8Array(totalLen);
var offset = 0;
buffers.forEach(function (buf) {
buf = toUint8(buf);
tempBuffer.set(buf, offset);
offset += buf.byteLength;
});
return tempBuffer;
};
/**
* Check if the bytes "b" are contained within bytes "a".
*
* @param {Uint8Array|Array} a
* Bytes to check in
*
* @param {Uint8Array|Array} b
* Bytes to check for
*
* @param {Object} options
* options
*
* @param {Array|Uint8Array} [offset=0]
* offset to use when looking at bytes in a
*
* @param {Array|Uint8Array} [mask=[]]
* mask to use on bytes before comparison.
*
* @return {boolean}
* If all bytes in b are inside of a, taking into account
* bit masks.
*/
var bytesMatch = function bytesMatch(a, b, _temp3) {
var _ref3 = _temp3 === void 0 ? {} : _temp3,
_ref3$offset = _ref3.offset,
offset = _ref3$offset === void 0 ? 0 : _ref3$offset,
_ref3$mask = _ref3.mask,
mask = _ref3$mask === void 0 ? [] : _ref3$mask;
a = toUint8(a);
b = toUint8(b); // ie 11 does not support uint8 every
var fn = b.every ? b.every : Array.prototype.every;
return b.length && a.length - offset >= b.length && // ie 11 doesn't support every on uin8
fn.call(b, function (bByte, i) {
var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i];
return bByte === aByte;
});
};
/**
* Loops through all supported media groups in master and calls the provided
* callback for each group
@ -36516,7 +36714,7 @@
var DOMParser = domParser.DOMParser;
/*! @name mpd-parser @version 0.21.0 @license Apache-2.0 */
/*! @name mpd-parser @version 0.21.1 @license Apache-2.0 */
var isObject = function isObject(obj) {
return !!obj && typeof obj === 'object';
@ -38733,7 +38931,15 @@
var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) {
return contentProtectionNodes.reduce(function (acc, node) {
var attributes = parseAttributes(node);
var attributes = parseAttributes(node); // Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated
// as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
// UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
// .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
if (attributes.schemeIdUri) {
attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
}
var keySystem = keySystemsMap[attributes.schemeIdUri];
if (keySystem) {
@ -38744,8 +38950,7 @@
if (psshNode) {
var pssh = getContent(psshNode);
var psshBuffer = pssh && decodeB64ToUint8Array(pssh);
acc[keySystem].pssh = psshBuffer;
acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh);
}
}
@ -39268,186 +39473,6 @@
var parseSidx_1 = parseSidx;
// const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2));
// we used to do this with log2 but BigInt does not support builtin math
// Math.ceil(log2(x));
var countBits = function countBits(x) {
return x.toString(2).length;
}; // count the number of whole bytes it would take to represent a number
var countBytes = function countBytes(x) {
return Math.ceil(countBits(x) / 8);
};
var isTypedArray = function isTypedArray(obj) {
return ArrayBuffer.isView(obj);
};
var toUint8 = function toUint8(bytes) {
if (bytes instanceof Uint8Array) {
return bytes;
}
if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) {
// any non-number or NaN leads to empty uint8array
// eslint-disable-next-line
if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) {
bytes = 0;
} else {
bytes = [bytes];
}
}
return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0);
};
var BigInt = window.BigInt || Number;
var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
var bytesToNumber = function bytesToNumber(bytes, _temp) {
var _ref = _temp === void 0 ? {} : _temp,
_ref$signed = _ref.signed,
signed = _ref$signed === void 0 ? false : _ref$signed,
_ref$le = _ref.le,
le = _ref$le === void 0 ? false : _ref$le;
bytes = toUint8(bytes);
var fn = le ? 'reduce' : 'reduceRight';
var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn];
var number = obj.call(bytes, function (total, _byte, i) {
var exponent = le ? i : Math.abs(i + 1 - bytes.length);
return total + BigInt(_byte) * BYTE_TABLE[exponent];
}, BigInt(0));
if (signed) {
var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1);
number = BigInt(number);
if (number > max) {
number -= max;
number -= max;
number -= BigInt(2);
}
}
return Number(number);
};
var numberToBytes = function numberToBytes(number, _temp2) {
var _ref2 = _temp2 === void 0 ? {} : _temp2,
_ref2$le = _ref2.le,
le = _ref2$le === void 0 ? false : _ref2$le; // eslint-disable-next-line
if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) {
number = 0;
}
number = BigInt(number);
var byteCount = countBytes(number);
var bytes = new Uint8Array(new ArrayBuffer(byteCount));
for (var i = 0; i < byteCount; i++) {
var byteIndex = le ? i : Math.abs(i + 1 - bytes.length);
bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF));
if (number < 0) {
bytes[byteIndex] = Math.abs(~bytes[byteIndex]);
bytes[byteIndex] -= i === 0 ? 1 : 2;
}
}
return bytes;
};
var stringToBytes = function stringToBytes(string, stringIsBytes) {
if (typeof string !== 'string' && string && typeof string.toString === 'function') {
string = string.toString();
}
if (typeof string !== 'string') {
return new Uint8Array();
} // If the string already is bytes, we don't have to do this
// otherwise we do this so that we split multi length characters
// into individual bytes
if (!stringIsBytes) {
string = unescape(encodeURIComponent(string));
}
var view = new Uint8Array(string.length);
for (var i = 0; i < string.length; i++) {
view[i] = string.charCodeAt(i);
}
return view;
};
var concatTypedArrays = function concatTypedArrays() {
for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) {
buffers[_key] = arguments[_key];
}
buffers = buffers.filter(function (b) {
return b && (b.byteLength || b.length) && typeof b !== 'string';
});
if (buffers.length <= 1) {
// for 0 length we will return empty uint8
// for 1 length we return the first uint8
return toUint8(buffers[0]);
}
var totalLen = buffers.reduce(function (total, buf, i) {
return total + (buf.byteLength || buf.length);
}, 0);
var tempBuffer = new Uint8Array(totalLen);
var offset = 0;
buffers.forEach(function (buf) {
buf = toUint8(buf);
tempBuffer.set(buf, offset);
offset += buf.byteLength;
});
return tempBuffer;
};
/**
* Check if the bytes "b" are contained within bytes "a".
*
* @param {Uint8Array|Array} a
* Bytes to check in
*
* @param {Uint8Array|Array} b
* Bytes to check for
*
* @param {Object} options
* options
*
* @param {Array|Uint8Array} [offset=0]
* offset to use when looking at bytes in a
*
* @param {Array|Uint8Array} [mask=[]]
* mask to use on bytes before comparison.
*
* @return {boolean}
* If all bytes in b are inside of a, taking into account
* bit masks.
*/
var bytesMatch = function bytesMatch(a, b, _temp3) {
var _ref3 = _temp3 === void 0 ? {} : _temp3,
_ref3$offset = _ref3.offset,
offset = _ref3$offset === void 0 ? 0 : _ref3$offset,
_ref3$mask = _ref3.mask,
mask = _ref3$mask === void 0 ? [] : _ref3$mask;
a = toUint8(a);
b = toUint8(b); // ie 11 does not support uint8 every
var fn = b.every ? b.every : Array.prototype.every;
return b.length && a.length - offset >= b.length && // ie 11 doesn't support every on uin8
fn.call(b, function (bByte, i) {
var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i];
return bByte === aByte;
});
};
var ID3 = toUint8([0x49, 0x44, 0x33]);
var getId3Size = function getId3Size(bytes, offset) {
if (offset === void 0) {
@ -40130,7 +40155,7 @@
};
var clock_1 = clock.ONE_SECOND_IN_TS;
/*! @name @videojs/http-streaming @version 2.14.0 @license Apache-2.0 */
/*! @name @videojs/http-streaming @version 2.14.2 @license Apache-2.0 */
/**
* @file resolve-url.js - Handling how URLs are resolved and manipulated
*/
@ -41964,7 +41989,7 @@
for (var _i2 = 0; _i2 < properties.playlists.length; _i2++) {
if (newMedia.id === properties.playlists[_i2].id) {
properties.playlists[_i2] = newMedia;
properties.playlists[_i2] = mergedPlaylist;
}
}
});
@ -42747,7 +42772,7 @@
Object.keys(message).forEach(function (key) {
var value = message[key];
if (ArrayBuffer.isView(value)) {
if (isArrayBufferView(value)) {
transferable[key] = {
bytes: value.buffer,
byteOffset: value.byteOffset,
@ -44241,7 +44266,7 @@
var getWorkerString = function getWorkerString(fn) {
return fn.toString().replace(/^function.+?{/, '').slice(0, -1);
};
/* rollup-plugin-worker-factory start for worker!/Users/poneill/dev/http-streaming/src/transmuxer-worker.js */
/* rollup-plugin-worker-factory start for worker!/Users/bclifford/Code/vhs-release-test/src/transmuxer-worker.js */
var workerCode$1 = transform(getWorkerString(function () {
@ -53047,7 +53072,7 @@
};
}));
var TransmuxWorker = factory(workerCode$1);
/* rollup-plugin-worker-factory end for worker!/Users/poneill/dev/http-streaming/src/transmuxer-worker.js */
/* rollup-plugin-worker-factory end for worker!/Users/bclifford/Code/vhs-release-test/src/transmuxer-worker.js */
var handleData_ = function handleData_(event, transmuxedData, callback) {
var _event$data$segment = event.data.segment,
@ -60834,10 +60859,12 @@
return TimelineChangeController;
}(videojs.EventTarget);
/* rollup-plugin-worker-factory start for worker!/Users/poneill/dev/http-streaming/src/decrypter-worker.js */
/* rollup-plugin-worker-factory start for worker!/Users/bclifford/Code/vhs-release-test/src/decrypter-worker.js */
var workerCode = transform(getWorkerString(function () {
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, basedir, module) {
return module = {
path: basedir,
@ -61030,7 +61057,7 @@
function unpad(padded) {
return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]);
}
/*! @name aes-decrypter @version 3.1.2 @license Apache-2.0 */
/*! @name aes-decrypter @version 3.1.3 @license Apache-2.0 */
/**
* @file aes.js
@ -61455,10 +61482,31 @@
}]);
return Decrypter;
}();
/**
* @file bin-utils.js
*/
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof commonjsGlobal !== "undefined") {
win = commonjsGlobal;
} else if (typeof self !== "undefined") {
win = self;
} else {
win = {};
}
var window_1 = win;
var isArrayBufferView = function isArrayBufferView(obj) {
if (ArrayBuffer.isView === 'function') {
return ArrayBuffer.isView(obj);
}
return obj && obj.buffer instanceof ArrayBuffer;
};
var BigInt = window_1.BigInt || Number;
[BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
/**
* Creates an object for sending to a web worker modifying properties that are TypedArrays
* into a new object with seperated properties for the buffer, byteOffset, and byteLength.
@ -61476,7 +61524,7 @@
Object.keys(message).forEach(function (key) {
var value = message[key];
if (ArrayBuffer.isView(value)) {
if (isArrayBufferView(value)) {
transferable[key] = {
bytes: value.buffer,
byteOffset: value.byteOffset,
@ -61514,7 +61562,7 @@
};
}));
var Decrypter = factory(workerCode);
/* rollup-plugin-worker-factory end for worker!/Users/poneill/dev/http-streaming/src/decrypter-worker.js */
/* rollup-plugin-worker-factory end for worker!/Users/bclifford/Code/vhs-release-test/src/decrypter-worker.js */
/**
* Convert the properties of an HLS track into an audioTrackKind.
@ -65350,11 +65398,11 @@
initPlugin(this, options);
};
var version$4 = "2.14.0";
var version$4 = "2.14.2";
var version$3 = "6.0.1";
var version$2 = "0.21.0";
var version$1 = "4.7.0";
var version = "3.1.2";
var version$2 = "0.21.1";
var version$1 = "4.7.1";
var version = "3.1.3";
var Vhs = {
PlaylistLoader: PlaylistLoader,
Playlist: Playlist,

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,14 @@
/*! @name videojs-license @version 0.1.0 @license MIT */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) :
typeof define === 'function' && define.amd ? define(['video.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojsLicense = factory(global.videojs));
})(this, (function (videojs) { 'use strict';
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js'), require('global/document')) :
typeof define === 'function' && define.amd ? define(['video.js', 'global/document'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojsLicense = factory(global.videojs, global.document));
}(this, (function (videojs, document) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
var document__default = /*#__PURE__*/_interopDefaultLegacy(document);
function createCommonjsModule(fn, basedir, module) {
return module = {
@ -32,7 +33,8 @@
return self;
}
module.exports = _assertThisInitialized, module.exports.__esModule = true, module.exports["default"] = module.exports;
module.exports = _assertThisInitialized;
module.exports["default"] = module.exports, module.exports.__esModule = true;
});
var setPrototypeOf = createCommonjsModule(function (module) {
@ -40,11 +42,14 @@
module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
}, module.exports.__esModule = true, module.exports["default"] = module.exports;
};
module.exports["default"] = module.exports, module.exports.__esModule = true;
return _setPrototypeOf(o, p);
}
module.exports = _setPrototypeOf, module.exports.__esModule = true, module.exports["default"] = module.exports;
module.exports = _setPrototypeOf;
module.exports["default"] = module.exports, module.exports.__esModule = true;
});
var inheritsLoose = createCommonjsModule(function (module) {
@ -54,14 +59,15 @@
setPrototypeOf(subClass, superClass);
}
module.exports = _inheritsLoose, module.exports.__esModule = true, module.exports["default"] = module.exports;
module.exports = _inheritsLoose;
module.exports["default"] = module.exports, module.exports.__esModule = true;
});
var version = "0.1.0";
var Plugin = videojs__default["default"].getPlugin('plugin');
var Component = videojs__default["default"].getComponent('Component');
var Button = videojs__default["default"].getComponent('MenuButton'); // Default options for the plugin.
var Plugin = videojs__default['default'].getPlugin('plugin');
var Component = videojs__default['default'].getComponent('Component');
var Button = videojs__default['default'].getComponent('MenuButton'); // Default options for the plugin.
var defaults = {
license: 'none',
@ -100,7 +106,7 @@
// the parent class will add player under this.player
_this = _Plugin.call(this, player) || this;
_this.playerId = _this.player.id();
_this.options = videojs__default["default"].mergeOptions(defaults, options);
_this.options = videojs__default['default'].mergeOptions(defaults, options);
if (options.license === 'none') {
return assertThisInitialized(_this);
@ -111,14 +117,14 @@
_this.buildUI();
if (videojs__default["default"].browser.IS_IOS || videojs__default["default"].browser.IS_ANDROID) {
if (videojs__default['default'].browser.IS_IOS || videojs__default['default'].browser.IS_ANDROID) {
_this.mobileBuildUI();
}
}); // close the menu if open on userinactive
_this.player.on('userinactive', function () {
document.getElementById(_this.playerId).querySelectorAll('.vjs-menu').forEach(function (element) {
document__default['default'].getElementById(_this.playerId).querySelectorAll('.vjs-menu').forEach(function (element) {
element.classList.remove('vjs-lock-open');
});
}); // close the menu if anywhere in the player is clicked
@ -126,7 +132,7 @@
_this.player.on('click', function (evt) {
if (evt.target.tagName === 'VIDEO') {
document.getElementById(_this.playerId).querySelectorAll('.vjs-menu').forEach(function (element) {
document__default['default'].getElementById(_this.playerId).querySelectorAll('.vjs-menu').forEach(function (element) {
element.classList.remove('vjs-lock-open');
});
}
@ -135,7 +141,7 @@
_this.player.on('loadstart', function (_event) {
_this.removeElementsByClass('vjs-license-clear');
if (videojs__default["default"].browser.IS_IOS || videojs__default["default"].browser.IS_ANDROID) {
if (videojs__default['default'].browser.IS_IOS || videojs__default['default'].browser.IS_ANDROID) {
_this.mobileBuildTopLevelMenu();
} else {
_this.buildTopLevelMenu();
@ -190,7 +196,7 @@
var _proto2 = LicenseMenuButton.prototype;
_proto2.handleClick = function handleClick() {
if (videojs__default["default"].browser.IS_IOS || videojs__default["default"].browser.IS_ANDROID) {
if (videojs__default['default'].browser.IS_IOS || videojs__default['default'].browser.IS_ANDROID) {
this.player.getChild('licenseMenuMobileModal').el().style.display = 'block';
} else {
this.el().classList.toggle('vjs-toogle-btn');
@ -201,7 +207,7 @@
return LicenseMenuButton;
}(Button);
videojs__default["default"].registerComponent('licenseMenuButton', LicenseMenuButton);
videojs__default['default'].registerComponent('licenseMenuButton', LicenseMenuButton);
this.player.getChild('controlBar').addChild('licenseMenuButton');
if (this.player.getChild('controlBar').getChild('fullscreenToggle')) {
@ -222,26 +228,26 @@
main.innerHTML = '';
main.classList.add('vjs-license-top-level'); // Start building new list items
var menuTitle = document.createElement('li');
var menuTitle = document__default['default'].createElement('li');
menuTitle.className = 'vjs-license-top-level-header';
var menuTitleInner = document.createElement('span');
var menuTitleInner = document__default['default'].createElement('span');
menuTitleInner.innerHTML = 'About';
menuTitleInner.className = 'vjs-license-top-level-header-titel';
menuTitle.appendChild(menuTitleInner);
main.appendChild(menuTitle);
var itemTitel = document.createElement('li');
var itemTitel = document__default['default'].createElement('li');
itemTitel.innerHTML = this.buildItemTitel();
itemTitel.className = 'vjs-license-top-level-item';
main.appendChild(itemTitel);
if (this.options.author) {
var itemAuthor = document.createElement('li');
var itemAuthor = document__default['default'].createElement('li');
itemAuthor.innerHTML = this.buildItemAuthor();
itemAuthor.className = 'vjs-license-top-level-item';
main.appendChild(itemAuthor);
}
var itemLicense = document.createElement('li');
var itemLicense = document__default['default'].createElement('li');
itemLicense.innerHTML = this.buildItemLicense();
itemLicense.className = 'vjs-license-top-level-item';
main.appendChild(itemLicense);
@ -277,7 +283,7 @@
var _proto3 = LicenseMenuMobileModal.prototype;
_proto3.createEl = function createEl() {
return videojs__default["default"].createEl('div', {
return videojs__default['default'].createEl('div', {
className: 'vjs-license-mobile'
});
};
@ -285,8 +291,8 @@
return LicenseMenuMobileModal;
}(Component);
videojs__default["default"].registerComponent('licenseMenuMobileModal', LicenseMenuMobileModal);
videojs__default["default"].dom.prependTo(this.player.addChild('licenseMenuMobileModal').el(), document.body);
videojs__default['default'].registerComponent('licenseMenuMobileModal', LicenseMenuMobileModal);
videojs__default['default'].dom.prependTo(this.player.addChild('licenseMenuMobileModal').el(), document__default['default'].body);
}
/**
* Add the menu ui button to the controlbar
@ -297,28 +303,28 @@
var _this3 = this;
var settingsButton = this.player.getChild('licenseMenuMobileModal');
var menuTopLevel = document.createElement('ul');
var menuTopLevel = document__default['default'].createElement('ul');
menuTopLevel.className = 'vjs-license-mob-top-level vjs-setting-menu-clear';
settingsButton.el().appendChild(menuTopLevel); // Empty the main menu div to repopulate
var menuTitle = document.createElement('li');
var menuTitle = document__default['default'].createElement('li');
menuTitle.innerHTML = 'About';
menuTitle.className = 'vjs-setting-menu-mobile-top-header';
menuTopLevel.appendChild(menuTitle);
var itemTitel = document.createElement('li');
var itemTitel = document__default['default'].createElement('li');
itemTitel.innerHTML = this.buildItemTitel();
itemTitel.className = 'vjs-license-top-level-item';
if (this.options.author) {
var itemAuthor = document.createElement('li');
var itemAuthor = document__default['default'].createElement('li');
itemAuthor.innerHTML = this.buildItemAuthor();
itemAuthor.className = 'vjs-license-top-level-item';
}
var itemLicense = document.createElement('li');
var itemLicense = document__default['default'].createElement('li');
itemLicense.innerHTML = this.buildItemLicense();
itemLicense.className = 'vjs-license-top-level-item';
var menuClose = document.createElement('li');
var menuClose = document__default['default'].createElement('li');
menuClose.innerHTML = 'Close';
menuClose.className = 'setting-menu-footer-default';
@ -342,7 +348,7 @@
titel = "" + this.options.title;
}
return 'Titel: ' + titel;
return 'Title: ' + titel;
}
/**
* Add the menu ui button to the controlbar
@ -428,10 +434,10 @@
_proto.removeElementsByClass = function removeElementsByClass(className) {
// Need to prevent the menu from not showing sometimes
document.querySelectorAll('.vjs-sm-top-level').forEach(function (element) {
document__default['default'].querySelectorAll('.vjs-sm-top-level').forEach(function (element) {
element.classList.remove('vjs-hidden');
});
var elements = document.getElementsByClassName(className);
var elements = document__default['default'].getElementsByClassName(className);
while (elements.length > 0) {
elements[0].parentNode.removeChild(elements[0]);
@ -446,8 +452,8 @@
License.VERSION = version; // Register the plugin with video.js.
videojs__default["default"].registerPlugin('license', License);
videojs__default['default'].registerPlugin('license', License);
return License;
}));
})));

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,7 @@
{{#if channel_creator_name}}
<meta name="author" content="{{channel_creator_name}}">
{{/if}}
<meta property="og:site_name" content="{{titel}}">
<meta property="og:site_name" content="{{title}}">
<meta property="og:url" content="{{url}}">
<meta property="og:title" content="{{channel_name}}">
<meta property="og:image" content="{{channel_poster}}">
@ -99,7 +99,7 @@
background-color: {{bgcolor_header}};
}
.header-title {
color: {{textcolor_titel}};
color: {{textcolor_title}};
font-size: 1.4rem;
font-weight: 400;
padding-left: .5em;
@ -181,7 +181,7 @@
}
}
.content-headline {
color: {{textcolor_titel}};
color: {{textcolor_title}};
font-size: 1.5rem;
font-weight: 500;
}
@ -195,13 +195,13 @@
}
}
.channel-stats {
color: {{textcolor_titel}};
color: {{textcolor_title}};
font-weight: 500;
padding-top: 0.6em;
padding-bottom: 0.8em;
}
.channel-stats-icon {
color: {{textcolor_titel}};
color: {{textcolor_title}};
font-size: 1.3rem !important;
height: 1em;
margin-right: 0.5em;
@ -344,14 +344,14 @@
display: flex;
}
.close {
color: {{textcolor_titel}};
color: {{textcolor_title}};
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: {{textcolor_titel}};
color: {{textcolor_title}};
text-decoration: none;
cursor: pointer;
}
@ -364,7 +364,7 @@
<body>
<div class="row">
<div class="col-xs-12 middle-xs header">
<h1 class="header-title">{{titel}}</h1>
<h1 class="header-title">{{title}}</h1>
</div>
</div>
<div class="row max-width">
@ -397,7 +397,9 @@
<span id="channel_stats_runtime_{{channel_id}}"></span> <span class="material-icons inline-icon channel-stats-icon">live_tv</span>
</div>
<div class="col-xs-3 middle-xs right-align">
{{#if share}}
<a id="btn-share" href="#"><span class="material-icons inline-icon">share</span> SHARE</a>
{{/if}}
</div>
</div>
<hr>
@ -411,8 +413,10 @@
<strong>Creator:</strong> <a id="btn-creator" href="#">{{channel_creator_name}}</a><br />
{{/if}}
{{#if channel_license}}
{{#ifnoteq channel_license "none"}}
<strong>Content license:</strong><br />
<a id="license" target="_blank" rel="noopener"><img id="license_image" class="channel-license-image" height="40" width="114" /></a>
{{/ifnoteq}}
{{/if}}
</p>
</div>

View File

@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Restreamer Video-Streaming" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="manifest.json" />
<title>Restreamer</title>
</head>
<body>

View File

@ -132,7 +132,7 @@ function Resources(props) {
const core = $resources.core;
return (
<Stack className="footerRight" direction="row" alignItems="center" spacing={0} >
<Stack className="footerRight" direction="row" alignItems="center" spacing={0}>
{(system.cpu_used >= 75 || system.mem_used >= 75 || core.memfs_used >= 75 || core.disk_used >= 75 || core.net_used >= 75) && (
<WarningIcon className={classes.warningIcon} color="service" />
)}

View File

@ -182,10 +182,7 @@ function AboutModal(props) {
<Grid item xs={12}></Grid>
<Grid item xs={12}>
<Typography>
<strong>Release</strong>:{' '}
<Link color="secondary" target="_blank" href={'https://github.com/datarhei/restreamer/releases/tag/v' + Version.UI}>
v{Version.UI}
</Link>
<strong>Release</strong>: {Version.UI}
</Typography>
<Typography>
<strong>Repo</strong>:{' '}

View File

@ -10,6 +10,7 @@ import { messages as FR } from './locales/fr/messages.js';
import { messages as IT } from './locales/it/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 });
@ -18,6 +19,7 @@ i18n.loadLocaleData('fr', { plurals: plurals.fr });
i18n.loadLocaleData('it', { plurals: plurals.it });
i18n.loadLocaleData('pt', { plurals: plurals.pt });
i18n.loadLocaleData('es', { plurals: plurals.es });
i18n.loadLocaleData('ru', { plurals: plurals.ru });
i18n.load({
en: EN,
de: DE,
@ -25,6 +27,7 @@ i18n.load({
it: IT,
pt: PT,
es: ES,
ru: RU,
});
const getLanguage = (defaultLanguage, supportedLanguages) => {
@ -53,7 +56,7 @@ const getBrowserLanguage = (defaultLanguage) => {
return match[0].toLowerCase();
};
i18n.activate(getLanguage('en', ['en', 'de', 'fr', 'it', 'pt', 'es']));
i18n.activate(getLanguage('en', ['en', 'de', 'fr', 'it', 'pt', 'es', 'ru']));
export default function Provider(props) {
return <I18nProvider i18n={i18n}>{props.children}</I18nProvider>;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2670
src/locales/ru/messages.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,9 @@ export default function Component(props) {
justifyContent="center"
alignItems="center"
spacing={1}
className={props.color === 'dark' ? classes.dark : props.color === 'success' ? classes.success : props.color === 'danger' ? classes.danger : classes.light}
className={
props.color === 'dark' ? classes.dark : props.color === 'success' ? classes.success : props.color === 'danger' ? classes.danger : classes.light
}
{...props}
>
{props.children}
@ -59,5 +61,5 @@ export default function Component(props) {
}
Component.defaultProps = {
color: 'light'
color: 'light',
};

View File

@ -55,7 +55,7 @@ const useStyles = makeStyles((theme) => ({
marginRight: '-1.5em!important',
paddingBottom: '1em',
},
imageTitel: {
imageTitle: {
textAlign: 'initial',
padding: '.5em 0em 0em .1em',
},
@ -169,7 +169,7 @@ function ChannelButton(props) {
<ImageSrc style={{ backgroundImage: `url(${props.url})`, borderColor: color_active }} />
<ImageBackdrop className="MuiImageBackdrop-root" style={{ borderColor: color_active }} />
</Image>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" className={classes.imageTitel}>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" className={classes.imageTitle}>
<Typography variant="body2" color="inherit">
{props.title}
</Typography>

35
src/misc/CopyButton.js Normal file
View File

@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import { useLingui } from '@lingui/react';
import { t } from '@lingui/macro';
import Button from '@mui/material/Button';
import FileCopyIcon from '@mui/icons-material/FileCopy';
import CopyToClipboard from '../utils/clipboard';
import NotifyContext from '../contexts/Notify';
export default function CopyButton(props) {
const notify = useContext(NotifyContext);
const { i18n } = useLingui();
const { children, value, ...other } = props;
const handleCopy = async () => {
const success = await CopyToClipboard(value);
if (success === true) {
notify.Dispatch('success', 'clipboard', i18n._(t`Data copied to clipboard`));
} else {
notify.Dispatch('error', 'clipboard', i18n._(t`Error while copying data to clipboard`));
}
};
return (
<Button {...other} endIcon={<FileCopyIcon />} onClick={handleCopy}>
{children}
</Button>
);
}
CopyButton.defaultProps = {
value: '',
};

View File

@ -5,10 +5,12 @@ import { Trans, t } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import * as Encoders from './coders/Encoders';
import * as Decoders from './coders/Decoders';
import Select from './Select';
import H from '../utils/help';
export default function EncodingSelect(props) {
const { i18n } = useLingui();
@ -75,6 +77,11 @@ export default function EncodingSelect(props) {
props.onChange(encoder, profile.decoder, automatic);
};
const handleEncoderHelp = (topic) => (event) => {
event.preventDefault();
H('encoder-' + topic);
};
let stream = null;
if (profile.stream >= 0) {
@ -106,12 +113,17 @@ export default function EncodingSelect(props) {
}
let encoderSettings = null;
let encoderSettingsHelp = null;
let coder = encoderRegistry.Get(profile.encoder.coder);
if (coder !== null && props.availableEncoders.includes(coder.coder)) {
const Settings = coder.component;
encoderSettings = <Settings stream={stream} settings={profile.encoder.settings} onChange={handleEncoderSettingsChange} />;
if (props.type === 'video' && !['copy', 'none', 'rawvideo'].includes(coder.coder)) {
encoderSettingsHelp = handleEncoderHelp(coder.coder);
}
}
let encoderList = [];
@ -207,6 +219,15 @@ export default function EncodingSelect(props) {
<Grid item xs={12}>
{encoderSettings}
</Grid>
{encoderSettingsHelp !== null && (
<Grid item xs={12}>
<Trans>
<Link color="secondary" href="#" onClick={encoderSettingsHelp}>
Compatibility list
</Link>
</Trans>
</Grid>
)}
</Grid>
);
}

View File

@ -14,14 +14,7 @@ export default function Component(props) {
const classes = useStyles();
return (
<Button
variant="outlined"
size="large"
fullWidth
color="primary"
className={classes.button}
{...props}
>
<Button variant="outlined" size="large" fullWidth color="primary" className={classes.button} {...props}>
{props.children}
</Button>
);

View File

@ -48,6 +48,7 @@ export default function LanguageSelect(props) {
<MenuItem value="it">Italiano </MenuItem>
<MenuItem value="pt">Português </MenuItem>
<MenuItem value="es">Español </MenuItem>
<MenuItem value="ru">Русский </MenuItem>
</Select>
);
}

View File

@ -53,19 +53,9 @@ const Component = React.forwardRef((props, ref) => {
<Paper className={classes.modalPaper} elevation={0} tabIndex={-1} ref={ref} {...other}>
<Grid container spacing={0}>
<Grid item xs={12} className={classes.modalHeader}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Typography variant="button">{props.title}</Typography>
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={2}>
{typeof props.onHelp === 'function' && (
<IconButton color="inherit" size="small" onClick={props.onHelp}>
<HelpIcon fontSize="small" />

View File

@ -57,7 +57,7 @@ Component.defaultProps = {
spacing: 0,
padding: null,
title: '',
variant: 'pagetitel',
variant: 'pagetitle',
onAbort: null,
onHelp: null,
onEdit: null,

View File

@ -13,11 +13,11 @@ const useStyles = makeStyles((theme) => ({
export default function Component(props) {
const classes = useStyles();
return <CardMedia className={classes.media} style={{height: props.height}} image={props.image} title={props.title} />;
return <CardMedia className={classes.media} style={{ height: props.height }} image={props.image} title={props.title} />;
}
Component.defaultProps = {
image: '',
title: '',
height: '0px'
height: '0px',
};

View File

@ -54,9 +54,7 @@ export default function Password(props) {
label={props.label}
autoComplete={props.autoComplete}
/>
{props.helperText && (
<FormHelperText>{props.helperText}</FormHelperText>
)}
{props.helperText && <FormHelperText>{props.helperText}</FormHelperText>}
</FormControl>
);
}

View File

@ -1,5 +1,5 @@
.vjs-internal {
--video-js--primary: #EAEA05;
--video-js--primary: #eaea05;
}
/* play-button */
@ -13,13 +13,13 @@
top: 50%;
left: 50%;
margin-top: -90px;
color: rgba(255,255,255,.65);
color: rgba(255, 255, 255, 0.65);
}
.vjs-internal:hover .vjs-big-play-button,
.vjs-internal.vjs-big-play-button:focus {
background-color: transparent;
color: rgba(255,255,255,1);
color: rgba(255, 255, 255, 1);
}
/* controlbar */
@ -33,14 +33,14 @@
border-bottom-right-radius: 4px;
}
.vjs-internal .vjs-button>.vjs-icon-placeholder:before {
line-height: 50px
.vjs-internal .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
/* progressbar */
.vjs-internal .vjs-play-progress:before {
display: none
display: none;
}
.vjs-internal .vjs-progress-control {
@ -49,7 +49,8 @@
/* buttons */
.vjs-internal .vjs-playing, .vjs-internal .vjs-paused {
.vjs-internal .vjs-playing,
.vjs-internal .vjs-paused {
width: 50px;
height: 50px;
border-radius: 4px;
@ -67,14 +68,24 @@
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
transition: visibility 1s,opacity 1s,height 1s 1s,width 1s,right 1s 1s,top 1s 1s;
transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, right 1s 1s,
top 1s 1s;
}
.vjs-internal .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control, .vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control, .vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active, .vjs-internal .vjs-volume-panel .vjs-volume-control:active, .vjs-internal .vjs-volume-panel:active .vjs-volume-control, .vjs-internal .vjs-volume-panel:focus .vjs-volume-control {
.vjs-internal
.vjs-volume-panel.vjs-hover
.vjs-mute-control
~ .vjs-volume-control,
.vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control,
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active,
.vjs-internal .vjs-volume-panel .vjs-volume-control:active,
.vjs-internal .vjs-volume-panel:active .vjs-volume-control,
.vjs-internal .vjs-volume-panel:focus .vjs-volume-control {
visibility: visible;
opacity: 1;
position: relative;
transition: visibility .1s,opacity .1s,height .1s,width .1s,right 0s,top 0s;
transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, right 0s,
top 0s;
}
.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before {

View File

@ -1 +1,98 @@
.vjs-internal{--video-js--primary:#EAEA05}.vjs-internal .vjs-big-play-button{width:70px;height:70px;background:0 0;line-height:180px;font-size:180px;border:none;top:50%;left:50%;margin-top:-90px;color:rgba(255,255,255,.65)}.vjs-internal.vjs-big-play-button:focus,.vjs-internal:hover .vjs-big-play-button{background-color:transparent;color:rgba(255,255,255,1)}.vjs-internal .vjs-control-bar{top:calc(50% - 45px);height:100px;width:50px;background-color:#4d4d4d;border-top-right-radius:4px;border-bottom-right-radius:4px}.vjs-internal .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-internal .vjs-play-progress:before{display:none}.vjs-internal .vjs-progress-control{display:none}.vjs-internal .vjs-paused,.vjs-internal .vjs-playing{width:50px;height:50px;border-radius:4px;background-color:#4d4d4d}.vjs-internal .vjs-volume-panel{width:50px;height:50px;border-radius:4px;padding-left:4px;background-color:#4d4d4d;margin:50px 0 0 -50px}.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:1em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,right 1s 1s,top 1s 1s}.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.vjs-internal .vjs-volume-panel .vjs-volume-control:active,.vjs-internal .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control,.vjs-internal .vjs-volume-panel:active .vjs-volume-control,.vjs-internal .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,right 0s,top 0s}.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before{top:-.4em;right:-.5em}.vjs-internal .vjs-remaining-time{display:none}.vjs-internal .vjs-live-display{display:none}.vjs-internal .vjs-picture-in-picture-control{display:none}.vjs-internal .vjs-fullscreen-control{display:none}.vjs-internal .vjs-seek-to-live-control{display:none}.vjs-internal .vjs-subs-caps-button{display:none}.vjs-internal .vjs-custom-control-spacer{display:block;width:100%}
.vjs-internal {
--video-js--primary: #eaea05;
}
.vjs-internal .vjs-big-play-button {
width: 70px;
height: 70px;
background: 0 0;
line-height: 180px;
font-size: 180px;
border: none;
top: 50%;
left: 50%;
margin-top: -90px;
color: rgba(255, 255, 255, 0.65);
}
.vjs-internal.vjs-big-play-button:focus,
.vjs-internal:hover .vjs-big-play-button {
background-color: transparent;
color: rgba(255, 255, 255, 1);
}
.vjs-internal .vjs-control-bar {
top: calc(50% - 45px);
height: 100px;
width: 50px;
background-color: #4d4d4d;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.vjs-internal .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
.vjs-internal .vjs-play-progress:before {
display: none;
}
.vjs-internal .vjs-progress-control {
display: none;
}
.vjs-internal .vjs-paused,
.vjs-internal .vjs-playing {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #4d4d4d;
}
.vjs-internal .vjs-volume-panel {
width: 50px;
height: 50px;
border-radius: 4px;
padding-left: 4px;
background-color: #4d4d4d;
margin: 50px 0 0 -50px;
}
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, right 1s 1s,
top 1s 1s;
}
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active,
.vjs-internal .vjs-volume-panel .vjs-volume-control:active,
.vjs-internal
.vjs-volume-panel.vjs-hover
.vjs-mute-control
~ .vjs-volume-control,
.vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control,
.vjs-internal .vjs-volume-panel:active .vjs-volume-control,
.vjs-internal .vjs-volume-panel:focus .vjs-volume-control {
visibility: visible;
opacity: 1;
position: relative;
transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, right 0s,
top 0s;
}
.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before {
top: -0.4em;
right: -0.5em;
}
.vjs-internal .vjs-remaining-time {
display: none;
}
.vjs-internal .vjs-live-display {
display: none;
}
.vjs-internal .vjs-picture-in-picture-control {
display: none;
}
.vjs-internal .vjs-fullscreen-control {
display: none;
}
.vjs-internal .vjs-seek-to-live-control {
display: none;
}
.vjs-internal .vjs-subs-caps-button {
display: none;
}
.vjs-internal .vjs-custom-control-spacer {
display: block;
width: 100%;
}

View File

@ -1,5 +1,5 @@
.vjs-public {
--video-js--primary: #EAEA05;
--video-js--primary: #eaea05;
}
/* play btn */
@ -15,14 +15,14 @@
left: 50%;
margin-top: -90px;
margin-left: -90px;
color: rgba(255,255,255,.65);
}
color: rgba(255, 255, 255, 0.65);
}
.vjs-public:hover .vjs-big-play-button,
.vjs-public.vjs-big-play-button:focus {
.vjs-public:hover .vjs-big-play-button,
.vjs-public.vjs-big-play-button:focus {
background-color: transparent;
color: rgba(255,255,255,1);
}
color: rgba(255, 255, 255, 1);
}
/* controlbar */
@ -30,21 +30,21 @@
height: 70px;
padding-top: 20px;
background: none;
background-image: linear-gradient(0deg, rgba(0,0,0,.85), transparent)
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.85), transparent);
}
.vjs-public .vjs-time-tooltip {
z-index: 0;
}
.vjs-public .vjs-button>.vjs-icon-placeholder:before {
line-height: 50px
.vjs-public .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
/* progressbar */
.vjs-public .vjs-play-progress:before {
display: none
display: none;
}
.vjs-public .vjs-progress-control {
@ -53,7 +53,7 @@
right: 0;
left: 15px;
width: calc(100% - 30px);
height: 20px
height: 20px;
}
.vjs-public .vjs-progress-control .vjs-progress-holder {
@ -62,7 +62,7 @@
right: 0;
left: 0;
width: 100%;
margin: 0
margin: 0;
}
.vjs-public .vjs-play-progress {
@ -70,15 +70,15 @@
}
.vjs-public .vjs-slider {
background: rgba(255,255,255,.25);
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress {
background: rgba(255,255,255,.25);
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress div {
background: rgba(255,255,255,.25);
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-remaining-time {
@ -128,43 +128,42 @@
}
.vjs-public .vjs-overlay-no-background {
max-width: 28%!important;
max-height: 28%!important;
max-width: 28% !important;
max-height: 28% !important;
}
.vjs-public .vjs-overlay-top-left {
top: 20px!important;
left: 30px!important;
top: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-top-right {
top: 20px!important;
right: 30px!important;
top: 20px !important;
right: 30px !important;
}
.vjs-public .vjs-overlay-bottom-left {
bottom: 20px!important;
left: 30px!important;
bottom: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-bottom-right {
bottom: 20px!important;
right: 30px!important;
bottom: 20px !important;
right: 30px !important;
}
/* context menu */
.vjs-public .vjs-license .vjs-menu .vjs-menu-content {
background: rgba(0,0,0,.8);
background: rgba(0, 0, 0, 0.8);
}
.vjs-public .vjs-license-top-level-header {
background: unset!important;
border-bottom: 1px solid rgba(255,255,255,.25);
background: unset !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
min-width: 100px;
}
.vjs-public .vjs-lock-open {
z-index: 1000;
}

View File

@ -1 +1,123 @@
.vjs-public{--video-js--primary:#EAEA05}.vjs-public .vjs-big-play-button{width:70px;height:70px;background:0 0;line-height:180px;font-size:180px;border:none;top:50%;left:50%;margin-top:-90px;margin-left:-90px;color:rgba(255,255,255,.65)}.vjs-public.vjs-big-play-button:focus,.vjs-public:hover .vjs-big-play-button{background-color:transparent;color:rgba(255,255,255,1)}.vjs-public .vjs-control-bar{height:70px;padding-top:20px;background:0 0;background-image:linear-gradient(0deg,rgba(0,0,0,.85),transparent)}.vjs-public .vjs-time-tooltip{z-index:0}.vjs-public .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-public .vjs-play-progress:before{display:none}.vjs-public .vjs-progress-control{position:absolute;top:0;right:0;left:15px;width:calc(100% - 30px);height:20px}.vjs-public .vjs-progress-control .vjs-progress-holder{position:absolute;top:20px;right:0;left:0;width:100%;margin:0}.vjs-public .vjs-play-progress{background-color:var(--video-js--primary)}.vjs-public .vjs-slider{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress div{background:rgba(255,255,255,.25)}.vjs-public .vjs-remaining-time{order:0;line-height:50px;flex:3;text-align:left}.vjs-public .vjs-live-control{line-height:50px}.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:1em}.vjs-public .vjs-control .vjs-volume-panel{width:4.5em}.vjs-public .vjs-live-display{margin-left:1.8em}.vjs-internal .vjs-subs-caps-button{display:none}.vjs-public .vjs-custom-control-spacer{display:block;width:100%}.vjs-public .vjs-overlay>a>img{width:100%}.vjs-public .vjs-overlay-no-background{max-width:28%!important;max-height:28%!important}.vjs-public .vjs-overlay-top-left{top:20px!important;left:30px!important}.vjs-public .vjs-overlay-top-right{top:20px!important;right:30px!important}.vjs-public .vjs-overlay-bottom-left{bottom:20px!important;left:30px!important}.vjs-public .vjs-overlay-bottom-right{bottom:20px!important;right:30px!important}.vjs-public .vjs-license .vjs-menu .vjs-menu-content{background:rgba(0,0,0,.8)}.vjs-public .vjs-license-top-level-header{background:unset!important;border-bottom:1px solid rgba(255,255,255,.25);min-width:100px}.vjs-public .vjs-lock-open{z-index:1000}
.vjs-public {
--video-js--primary: #eaea05;
}
.vjs-public .vjs-big-play-button {
width: 70px;
height: 70px;
background: 0 0;
line-height: 180px;
font-size: 180px;
border: none;
top: 50%;
left: 50%;
margin-top: -90px;
margin-left: -90px;
color: rgba(255, 255, 255, 0.65);
}
.vjs-public.vjs-big-play-button:focus,
.vjs-public:hover .vjs-big-play-button {
background-color: transparent;
color: rgba(255, 255, 255, 1);
}
.vjs-public .vjs-control-bar {
height: 70px;
padding-top: 20px;
background: 0 0;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.85), transparent);
}
.vjs-public .vjs-time-tooltip {
z-index: 0;
}
.vjs-public .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
.vjs-public .vjs-play-progress:before {
display: none;
}
.vjs-public .vjs-progress-control {
position: absolute;
top: 0;
right: 0;
left: 15px;
width: calc(100% - 30px);
height: 20px;
}
.vjs-public .vjs-progress-control .vjs-progress-holder {
position: absolute;
top: 20px;
right: 0;
left: 0;
width: 100%;
margin: 0;
}
.vjs-public .vjs-play-progress {
background-color: var(--video-js--primary);
}
.vjs-public .vjs-slider {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress div {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-remaining-time {
order: 0;
line-height: 50px;
flex: 3;
text-align: left;
}
.vjs-public .vjs-live-control {
line-height: 50px;
}
.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
}
.vjs-public .vjs-control .vjs-volume-panel {
width: 4.5em;
}
.vjs-public .vjs-live-display {
margin-left: 1.8em;
}
.vjs-internal .vjs-subs-caps-button {
display: none;
}
.vjs-public .vjs-custom-control-spacer {
display: block;
width: 100%;
}
.vjs-public .vjs-overlay > a > img {
width: 100%;
}
.vjs-public .vjs-overlay-no-background {
max-width: 28% !important;
max-height: 28% !important;
}
.vjs-public .vjs-overlay-top-left {
top: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-top-right {
top: 20px !important;
right: 30px !important;
}
.vjs-public .vjs-overlay-bottom-left {
bottom: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-bottom-right {
bottom: 20px !important;
right: 30px !important;
}
.vjs-public .vjs-license .vjs-menu .vjs-menu-content {
background: rgba(0, 0, 0, 0.8);
}
.vjs-public .vjs-license-top-level-header {
background: unset !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
min-width: 100px;
}
.vjs-public .vjs-lock-open {
z-index: 1000;
}

View File

@ -4,14 +4,14 @@ import makeStyles from '@mui/styles/makeStyles';
import Box from '@mui/material/Box';
import PropTypes from 'prop-types';
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
padding: 0,
},
'& .MuiBox-root': {
padding: 0,
},
}));
}));
export default function TabPanel(props) {
const classes = useStyles();
@ -19,7 +19,11 @@ export default function TabPanel(props) {
return (
<div className={classes.root} role="tabpanel" hidden={value !== index} id={`vertical-tabpanel-${index}`} {...other}>
{value === index && <Box classes={{ root: classes }} p={0}>{children}</Box>}
{value === index && (
<Box classes={{ root: classes }} p={0}>
{children}
</Box>
)}
</div>
);
}

View File

@ -34,9 +34,7 @@ export default function Component(props) {
rows={props.rows}
type={props.type}
/>
{props.helperText && (
<FormHelperText>{props.helperText}</FormHelperText>
)}
{props.helperText && <FormHelperText>{props.helperText}</FormHelperText>}
</FormControl>
);
}

View File

@ -10,6 +10,8 @@ import InputLabel from '@mui/material/InputLabel';
import NotifyContext from '../contexts/Notify';
import OutlinedInput from '@mui/material/OutlinedInput';
import CopyToClipboard from '../utils/clipboard';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiOutlinedInput-notchedOutline': {
@ -23,22 +25,9 @@ export default function Component(props) {
const { i18n } = useLingui();
const notify = useContext(NotifyContext);
const textAreaRef = React.createRef();
const handleCopy = async () => {
let success = false;
if (!navigator.clipboard) {
textAreaRef.current.select();
try {
success = document.execCommand('copy');
} catch (err) {}
textAreaRef.current.setSelectionRange(0, 0);
} else {
success = await writeText(navigator.clipboard.writeText(props.value));
}
const success = await CopyToClipboard(props.value);
if (success === true) {
notify.Dispatch('success', 'clipboard', i18n._(t`Data copied to clipboard`));
@ -47,21 +36,11 @@ export default function Component(props) {
}
};
const writeText = (promise) => {
return promise
.then(() => true)
.catch((err) => {
console.warn(err);
return false;
});
};
return (
<FormControl variant="outlined" disabled={props.disabled} fullWidth>
<InputLabel htmlFor={props.id}>{props.label}</InputLabel>
<OutlinedInput
className={classes.root}
inputRef={textAreaRef}
id={props.id}
value={props.value}
label={props.label}

View File

@ -99,7 +99,7 @@ export default function Component(props) {
}
let textAreaDivStyle = {
width: '100%'
width: '100%',
};
let textAreaStyle = {
lineHeight: 1.3,
@ -116,7 +116,7 @@ export default function Component(props) {
if (props.rows === 1) {
textAreaStyle = {
...textAreaStyle,
height: ((18 * props.rows) + 9.5) + 'px',
height: 18 * props.rows + 9.5 + 'px',
overflowY: 'hidden',
marginBottom: '0em',
marginTop: '0em',
@ -134,7 +134,7 @@ export default function Component(props) {
} else {
textAreaStyle = {
...textAreaStyle,
height: ((18 * props.rows) + 8) + 'px',
height: 18 * props.rows + 8 + 'px',
};
textAreaDivStyle = {
...textAreaDivStyle,
@ -149,20 +149,8 @@ export default function Component(props) {
return (
<React.Fragment>
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={1}
style={textAreaDivStyle}
>
<Stack
direction="column"
justifyContent="flex-start"
alignItems="flex-end"
spacing={0}
width="100%"
>
<Stack direction="column" justifyContent="center" alignItems="flex-start" spacing={1} style={textAreaDivStyle}>
<Stack direction="column" justifyContent="flex-start" alignItems="flex-end" spacing={0} width="100%">
{allowCopy && (
<IconButton size="small" onClick={handleCopy} style={actionButton}>
<FileCopyIcon fontSize="small" />

View File

@ -13,6 +13,8 @@ import * as X265 from './video/X265';
import * as H264VideoToolbox from './video/H264VideoToolbox';
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 VideoCopy from './video/Copy';
import * as VideoNone from './video/None';
import * as VideoRaw from './video/Raw';
@ -123,6 +125,8 @@ videoRegistry.Register(X265);
videoRegistry.Register(H264VideoToolbox);
videoRegistry.Register(H264NVENC);
videoRegistry.Register(H264OMX);
videoRegistry.Register(H264V4L2M2M);
videoRegistry.Register(H264VAAPI);
videoRegistry.Register(VP9);
videoRegistry.Register(VideoRaw);

View File

@ -0,0 +1,163 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import Video from '../../settings/Video';
function init(initialState) {
const state = {
bitrate: '4096',
fps: '25',
gop: '2',
profile: 'auto',
...initialState,
};
return state;
}
// https://forums.raspberrypi.com/viewtopic.php?t=294161
// ffmpeg -y -nostdin -f v4l2 -threads auto -input_format yuyv422 -fflags +genpts -flags +global_header \
// -i /dev/video0 -s 1280x720 -r 25 \
// -vcodec h264_v4l2m2m \
// -num_output_buffers 32 -num_capture_buffers 16 \
// -keyint_min 25 -force_key_frames "expr:gte(t,n_forced*1)" \
// -g 50 -b:v 6M -pix_fmt nv12 \
// -f mp4 -f segment -segment_format_options movflags=+faststart -segment_time 1 -reset_timestamps 1 -segment_time 1 -segment_time_delta 1 -segment_format mp4 -muxdelay 0 -muxpreload 0 /home/pi/Videos/%d-output.mp4
/**
v4l2-ctl --list-ctrls-menu -d 11
Codec Controls
video_bitrate_mode 0x009909ce (menu) : min=0 max=1 default=0 value=0 (Variable Bitrate) flags=update
0: Variable Bitrate
1: Constant Bitrate
video_bitrate 0x009909cf (int) : min=25000 max=25000000 step=25000 default=10000000 value=10000000
sequence_header_mode 0x009909d8 (menu) : min=0 max=1 default=1 value=1 (Joined With 1st Frame)
0: Separate Buffer
1: Joined With 1st Frame
repeat_sequence_header 0x009909e2 (bool) : default=0 value=0
force_key_frame 0x009909e5 (button) : value=0 flags=write-only, execute-on-write
h264_minimum_qp_value 0x00990a61 (int) : min=0 max=51 step=1 default=20 value=20
h264_maximum_qp_value 0x00990a62 (int) : min=0 max=51 step=1 default=51 value=51
h264_i_frame_period 0x00990a66 (int) : min=0 max=2147483647 step=1 default=60 value=60
h264_level 0x00990a67 (menu) : min=0 max=13 default=11 value=11 (4)
0: 1
1: 1b
2: 1.1
3: 1.2
4: 1.3
5: 2
6: 2.1
7: 2.2
8: 3
9: 3.1
10: 3.2
11: 4
12: 4.1
13: 4.2
h264_profile 0x00990a6b (menu) : min=0 max=4 default=4 value=4 (High)
0: Baseline
1: Constrained Baseline
2: Main
4: High
*/
function createMapping(settings) {
const mapping = [
'-codec:v',
'h264_v4l2m2m',
'-b:v',
`${settings.bitrate}k`,
'-maxrate',
`${settings.bitrate}k`,
'-bufsize',
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-pix_fmt',
'yuv420p',
'-vsync',
'1',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
if (settings.profile !== 'auto') {
mapping.push('-profile:v', `${settings.profile}`);
}
return mapping;
}
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}>
<Video.Bitrate value={settings.bitrate} onChange={update('bitrate')} allowCustom />
</Grid>
<Grid item xs={12}>
<Video.Framerate value={settings.fps} onChange={update('fps')} allowCustom />
</Grid>
<Grid item xs={12}>
<Video.GOP value={settings.gop} onChange={update('gop')} allowAuto allowCustom />
</Grid>
</Grid>
);
}
Coder.defaultProps = {
stream: {},
settings: {},
onChange: function (settings, mapping) {},
};
const coder = 'h264_v4l2m2m';
const name = 'H.264 (V4L2 Memory to Memory)';
const codec = 'h264';
const type = 'video';
const hwaccel = true;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
};
}
export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as component };

View File

@ -0,0 +1,171 @@
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 mapping = [
'-vaapi_device',
'/dev/dri/renderD128',
'-vf',
'format=nv12,hwupload',
'-codec:v',
'h264_vaapi',
'-profile:v',
`${settings.profile}`,
'-quality',
`${settings.quality}`,
'-level',
`${settings.level}`,
'-b:v',
`${settings.bitrate}k`,
'-maxrate',
`${settings.bitrate}k`,
'-bufsize',
`${settings.bitrate}k`,
'-r',
`${settings.fps}`,
'-g',
`${settings.gop}`,
'-vsync',
'1',
];
if (settings.gop !== 'auto') {
mapping.push('-g', `${Math.round(parseInt(settings.fps) * parseInt(settings.gop)).toFixed(0)}`);
}
return mapping;
}
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 = 'h264_vaapi';
const name = 'H.264 (Intel VAAPI)';
const codec = 'h264';
const type = 'video';
const hwaccel = true;
function summarize(settings) {
return `${name}, ${settings.bitrate} kbit/s, ${settings.fps} FPS, Profile: ${settings.profile}`;
}
function defaults() {
const settings = init({});
return {
settings: settings,
mapping: createMapping(settings),
};
}
export { coder, name, codec, type, hwaccel, summarize, defaults, Coder as component };

View File

@ -5,11 +5,14 @@ import Grid from '@mui/material/Grid';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Checkbox from '../Checkbox';
function init(settings) {
const initSettings = {
lhls: false,
segmentDuration: 2,
listSize: 6,
cleanup: true,
...settings,
};
@ -28,7 +31,7 @@ export default function Control(props) {
const handleChange = (what) => (event) => {
const value = event.target.value;
if (what === 'lhls') {
if (['lhls', 'cleanup'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
@ -76,6 +79,9 @@ 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>Automatic cleanup of all media data</Trans>} checked={settings.cleanup} onChange={handleChange('cleanup')} />
</Grid>
</Grid>
);
}

View File

@ -66,10 +66,7 @@ export default function Control(props) {
return (
<Grid container spacing={0}>
<Grid item xs={12}>
<TabsHorizontal
value={$tab}
onChange={handleChangeTab}
>
<TabsHorizontal value={$tab} onChange={handleChangeTab}>
<Tab className="tab" label={<Trans>Content</Trans>} value="content" />
<Tab className="tab" label={<Trans>Author</Trans>} value="author" />
</TabsHorizontal>

View File

@ -31,7 +31,7 @@ export default function Control(props) {
const handleChange = (what) => (event) => {
const value = event.target.value;
if (['autostart', 'reconnect'].includes(what)) {
if (['autostart', 'reconnect', 'cleanup'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;

View File

@ -43,7 +43,7 @@ export default function Control(props) {
<Grid item xs={12}>
<Checkbox label={<Trans>Enable snapshots</Trans>} checked={settings.enable} onChange={handleChange('enable')} />
</Grid>
<Grid item xs={12}>
<Grid item xs={12} md={6}>
<TextField
variant="outlined"
fullWidth

View File

@ -26,13 +26,7 @@ const Component = function (props) {
<ModalContent title={props.title} onClose={props.onClose} onHelp={props.onHelp} style={{ overflow: 'hidden' }}>
<Grid container spacing={1}>
<Grid item xs={12}>
<Stack
direction="column"
justifyContent="center"
alignItems="center"
spacing={1}
className={classes.box}
>
<Stack direction="column" justifyContent="center" alignItems="center" spacing={1} className={classes.box}>
<Textarea {...other} />
</Stack>
</Grid>

View File

@ -1,14 +1,14 @@
/* eslint-disable import/no-anonymous-default-export */
import palette from './palette';
import "@fontsource/dosis/300.css"
import "@fontsource/dosis/400.css"
import "@fontsource/dosis/500.css"
import "@fontsource/dosis/700.css"
import "@fontsource/roboto/300.css"
import "@fontsource/roboto/400.css"
import "@fontsource/roboto/500.css"
import "@fontsource/roboto/700.css"
import '@fontsource/dosis/300.css';
import '@fontsource/dosis/400.css';
import '@fontsource/dosis/500.css';
import '@fontsource/dosis/700.css';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
const base = {
htmlFontSize: 16,
@ -74,7 +74,7 @@ const base = {
button: {
fontSize: '.9rem',
},
pagetitel: {
pagetitle: {
fontSize: '1rem',
textTransform: 'uppercase',
fontWeight: 500,

View File

@ -8,7 +8,6 @@ const root = {
};
const outlined = {
base: {
color: base.palette.text.primary,
backgroundColor: base.palette.background.dark1,

View File

@ -8,7 +8,7 @@ export default {
marginTop: '0.35em',
marginBottom: '0.35em',
borderTop: `2px solid ${base.palette.text.disabled}`,
opacity: .6,
opacity: 0.6,
},
},
};

View File

@ -6,13 +6,13 @@ export default {
html: {
width: '100%',
height: '100%',
fontSize: '16px/1.5'
fontSize: '16px/1.5',
},
body: {
background: `${base.palette.background.button_disabled} url(${universe}) no-repeat fixed left top`,
backgroundSize: 'cover',
overflowX: 'hidden',
overflowY: 'scroll'
overflowY: 'scroll',
},
code: {
fontFamily: 'soure-code-pro, monospace',
@ -20,7 +20,8 @@ export default {
textarea: {
width: '100%',
backgroundColor: base.palette.background.modalbox,
fontFamily: 'Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace',
fontFamily:
'Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace',
fontSize: 14,
whiteSpace: 'pre',
overflow: 'auto',
@ -35,7 +36,7 @@ export default {
outline: 'none',
},
'::-webkit-scrollbar': {
width:6,
width: 6,
height: 6,
},
'::-webkit-scrollbar:vertical': {

46
src/utils/clipboard.js Normal file
View File

@ -0,0 +1,46 @@
export default async function CopyToClipboard(content) {
let success = false;
if (!navigator.clipboard) {
success = writeTextDeprecated(content);
} else {
success = await writeText(navigator.clipboard.writeText(content));
}
return success;
}
const writeText = (promise) => {
return promise
.then(() => true)
.catch((err) => {
console.warn(err);
return false;
});
};
const writeTextDeprecated = (value) => {
// Strangely, this doesn't seem to work if the text is longer than just one row
var element = document.createElement('textarea');
element.value = value;
element.style.position = 'absolute';
element.style.width = '10px';
element.style.height = '10px';
element.style.top = '-1000px';
element.style.left = '-1000px';
document.body.appendChild(element);
element.select();
try {
document.execCommand('copy');
} catch (err) {
console.warn(err);
return false;
}
document.body.removeChild(element);
return true;
};

View File

@ -165,6 +165,7 @@ const topics = {
function getTopicURL(topic, locale) {
if (!(topic in topics)) {
console.warn(`help topic "${topic}" not found`);
// If topic doesn't exist, return default URL
return 'https://docs.datarhei.com/restreamer';
}

View File

@ -588,11 +588,12 @@ class Restreamer {
};
for (let device of val.devices.demuxers) {
if (['avfoundation', 'video4linux2', 'alsa', 'fbdev'].includes(device.id)) {
if (device.devices.length === 0) {
if (!['avfoundation', 'video4linux2', 'alsa', 'fbdev'].includes(device.id)) {
continue;
}
// It's OK to have an empty list of devices because a device might get
// plugged meanwhile and a refresh is required.
skills.sources[device.id] = [];
// Split out a Raspberry Pi camera and create a dedicated source
@ -613,14 +614,6 @@ class Restreamer {
}
}
}
}
// Check if the V4L sources are empty and remove them in this case
if ('video4linux2' in skills.sources) {
if (skills.sources['video4linux2'].length === 0) {
delete skills.sources['video4linux2'];
}
}
for (let device of val.devices.muxers) {
if (['fbdev'].includes(device.id)) {
@ -1032,7 +1025,7 @@ class Restreamer {
}
async DeleteChannel(channelid) {
const channel = this.channels.get(channelid);
const channel = this.GetChannel(channelid);
if (channel === null) {
return false;
}
@ -1045,8 +1038,6 @@ class Restreamer {
await this.DeleteEgress(channel.channelid, egressid);
}
await this.CleanupIngest(channel.channelid);
this.channels.delete(channel.channelid);
if (this.channels.size === 0) {
@ -1425,7 +1416,7 @@ class Restreamer {
return await this._deleteProcess(channel.id);
}
// Delete the inges snaphot process
// Delete the ingest snaphot process
async DeleteIngestSnapshot(channelid) {
const channel = this.GetChannel(channelid);
if (channel === null) {
@ -1473,11 +1464,13 @@ class Restreamer {
{
pattern: `memfs:/${channel.channelid}_*.ts`,
max_files: parseInt(control.hls.listSize) + 2,
max_file_age_seconds: parseInt(control.hls.segmentDuration) * (parseInt(control.hls.listSize) + 2),
max_file_age_seconds: control.hls.cleanup ? parseInt(control.hls.segmentDuration) * (parseInt(control.hls.listSize) + 2) : 0,
purge_on_delete: true,
},
{
pattern: `memfs:/${channel.channelid}.m3u8`,
max_file_age_seconds: parseInt(control.hls.segmentDuration) * (parseInt(control.hls.listSize) + 2),
max_file_age_seconds: control.hls.cleanup ? parseInt(control.hls.segmentDuration) * (parseInt(control.hls.listSize) + 2) : 0,
purge_on_delete: true,
},
],
};
@ -1565,12 +1558,18 @@ class Restreamer {
id: 'output_0',
address: `{memfs}/${channel.channelid}.jpg`,
options: ['-vframes', '1', '-f', 'image2', '-update', '1'],
cleanup: [
{
pattern: `memfs:/${channel.channelid}.jpg`,
purge_on_delete: true,
},
],
},
],
options: ['-err_detect', 'ignore_err'],
autostart: control.process.autostart,
reconnect: true,
reconnect_delay_seconds: control.snapshot.interval,
reconnect_delay_seconds: parseInt(control.snapshot.interval),
stale_timeout_seconds: 30,
};
@ -1592,30 +1591,6 @@ class Restreamer {
return [val, null];
}
// Cleanup the ingest files from the memfs
async CleanupIngest(channelid) {
const channel = this.GetChannel(channelid);
if (channel === null) {
return false;
}
let [files] = await this._call(this.api.MemFSListFiles, `/${channel.channelid}*`);
if (Array.isArray(files) === false) {
files = [];
}
let cleanup = true;
for (let file of files) {
const [, err] = await this._call(this.api.MemFSDeleteFile, file.name);
if (err !== null) {
cleanup = false;
}
}
return cleanup;
}
// Check whether the manifest of the ingest process is available
async HasIngestFiles(channelid) {
const channel = this.GetChannel(channelid);
@ -1870,11 +1845,12 @@ class Restreamer {
player: 'videojs',
playersite: true,
channelid: 'current',
titel: 'restreamer',
title: 'restreamer',
share: true,
support: true,
template: '!default',
templatename: '',
textcolor_titel: 'rgba(255,255,255,1)',
textcolor_title: 'rgba(255,255,255,1)',
textcolor_default: 'rgba(230,230,230,1)',
textcolor_link: 'rgba(230,230,230,1)',
textcolor_link_hover: 'rgba(255,255,255,1)',
@ -1938,15 +1914,23 @@ class Restreamer {
return arg1 === arg2 ? options.fn(this) : options.inverse(this);
});
Handlebars.registerHelper('ifnoteq', function (arg1, arg2, options) {
if (arg1 !== arg2) {
return options.fn(this);
}
return options.inverse(this);
});
for (const item of channels) {
const ingestMetadata = await this.GetIngestMetadata(item.channelid);
const templateData = {
player: settings.player,
playersite: settings.playersite,
titel: settings.titel,
title: settings.title,
share: settings.share,
support: settings.support,
url: this.GetPlayersiteUrl(),
textcolor_titel: settings.textcolor_titel,
textcolor_title: settings.textcolor_title,
textcolor_default: settings.textcolor_default,
textcolor_link: settings.textcolor_link,
textcolor_link_hover: settings.textcolor_link_hover,
@ -2466,7 +2450,7 @@ class Restreamer {
};
const findVersion = (name) => {
const matches = name.match(/\sv(\d+\.\d+\.\d+)\s?/);
const matches = name.match(/v(\d+\.\d+\.\d+)\s*$/);
if (matches === null) {
return '0.0.0';
}
@ -2477,11 +2461,13 @@ class Restreamer {
const currentVersion = findVersion(Version.UI);
const announcedVersion = findVersion(value.latest_version);
if (currentVersion !== '0.0.0') {
if (SemverGt(announcedVersion, currentVersion)) {
this.hasUpdates = true;
} else {
this.hasUpdates = false;
}
}
const serviceVersion = findVersion(value.service_version);
if (SemverGte(serviceVersion, '1.0.0')) {
@ -2625,14 +2611,15 @@ class Restreamer {
}
if (p.state) {
for (let i in p.state.progress.input) {
p.state.progress.input[i].address = replace(p.state.progress.input[i].address);
for (let i in p.state.progress.inputs) {
p.state.progress.inputs[i].address = replace(p.state.progress.inputs[i].address);
}
for (let i in p.state.progress.output) {
p.state.progress.output[i].address = replace(p.state.progress.output[i].address);
for (let i in p.state.progress.outputs) {
p.state.progress.outputs[i].address = replace(p.state.progress.outputs[i].address);
}
p.state.command = p.state.command.map(replace);
p.state.last_logline = replace(p.state.last_logline);
}

View File

@ -1,7 +1,7 @@
import { name, version, bundle } from '../package.json';
const Core = '^15.0.0 || ^16.0.0';
const FFmpeg = '^4.1.0';
const FFmpeg = '^4.1.0 || ^5.0.0';
const UI = bundle ? bundle : name + ' v' + version;
export { Core, FFmpeg, UI };

View File

@ -36,6 +36,7 @@ export default function Source(props) {
modal: false,
status: 'none',
});
const [$skillsRefresh, setSkillsRefresh] = React.useState(false);
const [$modal, setModal] = React.useState({
open: false,
data: '',
@ -193,6 +194,12 @@ export default function Source(props) {
return status === 'success';
};
const handleRefresh = async () => {
setSkillsRefresh(true);
await props.onRefresh();
setSkillsRefresh(false);
};
const handleEncoding = (type) => (encoder, decoder) => {
const profile = $profile[type];
@ -321,6 +328,7 @@ export default function Source(props) {
config={props.config}
onProbe={handleProbe}
onChange={handleSourceSettingsChange}
onRefresh={handleRefresh}
/>
</Grid>
{$videoProbe.status !== 'none' && (
@ -426,6 +434,7 @@ export default function Source(props) {
onProbe={handleProbe}
onSelect={handleSourceChange}
onChange={handleSourceSettingsChange}
onRefresh={handleRefresh}
/>
</Grid>
{$profile.custom.selected === false && $profile.custom.stream >= 0 && (
@ -544,7 +553,7 @@ export default function Source(props) {
/>
</React.Fragment>
)}
<Backdrop open={$videoProbe.probing || $audioProbe.probing}>
<Backdrop open={$videoProbe.probing || $audioProbe.probing || $skillsRefresh}>
<CircularProgress color="inherit" />
</Backdrop>
<ProbeModal open={$modal.open} onClose={handleModal('none')} data={$modal.data} />
@ -566,4 +575,5 @@ Source.defaultProps = {
log: ['onProbe function not provided for this component'],
};
},
onRefresh: function () {},
};

View File

@ -1,22 +1,23 @@
import React from 'react';
import { faMagic, faPen } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Trans } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import EditIcon from '@mui/icons-material/Edit';
import * as M from '../../utils/metadata';
import Summary from './Summary';
function WizardIcon(props) {
return <FontAwesomeIcon icon={faMagic} {...props} />;
function IconWizard(props) {
return (<AutoFixHighIcon {...props} />);
}
function EditIcon(props) {
return <FontAwesomeIcon icon={faPen} {...props} />;
function IconEdit(props) {
return (<EditIcon {...props} />);
}
const useStyles = makeStyles((theme) => ({
@ -56,10 +57,10 @@ export default function ProfileSummary(props) {
}}
>
<IconButton size="small" color="inherit" onClick={handleEdit('video')}>
<EditIcon />
<IconEdit />
</IconButton>
<IconButton size="small" color="inherit" onClick={handleWizard()}>
<WizardIcon />
<IconWizard />
</IconButton>
<Typography variant="h3">
<Trans>Video settings</Trans>
@ -80,10 +81,10 @@ export default function ProfileSummary(props) {
}}
>
<IconButton size="small" color="inherit" onClick={handleEdit('audio')}>
<EditIcon />
<IconEdit />
</IconButton>
<IconButton size="small" color="inherit" onClick={handleWizard()}>
<WizardIcon />
<IconWizard />
</IconButton>
<Typography variant="h3" className={classes.title}>
<Trans>Audio settings</Trans>

View File

@ -1,9 +1,12 @@
import React from 'react';
import SemverSatisfies from 'semver/functions/satisfies';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { Trans } from '@lingui/macro';
import Sources from './Sources';
function initConfig(initialConfig) {
@ -61,6 +64,10 @@ export default function SourceSelect(props) {
props.onSelect(props.type, source);
};
const handleRefresh = async () => {
await props.onRefresh();
};
const handleProbe = async (settings, inputs) => {
await props.onProbe(props.type, $source, settings, inputs);
};
@ -80,6 +87,7 @@ export default function SourceSelect(props) {
if (s !== null) {
const Component = s.component;
if (SemverSatisfies(props.skills.ffmpeg.version, s.ffversion)) {
sourceControl = (
<Component
knownDevices={props.skills.sources[$source]}
@ -88,14 +96,22 @@ export default function SourceSelect(props) {
settings={$settings[$source]}
onChange={handleChange($source)}
onProbe={handleProbe}
onRefresh={handleRefresh}
/>
);
}
}
return (
<Grid container spacing={1}>
<Grid item xs={12}>
<Select type={props.type} selected={$source} availableSources={props.skills.sources} onSelect={handleSource} />
<Select
type={props.type}
selected={$source}
ffversion={props.skills.ffmpeg.version}
availableSources={props.skills.sources}
onSelect={handleSource}
/>
</Grid>
<Grid item xs={12}>
{sourceControl}
@ -112,6 +128,7 @@ SourceSelect.defaultProps = {
onProbe: function (type, device, settings, inputs) {},
onSelect: function (type, device) {},
onChange: function (type, device, settings) {},
onRefresh: function () {},
};
function Select(props) {
@ -130,6 +147,10 @@ function Select(props) {
continue;
}
if (!SemverSatisfies(props.ffversion, s.ffversion)) {
continue;
}
const variant = s.id === props.selected ? 'bigSelected' : 'big';
const Icon = s.icon;
@ -145,6 +166,18 @@ function Select(props) {
);
}
if (availableSources.length === 0) {
return (
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>No sources available</Trans>
</Typography>
</Grid>
</Grid>
);
}
return (
<Grid container spacing={1}>
{availableSources}
@ -155,6 +188,7 @@ function Select(props) {
Select.defaultProps = {
type: '',
selected: '',
ffversion: '0.0.0',
availableSources: {},
onSelect: function (source) {},
};

View File

@ -3,8 +3,10 @@ import React from 'react';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Icon from '@mui/icons-material/Usb';
import RefreshIcon from '@mui/icons-material/Refresh';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
@ -73,6 +75,10 @@ function Source(props) {
});
};
const handleRefresh = () => {
props.onRefresh();
};
const handleProbe = () => {
props.onProbe(settings, createInputs(settings));
};
@ -95,7 +101,14 @@ function Source(props) {
label: i18n._(t`Custom ...`),
});
const audioDevices = (
return (
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12}>
<Typography>
<Trans>Select a device:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<SelectCustom
options={options}
label={<Trans>Audio device</Trans>}
@ -105,17 +118,9 @@ function Source(props) {
variant="outlined"
allowCustom
/>
);
return (
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12}>
<Typography>
<Trans>Select a device:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
{audioDevices}
<Button size="small" startIcon={<RefreshIcon />} onClick={handleRefresh} sx={{ float: 'right' }}>
<Trans>Refresh</Trans>
</Button>
</Grid>
<Grid item xs={12}>
<Audio.Sampling value={settings.sampling} onChange={handleChange('sampling')} allowCustom />
@ -140,6 +145,7 @@ Source.defaultProps = {
settings: {},
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function SourceIcon(props) {
@ -149,10 +155,11 @@ function SourceIcon(props) {
const id = 'alsa';
const name = <Trans>ALSA</Trans>;
const capabilities = ['audio'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -3,8 +3,10 @@ import React from 'react';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Icon from '@mui/icons-material/Apple';
import RefreshIcon from '@mui/icons-material/Refresh';
import Typography from '@mui/material/Typography';
import Checkbox from '../../../misc/Checkbox';
@ -92,6 +94,10 @@ function Source(props) {
});
};
const handleRefresh = () => {
props.onRefresh();
};
const handleProbe = () => {
props.onProbe(settings, createInputs(settings));
};
@ -171,6 +177,9 @@ function Source(props) {
</Grid>
<Grid item xs={12}>
{audioDevices}
<Button size="small" startIcon={<RefreshIcon />} onClick={handleRefresh} sx={{ float: 'right' }}>
<Trans>Refresh</Trans>
</Button>
</Grid>
<Grid item xs={12}>
<Video.Format value={settings.format} onChange={handleChange('format')} allowCustom />
@ -201,6 +210,7 @@ Source.defaultProps = {
settings: {},
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function SourceIcon(props) {
@ -210,10 +220,11 @@ function SourceIcon(props) {
const id = 'avfoundation';
const name = <Trans>AVFoundation</Trans>;
const capabilities = ['audio', 'video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -128,10 +128,11 @@ function SourceIcon(props) {
const id = 'fbdev';
const name = <Trans>Framebuffer</Trans>;
const capabilities = ['video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -1,5 +1,6 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import SemverSatisfies from 'semver/functions/satisfies';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
@ -116,10 +117,16 @@ const initSkills = (initialSkills) => {
}
const skills = {
ffmpeg: {},
protocols: {},
...initialSkills,
};
skills.ffmpeg = {
version: '0.0.0',
...skills.version,
};
skills.protocols = {
input: [],
...skills.protocols,
@ -132,6 +139,11 @@ const createInputs = (settings, config, skills) => {
config = initConfig(config);
skills = initSkills(skills);
let ffmpeg_version = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffmpeg_version = 5;
}
const input = {
address: '',
options: [],
@ -159,7 +171,11 @@ const createInputs = (settings, config, skills) => {
input.address = addUsernamePassword(input.address, settings.username, settings.password);
if (protocol === 'rtsp') {
if (ffmpeg_version === 4) {
input.options.push('-stimeout', settings.rtsp.stimeout);
} else {
input.options.push('-timeout', settings.rtsp.stimeout);
}
if (settings.rtsp.udp === true) {
input.options.push('-rtsp_transport', 'udp');
@ -209,10 +225,10 @@ const addUsernamePassword = (address, username, password) => {
const url = urlparser(address, {});
if (username.length !== 0) {
url.set('username', encodeURIComponent(username));
url.set('username', username);
}
if (password.length !== 0) {
url.set('password', encodeURIComponent(password));
url.set('password', password);
}
return url.toString();
@ -667,6 +683,7 @@ function SourceIcon(props) {
const id = 'network';
const name = <Trans>Network source</Trans>;
const capabilities = ['audio', 'video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
@ -680,4 +697,4 @@ const func = {
isAuthProtocol,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -44,10 +44,11 @@ function SourceIcon(props) {
const id = 'noaudio';
const name = <Trans>No audio</Trans>;
const capabilities = ['audio'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -131,10 +131,11 @@ function SourceIcon(props) {
const id = 'raspicam';
const name = <Trans>Raspberry Pi camera</Trans>;
const capabilities = ['video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -5,7 +5,9 @@ import { useLingui } from '@lingui/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import RefreshIcon from '@mui/icons-material/Refresh';
import Typography from '@mui/material/Typography';
import FormInlineButton from '../../../misc/FormInlineButton';
@ -67,6 +69,10 @@ function Source(props) {
});
};
const handleRefresh = () => {
props.onRefresh();
};
const handleProbe = () => {
props.onProbe(settings, createInputs(settings));
};
@ -90,7 +96,14 @@ function Source(props) {
label: i18n._(t`Custom ...`),
});
const videoDevices = (
return (
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12}>
<Typography>
<Trans>Select a device:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<SelectCustom
options={options}
label={<Trans>Video device</Trans>}
@ -100,17 +113,9 @@ function Source(props) {
variant="outlined"
allowCustom
/>
);
return (
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12}>
<Typography>
<Trans>Select a device:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
{videoDevices}
<Button size="small" startIcon={<RefreshIcon />} onClick={handleRefresh} sx={{ float: 'right' }}>
<Trans>Refresh</Trans>
</Button>
</Grid>
<Grid item xs={12}>
<Video.Format value={settings.format} onChange={handleChange('format')} allowCustom />
@ -135,6 +140,7 @@ Source.defaultProps = {
settings: {},
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function SourceIcon(props) {
@ -144,10 +150,11 @@ function SourceIcon(props) {
const id = 'video4linux2';
const name = <Trans>Hardware device</Trans>;
const capabilities = ['video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -44,10 +44,11 @@ function SourceIcon(props) {
const id = 'videoaudio';
const name = <Trans>Video source</Trans>;
const capabilities = ['audio'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -81,7 +81,7 @@ function Source(props) {
<Grid container alignItems="flex-start" spacing={2} style={{ marginTop: '0.5em' }}>
<Grid item xs={12}>
<Typography>
<Trans>Select audio Source:</Trans>
<Trans>Select audio source:</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
@ -186,10 +186,11 @@ function SourceIcon(props) {
const id = 'virtualaudio';
const name = <Trans>Virtual source</Trans>;
const capabilities = ['audio'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -161,10 +161,22 @@ function Source(props) {
<TextField variant="outlined" fullWidth label={<Trans>Ratio</Trans>} value={settings.ratio} onChange={handleChange('ratio')} />
</Grid>
<Grid item xs={12}>
<TextField variant="outlined" fullWidth label={<Trans>Death color</Trans>} value={settings.death_color} onChange={handleChange('death_color')} />
<TextField
variant="outlined"
fullWidth
label={<Trans>Death color</Trans>}
value={settings.death_color}
onChange={handleChange('death_color')}
/>
</Grid>
<Grid item xs={12}>
<TextField variant="outlined" fullWidth label={<Trans>Life color</Trans>} value={settings.life_color} onChange={handleChange('life_color')} />
<TextField
variant="outlined"
fullWidth
label={<Trans>Life color</Trans>}
value={settings.life_color}
onChange={handleChange('life_color')}
/>
</Grid>
<Grid item xs={12}>
<TextField variant="outlined" fullWidth label={<Trans>Flags</Trans>} value={settings.flags} onChange={handleChange('flags')} />
@ -193,10 +205,11 @@ function SourceIcon(props) {
const id = 'virtualvideo';
const name = <Trans>Virtual source</Trans>;
const capabilities = ['video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
createInputs,
};
export { id, name, capabilities, SourceIcon as icon, Source as component, func };
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };

View File

@ -2,9 +2,11 @@ import React from 'react';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Icon from '@mui/icons-material/Apple';
import MenuItem from '@mui/material/MenuItem';
import RefreshIcon from '@mui/icons-material/Refresh';
import Typography from '@mui/material/Typography';
import * as S from '../../Sources/AVFoundation';
@ -28,7 +30,13 @@ function Source(props) {
const handleChange = (newSettings) => {
newSettings = newSettings || settings;
props.onChange(S.id, newSettings, S.func.createInputs(newSettings), true);
const filteredDevices = props.knownDevices.filter((device) => device.media === 'video');
props.onChange(S.id, newSettings, S.func.createInputs(newSettings), filteredDevices.length !== 0 ? true : false);
};
const handleRefresh = () => {
props.onRefresh();
};
const update = (what) => (event) => {
@ -56,11 +64,19 @@ function Source(props) {
);
});
if (options.length === 0) {
options.push(
<MenuItem key="none" value="none" disabled={true}>
{i18n._(t`No input device available`)}
</MenuItem>
);
} else {
options.unshift(
<MenuItem key="default" value="default">
{i18n._(t`Default`)}
</MenuItem>
);
}
const videoDevices = (
<Select label={<Trans>Video device</Trans>} value={settings.vindex} onChange={update('vindex')}>
@ -82,11 +98,14 @@ function Source(props) {
{i18n._(t`None`)}
</MenuItem>
);
if (options.length > 1) {
options.unshift(
<MenuItem key="default" value="default">
{i18n._(t`Default`)}
</MenuItem>
);
}
const audioDevices = (
<Select label={<Trans>Audio Device</Trans>} value={settings.aindex} onChange={update('aindex')}>
@ -112,6 +131,9 @@ function Source(props) {
<Grid container alignItems="center" spacing={1}>
<Grid item xs={12}>
{audioDevices}
<Button size="small" startIcon={<RefreshIcon />} onClick={handleRefresh} sx={{ float: 'right' }}>
<Trans>Refresh</Trans>
</Button>
</Grid>
</Grid>
</Grid>
@ -123,6 +145,7 @@ Source.defaultProps = {
knownDevices: [],
settings: {},
onChange: function (type, settings, inputs, ready) {},
onRefresh: function () {},
};
function SourceIcon(props) {

View File

@ -1,10 +1,13 @@
import React from 'react';
import { useLingui } from '@lingui/react';
import { faUsb } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Trans } from '@lingui/macro';
import { Trans, t } from '@lingui/macro';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import RefreshIcon from '@mui/icons-material/Refresh';
import Typography from '@mui/material/Typography';
import * as S from '../../Sources/V4L';
@ -52,13 +55,18 @@ function initDevices(initialDevices) {
}
function Source(props) {
const { i18n } = useLingui();
const settings = initSettings(props.settings, props.knownDevices);
const devices = initDevices(props.knownDevices);
const handleChange = (newSettings) => {
newSettings = newSettings || settings;
props.onChange(S.id, newSettings, S.func.createInputs(newSettings), true);
props.onChange(S.id, newSettings, S.func.createInputs(newSettings), devices.length !== 0 ? true : false);
};
const handleRefresh = () => {
props.onRefresh();
};
const update = (what) => (event) => {
@ -85,6 +93,14 @@ function Source(props) {
);
});
if (options.length === 0) {
options.push(
<MenuItem key="none" value="none" disabled={true}>
{i18n._(t`No input device available`)}
</MenuItem>
);
}
const videoDevices = (
<Select label={<Trans>Video device</Trans>} value={settings.device} onChange={update('device')}>
{options}
@ -100,6 +116,9 @@ function Source(props) {
</Grid>
<Grid item xs={12}>
{videoDevices}
<Button size="small" startIcon={<RefreshIcon />} onClick={handleRefresh} sx={{ float: 'right' }}>
<Trans>Refresh</Trans>
</Button>
</Grid>
</React.Fragment>
);
@ -109,6 +128,7 @@ Source.defaultProps = {
knownDevices: [],
settings: {},
onChange: function (type, settings, inputs, ready) {},
onRefresh: function () {},
};
function SourceIcon(props) {

View File

@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
@ -48,6 +49,7 @@ export default function Wizard(props) {
probing: false,
status: 'none',
});
const [$skillsRefresh, setSkillsRefresh] = React.useState(false);
const [$abort, setAbort] = React.useState({
step: 'TYPE',
});
@ -83,6 +85,13 @@ export default function Wizard(props) {
setReady(true);
};
const refreshSkills = async () => {
await props.restreamer.RefreshSkills();
const skills = await props.restreamer.Skills();
setSkills(skills);
};
const probe = async (type, source) => {
setProbe({
...$probe,
@ -164,8 +173,6 @@ export default function Wizard(props) {
data.streams = M.createOutputStreams(sources, profiles);
await props.restreamer.CleanupIngest(_channelid);
const [, err] = await props.restreamer.UpsertIngest(_channelid, inputs, outputs, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', err.message);
@ -327,6 +334,12 @@ export default function Wizard(props) {
});
};
const handleRefresh = async () => {
setSkillsRefresh(true);
await refreshSkills();
setSkillsRefresh(false);
};
const s = Sources.Get($sourceid);
if (s === null) {
setStep('TYPE');
@ -349,6 +362,7 @@ export default function Wizard(props) {
settings={$sources.video.settings}
skills={$skills}
onChange={handleChange}
onRefresh={handleRefresh}
/>
<Grid item xs={12}>
{$probe.status === 'error' && (
@ -400,6 +414,9 @@ export default function Wizard(props) {
</Button>
</Grid>
</Grid>
<Backdrop open={$skillsRefresh}>
<CircularProgress color="inherit" />
</Backdrop>
</Paper>
);
}
@ -1049,8 +1066,8 @@ export default function Wizard(props) {
<Grid item xs={12}>
<Typography>
<Trans>
Use your copyright and choose the correct image license. Whether free for all or highly restricted.
Briefly discuss what others are allowed to do with your image.
Use your copyright and choose the correct image license. Whether free for all or highly restricted. Briefly discuss what others
are allowed to do with your image.
</Trans>
</Typography>
</Grid>

View File

@ -195,6 +195,13 @@ export default function Edit(props) {
});
};
const handleSkillsRefresh = async () => {
await props.restreamer.RefreshSkills();
const skills = await props.restreamer.Skills();
setSkills(skills);
};
const handleSourceProbe = async (inputs) => {
let [res, err] = await props.restreamer.Probe(_channelid, inputs);
if (err !== null) {
@ -284,12 +291,6 @@ export default function Edit(props) {
return false;
}
// Cleanup previous files
let res = await props.restreamer.CleanupIngest(_channelid);
if (res === false) {
notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to correctly cleanup previous process data`));
}
// Create/update the ingest
const [, err] = await props.restreamer.UpsertIngest(_channelid, inputs, outputs, control);
if (err !== null) {
@ -298,7 +299,7 @@ export default function Edit(props) {
}
// Save the metadata
res = await props.restreamer.SetIngestMetadata(_channelid, $data);
let res = await props.restreamer.SetIngestMetadata(_channelid, $data);
if (res === false) {
notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to save ingest metadata`));
}
@ -449,6 +450,7 @@ export default function Edit(props) {
config={$config.source}
startWith={$state.edit}
onProbe={handleSourceProbe}
onRefresh={handleSkillsRefresh}
onDone={handleSourceDone}
onAbort={handleSourceAbort}
/>

View File

@ -226,7 +226,7 @@ export default function Login(props) {
return (
<Paper xs={11} sm={10} md={6}>
<Grid container spacing={3} padding={{xs: 1, sm: 5}}>
<Grid container spacing={3} padding={{ xs: 1, sm: 5 }}>
{$loginTarget !== 'none' && (
<Grid item xs={12} align="center">
{$auths.length > 1 && (

View File

@ -6,11 +6,13 @@ import makeStyles from '@mui/styles/makeStyles';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import useInterval from '../../hooks/useInterval';
import ActionButton from '../../misc/ActionButton';
import CopyButton from '../../misc/CopyButton';
import DebugModal from '../../misc/modals/Debug';
import H from '../../utils/help';
import Paper from '../../misc/Paper';
@ -19,7 +21,6 @@ import Player from '../../misc/Player';
import Progress from './Progress';
import Publication from './Publication';
import ProcessModal from '../../misc/modals/Process';
import TextFieldCopy from '../../misc/TextFieldCopy';
import Welcome from '../Welcome';
const useStyles = makeStyles((theme) => ({
@ -141,7 +142,6 @@ export default function Main(props) {
},
});
await props.restreamer.CleanupIngest(_channelid);
await props.restreamer.StartIngest(_channelid);
await props.restreamer.StartIngestSnapshot(_channelid);
};
@ -156,9 +156,6 @@ export default function Main(props) {
await props.restreamer.StopIngest(_channelid);
await disconnectEgresses();
// Cleanup previous files
await props.restreamer.CleanupIngest(_channelid);
};
const reconnect = async () => {
@ -363,10 +360,19 @@ export default function Main(props) {
<Progress progress={$state.progress} />
</Grid>
<Grid item xs={12} marginTop="-.2em">
<TextFieldCopy
label={<Trans>HLS URL</Trans>}
value={address + manifest}
/>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Typography variant="body">
<Trans>Content URL</Trans>
</Typography>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={0.5}>
<CopyButton variant="outlined" color="default" size="small" value={address + manifest}>
<Trans>HLS</Trans>
</CopyButton>
<CopyButton variant="outlined" color="default" size="small" value={address + poster}>
<Trans>Snapshot</Trans>
</CopyButton>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} marginTop="0em">
<ActionButton

View File

@ -76,7 +76,7 @@ export default function ResetPassword(props) {
});
};
const invalid = $login.username.length === 0 || $login.password.length === 0 || ($login.password !== $login.passwordConfirm);
const invalid = $login.username.length === 0 || $login.password.length === 0 || $login.password !== $login.passwordConfirm;
return (
<Paper xs={12} sm={10} md={6} className="PaperL">

View File

@ -98,7 +98,7 @@ export default function Playersite(props) {
const value = event.target.value;
const settings = $settings;
if (['playersite', 'header', 'support'].includes(what)) {
if (['playersite', 'header', 'share', 'support'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
@ -136,9 +136,10 @@ export default function Playersite(props) {
if (type === null) {
// not one of the allowed mimetypes
setSaving(false);
const types = imageAcceptString;
showUploadError(
<Trans>
The selected file type ({file.type}) is now allowed. Allowed file types are {imageAcceptString}
The selected file type ({file.type}) is not allowed. Allowed file types are {types}
</Trans>
);
return;
@ -210,9 +211,10 @@ export default function Playersite(props) {
if (type === null) {
// not one of the allowed mimetypes
setSaving(false);
const types = templateAcceptString;
showUploadError(
<Trans>
The selected file type ({file.type}) is now allowed. Allowed file types are {templateAcceptString}
The selected file type ({file.type}) is not allowed. Allowed file types are {types}
</Trans>
);
return;
@ -235,7 +237,7 @@ export default function Playersite(props) {
if (reader.result === null) {
// reading the file failed
setSaving(false);
showUploadError(<Trans>There war an error during upload: {reader.error.message}</Trans>);
showUploadError(<Trans>There was an error during upload: {reader.error.message}</Trans>);
return;
}
@ -364,7 +366,8 @@ export default function Playersite(props) {
<Grid item xs={12}>
<Typography variant="body1">
<Trans>
In addition to the player, the Restreamer offers a complete landingpage, which you can use to present your live stream easily and quickly.
In addition to the player, the Restreamer offers a complete landingpage, which you can use to present your live
stream easily and quickly.
</Trans>
</Typography>
</Grid>
@ -377,8 +380,8 @@ export default function Playersite(props) {
fullWidth
label={<Trans>Sitename</Trans>}
disabled={!$settings.playersite}
value={$settings.titel}
onChange={handleChange('titel')}
value={$settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
@ -409,6 +412,14 @@ export default function Playersite(props) {
<Trans>Main page channel (index.html).</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Share button</Trans>}
checked={$settings.share}
disabled={!$settings.playersite}
onChange={handleChange('share')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Support datarhei Restreamer</Trans>}
@ -448,7 +459,9 @@ export default function Playersite(props) {
})}
</Select>
<Typography variant="caption">
<Trans>Template to be used for creating the publication website. The delete button removes the selection from the system.</Trans>
<Trans>
Template to be used for creating the publication website. The delete button removes the selection from the system.
</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={3}>
@ -520,8 +533,8 @@ export default function Playersite(props) {
variant="outlined"
fullWidth
label={<Trans>Headline</Trans>}
value={$settings.textcolor_titel}
onChange={handleChange('textcolor_titel')}
value={$settings.textcolor_title}
onChange={handleChange('textcolor_title')}
/>
</Grid>
<Grid item xs={6}>

View File

@ -111,7 +111,9 @@ export default function Edit(props) {
setReady(true);
};
const handleChange = (what, section = '') => (event) => {
const handleChange =
(what, section = '') =>
(event) => {
const value = event.target.value;
const settings = $settings;
@ -171,9 +173,10 @@ export default function Edit(props) {
if (type === null) {
// not one of the allowed mimetypes
setSaving(false);
const types = logoAcceptString;
showUploadError(
<Trans>
The selected file type ({file.type}) is now allowed. Allowed file types are {logoAcceptString}
The selected file type ({file.type}) is not allowed. Allowed file types are {types}
</Trans>
);
return;

View File

@ -5,9 +5,10 @@ import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import TextField from '@mui/material/TextField';
import Checkbox from '../../../misc/Checkbox';
import Logo from './logos/akamai.svg';
import Checkbox from '../../../misc/Checkbox';
const id = 'akamai';
const name = 'Akamai';
const version = '1.0';

View File

@ -13,8 +13,7 @@ const version = '1.0';
const stream_key_link = '';
const description = (
<Trans>
Transmit the main source to an CDN77 RTMP Service.
More about the setup{' '}
Transmit the main source to an CDN77 RTMP Service. More about the setup{' '}
<Link color="secondary" target="_blank" href="https://client.cdn77.com/support/knowledgebase/streaming/ls_general_setup">
here
</Link>

View File

@ -6,13 +6,14 @@ import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import FormInlineButton from '../../../misc/FormInlineButton';
import Logo from './logos/datarhei.svg';
import Select from '../../../misc/Select';
const id = 'datarhei_core';
const name = 'datarhei Core';
const version = '1.0';
const stream_key_link = '';
const stream_key_link = 'https://docs.datarhei.com/restreamer/knowledge-base/manual/system-settings/rtmp';
const description = (
<Trans>
Transmit the main source to an datarhei Core Ressource. More details about the settings can be found{' '}
@ -77,7 +78,7 @@ function Service(props) {
const options = ['-f', 'flv'];
const output = {
address: settings.protocol + settings.base_url + settings.app_path + settings.stream_name + '?token=' + settings.rtmp_token,
address: settings.protocol + settings.base_url + '/' + settings.app_path + '/' + settings.stream_name + '?token=' + settings.rtmp_token,
options: options,
};
@ -103,13 +104,7 @@ function Service(props) {
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
variant="outlined"
fullWidth
label={<Trans>App</Trans>}
value={settings.app_path}
onChange={handleChange('app_path')}
/>
<TextField variant="outlined" fullWidth label={<Trans>App</Trans>} value={settings.app_path} onChange={handleChange('app_path')} />
</Grid>
<Grid item xs={12} md={12}>
<TextField
@ -120,7 +115,7 @@ function Service(props) {
onChange={handleChange('stream_name')}
/>
</Grid>
<Grid item xs={12} md={12}>
<Grid item xs={12} md={9}>
<TextField
variant="outlined"
fullWidth
@ -129,6 +124,11 @@ function Service(props) {
onChange={handleChange('rtmp_token')}
/>
</Grid>
<Grid item xs={12} md={3}>
<FormInlineButton target="blank" href={stream_key_link} component="a">
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
</Grid>
);
}

View File

@ -111,11 +111,7 @@ function Service(props) {
)}
{settings.rtmp_primary === true && (
<Grid item xs={12} md={3}>
<FormInlineButton
target="blank"
href={stream_key_link}
component="a"
>
<FormInlineButton target="blank" href={stream_key_link} component="a">
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
@ -133,11 +129,7 @@ function Service(props) {
)}
{settings.rtmp_backup === true && (
<Grid item xs={12} md={3}>
<FormInlineButton
target="blank"
href={stream_key_link}
component="a"
>
<FormInlineButton target="blank" href={stream_key_link} component="a">
<Trans>GET</Trans>
</FormInlineButton>
</Grid>

View File

@ -15,7 +15,7 @@ const version = '1.0';
const stream_key_link = '';
const description = (
<Trans>
Transmit the main source to an Red5/Pro Server. More details about the settings can be found {' '}
Transmit the main source to an Red5/Pro Server. More details about the settings can be found{' '}
<Link color="secondary" target="_blank" href="https://www.red5pro.com/docs/server/ffmpeg/publishing-live-streams/">
here
</Link>

View File

@ -116,11 +116,7 @@ function Service(props) {
<TextField variant="outlined" fullWidth label={<Trans>Stream key</Trans>} value={settings.stream_key} onChange={handleChange('stream_key')} />
</Grid>
<Grid item cs={12} md={3}>
<FormInlineButton
target="blank"
href={stream_key_link}
component="a"
>
<FormInlineButton target="blank" href={stream_key_link} component="a">
<Trans>GET</Trans>
</FormInlineButton>
</Grid>

View File

@ -129,11 +129,7 @@ function Service(props) {
<TextField variant="outlined" fullWidth label={<Trans>Stream key</Trans>} value={settings.key} onChange={handleChange('key')} />
</Grid>
<Grid item xs={12} md={3}>
<FormInlineButton
target="blank"
href={stream_key_link}
component="a"
>
<FormInlineButton target="blank" href={stream_key_link} component="a">
<Trans>GET</Trans>
</FormInlineButton>
</Grid>

View File

@ -18,8 +18,8 @@ const version = '1.0';
const stream_key_link = 'https://studio.twitter.com/producer/sources';
const description = (
<Trans>
Transmits your video stream with the required key, which was generated in Twitter Producer. You can find more information on seting up a live stream
at Twitter's{' '}
Transmits your video stream with the required key, which was generated in Twitter Producer. You can find more information on seting up a live stream at
Twitter's{' '}
<Link color="secondary" target="_blank" href="https://help.twitter.com/en/using-twitter/how-to-use-live-producer">
Producer
</Link>

View File

@ -9,7 +9,7 @@ import TextField from '@mui/material/TextField';
const id = 'vimeo';
const name = 'Vimeo';
const version = '1.0';
const description = <Trans>Live-Streaming to Vimeos Live RTMP Service</Trans>;
const description = <Trans>Live-Streaming to Vimeo Live RTMP Service</Trans>;
const stream_key_link = '';
const image_copyright = <Trans>More about licenses here</Trans>;
const author = {

View File

@ -1173,7 +1173,12 @@ export default function Settings(props) {
{$updates.has === true && (
<BoxText color="success">
<Typography variant="inherit" color="inherit" width="100%">
<Link color="inherit" style={{ textDecoration: 'underline' }} onClick={handleHelp('update-link')} target="_blank">
<Link
color="inherit"
style={{ textDecoration: 'underline', cursor: 'pointer' }}
onClick={handleHelp('update-link')}
target="_blank"
>
<Trans>There are updates available. Here you get more information.</Trans>
</Link>
</Typography>

2290
yarn.lock

File diff suppressed because it is too large Load Diff