Add v1.0.0

This commit is contained in:
Jan Stabenow 2022-05-13 19:38:30 +02:00
parent a3c08cbb3a
commit 0799686085
No known key found for this signature in database
GPG Key ID: 9C22DD65A9AAF133
340 changed files with 135690 additions and 2 deletions

11
.dockerignore Normal file
View File

@ -0,0 +1,11 @@
Dockerfile*
.dockerignore
.editorconfig
.gitignore
README.md
node_modules/
.yarn/cache
.eslintcache
.github
.github_build
.build

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
# For more information about the properties used in
# this file, please see the EditorConfig documentation:
# http://editorconfig.org/
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = outside
[*.js]
max_line_length = 160
[*.md]
trim_trailing_whitespace = false
indent_style = space
[*.patch]
indent_style = space

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/
# testing
/coverage
# production
/build
# misc
.DS_Store
.VSCodeCounter
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
messages.mo
.eslintcache

10
.linguirc Normal file
View File

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

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
src/locales

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"semi": true,
"jsxSingleQuote": false,
"singleQuote": true
}

4
.yarnrc.yml Normal file
View File

@ -0,0 +1,4 @@
packageExtensions:
react-scripts@*:
peerDependencies:
eslint-config-react-app: "*"

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:17-alpine3.15
WORKDIR /ui
COPY . /ui
RUN yarn install && \
npm run build
EXPOSE 3000
CMD [ "npm", "run", "start" ]

View File

@ -1,2 +1,19 @@
# restreamer-ui
The Restreamer is a complete streaming server solution for self-hosting. It has a visually appealing user interface and no ongoing license costs. Upload your live stream to YouTube, Twitch, Facebook, Vimeo, or other streaming solutions like Wowza. Receive video data from OBS and publish it with the internal RTMP server.
# Restreamer-UI
The user interface of the Restreamer for the connection to the Core application.
- React
- Material-UI (MUI)
## Development
```sh
yarn install
yarn run start
```
Connect the UI to datarhei Core:
http://localhost:3000?address=http://core-ip:core-port/
## License
See the [LICENSE](./LICENSE) file for licensing information.

97
package.json Normal file
View File

@ -0,0 +1,97 @@
{
"name": "restreamer-ui",
"version": "1.0.0",
"bundle": "restreamer-v2.0.0",
"private": false,
"dependencies": {
"@auth0/auth0-spa-js": "^1.16.1",
"@clappr/core": "^0.4.17",
"@clappr/hlsjs-playback": "^0.5.3",
"@clappr/plugins": "^0.4.10",
"@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",
"@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.0.1",
"@testing-library/dom": ">=5",
"@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",
"hls.js": "^0.13.2",
"jwt-decode": "^3.1.2",
"make-plural": "^7.1.0",
"react": "^17.0.2",
"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",
"typescript": "^3.9.7",
"url-parse": "^1.5.10",
"uuid": "^8.3.2",
"video.js": "^7.18.1",
"videojs-overlay": "^2.1.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts --optimize-for-size build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"i18n-extract": "lingui extract",
"i18n-extract:clean": "lingui extract --clean",
"i18n-compile": "lingui compile",
"format": "prettier --write ./src"
},
"eslintConfig": {
"extends": "react-app",
"overrides": [
{
"files": [
"**/*.js"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
"> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11, maintained node versions"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@lingui/cli": "^3.4.0",
"babel-core": "^7.0.0-bridge.0",
"prettier": "2.2.1",
"react-error-overlay": "^6.0.11"
},
"resolutions": {
"url-parse@1.5.3": "patch:url-parse@npm:1.5.3#.yarn/patches/url-parse-npm-1.5.3-225ab9cae7.patch",
"react-error-overlay": "6.0.9"
}
}

21
public/_player/README.md Normal file
View File

@ -0,0 +1,21 @@
# Player Templates
Affected files: `(clappr|videojs)/player.html`, `oembed.json`, and `oembed.xml`.
The templates are interpreted with [handlebars](https://handlebarsjs.com/).
The following placeholders will be replaced by their respective value:
| Placeholder | Description |
| ----------- | ----------------------------------------- |
| name | The user-given name of the ingest. |
| description | The user-given description of the ingest. |
| iframecode | The HTML iframe code for the player. |
| poster | The URL to the latest snapshot image. |
| width | The width of the video/poster. |
| height | The height of the video/poster. |
# File list
Each player directory has a `files.txt` that contains a list of files, that need to
be copied besides the `player.html`.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
dist/clappr.min.js.map
dist/clappr.min.js
dist/clappr-stats.min.js
dist/clappr-nerd-stats.min.js

View File

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{description}}">
<meta name="author" content="datarhei restreamer">
<title>{{name}}</title>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<link rel="alternate" type="application/json+oembed" href="channels/{{channelid}}/oembed.json" title="{{name}}">
<link rel="alternate" type="text/xml+oembed" href="channels/{{channelid}}/oembed.xml" title="{{name}}">
<script src="channels/{{channelid}}/config.js"></script>
<script src="player/clappr/dist/clappr.min.js"></script>
<script src="player/clappr/dist/clappr-stats.min.js"></script>
<script src="player/clappr/dist/clappr-nerd-stats.min.js"></script>
<style>
.player-poster[data-poster] .poster-background[data-poster] {
height: initial !important;
}
</style>
</head>
<body>
<div id="player" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
<script>
function getQueryParam(key, defaultValue) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for(var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if(pair[0] == key) {
return pair[1];
}
}
return defaultValue;
}
function convertBoolParam(key, defaultValue) {
var val = getQueryParam(key, defaultValue);
return val === true || val === "true" || val === "1" || val === "yes" || val === "on";
}
function convertColorParam(parameter, defaultColor) {
var re = new RegExp("^#([0-9a-f]{3}|[0-9a-f]{6})$");
var c = getQueryParam(parameter, defaultColor);
// decode color as # has to be represented by %23
var c = decodeURIComponent(c);
// if color was given without leading #, prepend it
if (!String(c).startsWith("#")) c = "#" + c;
if (re.test(c)) {
return c;
} else {
return defaultColor;
}
}
var autoplay = convertBoolParam("autoplay", playerConfig.autoplay);
var mute = convertBoolParam("mute", playerConfig.mute);
var statistics = convertBoolParam("stats", playerConfig.statistics);
var color = convertColorParam("color", playerConfig.color.buttons);
var plugins = [];
if(statistics == true) {
plugins.push(ClapprNerdStats);
plugins.push(ClapprStats);
}
var config = {
source: playerConfig.source,
parentId: '#player',
baseUrl: 'clappr/',
plugins: plugins,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
mediacontrol: {
seekbar: playerConfig.color.seekbar,
buttons: color
},
height: '100%',
width: '100%',
disableCanAutoPlay: true,
autoPlay: autoplay,
mute: mute,
clapprStats: {
runEach: 1000,
onReport: (metrics) => {},
},
clapprNerdStats: {
shortcut: ['command+shift+s', 'ctrl+shift+s'],
iconPosition: 'top-right'
}
};
if(playerConfig.logo.image.length != 0) {
config.watermark = playerConfig.logo.image;
config.position = playerConfig.logo.position;
if(playerConfig.logo.link.length != 0) {
config.watermarkLink = playerConfig.logo.link;
}
}
var player = new window.Clappr.Player(config);
var posterPlugin = player.core.mediaControl.container.getPlugin('poster');
player.on(window.Clappr.Events.PLAYER_STOP, function updatePoster () {
posterPlugin.options.poster = playerConfig.poster + '?t=' + String(new Date().getTime());
posterPlugin.render();
});
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
{
"version": "1.0",
"type": "video",
"title": {{{name}}},
"description": {{{description}}},
"author_name": {{{author_name}}},
"author_url": {{{author_url}}},
"provider_name": "datarhei Restreamer",
"provider_url": "https://datarhei.org",
"license": {{{license}}},
"html": {{{iframecode}}},
"width": {{width}},
"height": {{height}},
"thumbnail_url": {{{poster_url}}},
"thumbnail_width": {{width}},
"thumbnail_height": {{height}}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<oembed>
<version>1.0</version>
<type>video</type>
<title>{{title}}</title>
<description>{{description}}</description>
<author_name>{{author_name}}</author_name>
<author_url>{{author_url}}</author_url>
<provider_name>datarhei Restreamer</provider_name>
<provider_url>https://datarhei.org</provider_url>
<license>{{license}}</license>
<html>{{iframecode}}</html>
<width>{{width}}</width>
<height>{{height}}</height>
<thumbnail_url>{{poster_url}}</thumbnail_url>
<thumbnail_width>{{width}}</thumbnail_width>
<thumbnail_height>{{height}}</thumbnail_height>
</oembed>

View File

@ -0,0 +1,169 @@
.vjs-public {
--video-js--primary: #EAEA05;
}
/* play btn */
.vjs-public .vjs-big-play-button {
width: 70px;
height: 70px;
background: none;
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:hover .vjs-big-play-button,
.vjs-public.vjs-big-play-button:focus {
background-color: transparent;
color: rgba(255,255,255,1);
}
/* controlbar */
.vjs-public .vjs-control-bar {
height: 70px;
padding-top: 20px;
background: none;
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
}
/* progressbar */
.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;
}
/* volume-panel */
.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
}
.vjs-public .vjs-control .vjs-volume-panel {
width: 4.5em;
}
/* live display */
.vjs-public .vjs-live-display {
margin-left: 1.8em;
}
/* disable caps */
.vjs-internal .vjs-subs-caps-button {
display: none;
}
/* spacer */
.vjs-public .vjs-custom-control-spacer {
display: block;
width: 100%;
}
/* overlay */
.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;
}
/* context menu */
.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);
}
.vjs-public .vjs-lock-open {
z-index: 1000;
}

View File

@ -0,0 +1 @@
.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)}.vjs-public .vjs-lock-open{z-index:1000}

1762
public/_player/videojs/dist/video-js.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

66670
public/_player/videojs/dist/video.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,453 @@
/*! @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';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
function createCommonjsModule(fn, basedir, module) {
return module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
}
}, fn(module, module.exports), module.exports;
}
function commonjsRequire () {
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
}
var assertThisInitialized = createCommonjsModule(function (module) {
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
module.exports = _assertThisInitialized, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var setPrototypeOf = createCommonjsModule(function (module) {
function _setPrototypeOf(o, p) {
module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
}, module.exports.__esModule = true, module.exports["default"] = module.exports;
return _setPrototypeOf(o, p);
}
module.exports = _setPrototypeOf, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var inheritsLoose = createCommonjsModule(function (module) {
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
setPrototypeOf(subClass, superClass);
}
module.exports = _inheritsLoose, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
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 defaults = {
license: 'none',
title: '',
author: '',
languages: {
license: 'License',
loading: 'Loading'
}
};
/**
* An advanced Video.js plugin. For more information on the API
*
* See: https://blog.videojs.com/feature-spotlight-advanced-plugins/
*/
var License = /*#__PURE__*/function (_Plugin) {
inheritsLoose(License, _Plugin);
/**
* Create a License plugin instance.
*
* @param {Player} player
* A Video.js Player instance.
*
* @param {Object} [options]
* An optional options object.
*
* While not a core part of the Video.js plugin architecture, a
* second argument of options is a convenient way to accept inputs
* from your plugin's caller.
*/
function License(player, options) {
var _this;
// 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);
if (options.license === 'none') {
return assertThisInitialized(_this);
}
_this.player.ready(function () {
_this.player.addClass('vjs-license');
_this.buildUI();
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) {
element.classList.remove('vjs-lock-open');
});
}); // close the menu if anywhere in the player is clicked
_this.player.on('click', function (evt) {
if (evt.target.tagName === 'VIDEO') {
document.getElementById(_this.playerId).querySelectorAll('.vjs-menu').forEach(function (element) {
element.classList.remove('vjs-lock-open');
});
}
});
_this.player.on('loadstart', function (_event) {
_this.removeElementsByClass('vjs-license-clear');
if (videojs__default["default"].browser.IS_IOS || videojs__default["default"].browser.IS_ANDROID) {
_this.mobileBuildTopLevelMenu();
} else {
_this.buildTopLevelMenu();
}
});
return _this;
}
/**
* Add the menu ui button to the controlbar
*/
var _proto = License.prototype;
_proto.buildUI = function buildUI() {
var playerId = this.playerId;
var that = this;
/**
* LicenseMenuButton
*/
var LicenseMenuButton = /*#__PURE__*/function (_Button) {
inheritsLoose(LicenseMenuButton, _Button);
/**
* Contructor
*
* @param {*} player videojs player instance
* @param {*} options videojs player options
*/
function LicenseMenuButton(player, options) {
var _this2;
_this2 = _Button.call(this, player, options) || this;
_this2.addClass('vjs-license');
_this2.controlText(that.options.languages.loading);
player.one('canplaythrough', function (_event) {
_this2.controlText(that.options.languages.settings);
});
_this2.menu.contentEl_.id = playerId + '-vjs-license-default';
return _this2;
}
/**
* Handle click
*/
var _proto2 = LicenseMenuButton.prototype;
_proto2.handleClick = function handleClick() {
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');
this.menu.el().classList.toggle('vjs-lock-open');
}
};
return LicenseMenuButton;
}(Button);
videojs__default["default"].registerComponent('licenseMenuButton', LicenseMenuButton);
this.player.getChild('controlBar').addChild('licenseMenuButton');
if (this.player.getChild('controlBar').getChild('fullscreenToggle')) {
this.player.getChild('controlBar').el().insertBefore(this.player.getChild('controlBar').getChild('licenseMenuButton').el(), this.player.getChild('controlBar').getChild('fullscreenToggle').el());
}
}
/**
*
* This is just build the top level menu no sub menus
*/
;
_proto.buildTopLevelMenu = function buildTopLevelMenu() {
var settingsButton = this.player.getChild('controlBar').getChild('licenseMenuButton'); // settingsButton.addClass('vjs-license-is-loaded');
var main = settingsButton.menu.contentEl_; // Empty the main menu div to repopulate
main.innerHTML = '';
main.classList.add('vjs-license-top-level'); // Start building new list items
var menuTitle = document.createElement('li');
menuTitle.className = 'vjs-license-top-level-header';
var menuTitleInner = document.createElement('span');
menuTitleInner.innerHTML = 'About';
menuTitleInner.className = 'vjs-license-top-level-header-titel';
menuTitle.appendChild(menuTitleInner);
main.appendChild(menuTitle);
var itemTitel = document.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');
itemAuthor.innerHTML = this.buildItemAuthor();
itemAuthor.className = 'vjs-license-top-level-item';
main.appendChild(itemAuthor);
}
var itemLicense = document.createElement('li');
itemLicense.innerHTML = this.buildItemLicense();
itemLicense.className = 'vjs-license-top-level-item';
main.appendChild(itemLicense);
}
/**
* Add the menu ui button to the controlbar
*/
;
_proto.mobileBuildUI = function mobileBuildUI() {
/**
* bla
*/
var LicenseMenuMobileModal = /*#__PURE__*/function (_Component) {
inheritsLoose(LicenseMenuMobileModal, _Component);
/**
* Contructor
*
* @param {*} player videojs player instance
* @param {*} options videojs player options
*/
function LicenseMenuMobileModal(player, options) {
return _Component.call(this, player, options) || this;
}
/**
* Creates an HTML element
*
* @return {Object} HTML element
*/
var _proto3 = LicenseMenuMobileModal.prototype;
_proto3.createEl = function createEl() {
return videojs__default["default"].createEl('div', {
className: 'vjs-license-mobile'
});
};
return LicenseMenuMobileModal;
}(Component);
videojs__default["default"].registerComponent('licenseMenuMobileModal', LicenseMenuMobileModal);
videojs__default["default"].dom.prependTo(this.player.addChild('licenseMenuMobileModal').el(), document.body);
}
/**
* Add the menu ui button to the controlbar
*/
;
_proto.mobileBuildTopLevelMenu = function mobileBuildTopLevelMenu() {
var _this3 = this;
var settingsButton = this.player.getChild('licenseMenuMobileModal');
var menuTopLevel = document.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');
menuTitle.innerHTML = 'About';
menuTitle.className = 'vjs-setting-menu-mobile-top-header';
menuTopLevel.appendChild(menuTitle);
var itemTitel = document.createElement('li');
itemTitel.innerHTML = this.buildItemTitel();
itemTitel.className = 'vjs-license-top-level-item';
if (this.options.author) {
var itemAuthor = document.createElement('li');
itemAuthor.innerHTML = this.buildItemAuthor();
itemAuthor.className = 'vjs-license-top-level-item';
}
var itemLicense = document.createElement('li');
itemLicense.innerHTML = this.buildItemLicense();
itemLicense.className = 'vjs-license-top-level-item';
var menuClose = document.createElement('li');
menuClose.innerHTML = 'Close';
menuClose.className = 'setting-menu-footer-default';
menuClose.onclick = function (e) {
_this3.player.getChild('settingsMenuMobileModal').el().style.display = 'none';
};
menuTopLevel.appendChild(menuClose);
}
/**
* Add the menu ui button to the controlbar
*
* @return {string} Returns license text
*/
;
_proto.buildItemTitel = function buildItemTitel() {
var titel = '';
if (this.options.title) {
titel = "" + this.options.title;
}
return 'Titel: ' + titel;
}
/**
* Add the menu ui button to the controlbar
*
* @return {string} Returns license text
*/
;
_proto.buildItemAuthor = function buildItemAuthor() {
var author = '';
if (this.options.author) {
author = " by " + this.options.author;
}
return 'Author: ' + author;
}
/**
* Add the menu ui button to the controlbar
*
* @return {string} Returns license text
*/
;
_proto.buildItemLicense = function buildItemLicense() {
var license = '';
var reVersion = new RegExp('[0-9]+.[0-9]+$');
var version = '4.0';
var matches = this.options.license.match(reVersion);
if (matches !== null) {
version = matches[0];
}
var which = this.options.license.replace(reVersion, '').trim();
var deed = null;
switch (which) {
case 'CC0':
deed = 'https://creativecommons.org/licenses/zero/1.0/';
break;
case 'CC BY':
deed = "https://creativecommons.org/licenses/by/" + version + "/";
break;
case 'CC BY-SA':
deed = "https://creativecommons.org/licenses/by-sa/" + version + "/";
break;
case 'CC BY-NC':
deed = "https://creativecommons.org/licenses/by-nc/" + version + "/";
break;
case 'CC BY-NC-SA':
deed = "https://creativecommons.org/licenses/by-nc-sa/" + version + "/";
break;
case 'CC BY-ND':
deed = "https://creativecommons.org/licenses/by-nd/" + version + "/";
break;
case 'CC BY-NC-ND':
deed = "https://creativecommons.org/licenses/by-nc-nd/" + version + "/";
break;
}
if (deed) {
license = "<a href=\"" + deed + "\" onclick=\"window.open('" + deed + "')\" target=\"_blank\" rel=\"noopener\">" + this.options.license + "</a>";
} else {
license = this.options.license;
}
return 'License: ' + license;
}
/**
*
* Helper class to clear menu items before rebuild
*
* @param {*} className Name of a class
*/
;
_proto.removeElementsByClass = function removeElementsByClass(className) {
// Need to prevent the menu from not showing sometimes
document.querySelectorAll('.vjs-sm-top-level').forEach(function (element) {
element.classList.remove('vjs-hidden');
});
var elements = document.getElementsByClassName(className);
while (elements.length > 0) {
elements[0].parentNode.removeChild(elements[0]);
}
};
return License;
}(Plugin); // Define default values for the plugin's `state` object here.
License.defaultState = {}; // Include the version number.
License.VERSION = version; // Register the plugin with video.js.
videojs__default["default"].registerPlugin('license', License);
return License;
}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,386 @@
/*! @name videojs-overlay @version 2.1.5 @license Apache-2.0 */
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var videojs = _interopDefault(require('video.js'));
var window = _interopDefault(require('global/window'));
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
var version = "2.1.5";
var defaults = {
align: 'top-left',
class: '',
content: 'This overlay will show up while the video is playing',
debug: false,
showBackground: true,
attachToControlBar: false,
overlays: [{
start: 'playing',
end: 'paused'
}]
};
var Component = videojs.getComponent('Component');
var dom = videojs.dom || videojs;
var registerPlugin = videojs.registerPlugin || videojs.plugin;
/**
* Whether the value is a `Number`.
*
* Both `Infinity` and `-Infinity` are accepted, but `NaN` is not.
*
* @param {Number} n
* @return {Boolean}
*/
/* eslint-disable no-self-compare */
var isNumber = function isNumber(n) {
return typeof n === 'number' && n === n;
};
/* eslint-enable no-self-compare */
/**
* Whether a value is a string with no whitespace.
*
* @param {String} s
* @return {Boolean}
*/
var hasNoWhitespace = function hasNoWhitespace(s) {
return typeof s === 'string' && /^\S+$/.test(s);
};
/**
* Overlay component.
*
* @class Overlay
* @extends {videojs.Component}
*/
var Overlay =
/*#__PURE__*/
function (_Component) {
_inheritsLoose(Overlay, _Component);
function Overlay(player, options) {
var _this;
_this = _Component.call(this, player, options) || this;
['start', 'end'].forEach(function (key) {
var value = _this.options_[key];
if (isNumber(value)) {
_this[key + 'Event_'] = 'timeupdate';
} else if (hasNoWhitespace(value)) {
_this[key + 'Event_'] = value; // An overlay MUST have a start option. Otherwise, it's pointless.
} else if (key === 'start') {
throw new Error('invalid "start" option; expected number or string');
}
}); // video.js does not like components with multiple instances binding
// events to the player because it tracks them at the player level,
// not at the level of the object doing the binding. This could also be
// solved with Function.prototype.bind (but not videojs.bind because of
// its GUID magic), but the anonymous function approach avoids any issues
// caused by crappy libraries clobbering Function.prototype.bind.
// - https://github.com/videojs/video.js/issues/3097
['endListener_', 'rewindListener_', 'startListener_'].forEach(function (name$$1) {
_this[name$$1] = function (e) {
return Overlay.prototype[name$$1].call(_assertThisInitialized(_assertThisInitialized(_this)), e);
};
}); // If the start event is a timeupdate, we need to watch for rewinds (i.e.,
// when the user seeks backward).
if (_this.startEvent_ === 'timeupdate') {
_this.on(player, 'timeupdate', _this.rewindListener_);
}
_this.debug("created, listening to \"" + _this.startEvent_ + "\" for \"start\" and \"" + (_this.endEvent_ || 'nothing') + "\" for \"end\"");
_this.hide();
return _this;
}
var _proto = Overlay.prototype;
_proto.createEl = function createEl() {
var options = this.options_;
var content = options.content;
var background = options.showBackground ? 'vjs-overlay-background' : 'vjs-overlay-no-background';
var el = dom.createEl('div', {
className: "\n vjs-overlay\n vjs-overlay-" + options.align + "\n " + options.class + "\n " + background + "\n vjs-hidden\n "
});
if (typeof content === 'string') {
el.innerHTML = content;
} else if (content instanceof window.DocumentFragment) {
el.appendChild(content);
} else {
dom.appendContent(el, content);
}
return el;
};
/**
* Logs debug errors
* @param {...[type]} args [description]
* @return {[type]} [description]
*/
_proto.debug = function debug() {
if (!this.options_.debug) {
return;
}
var log = videojs.log;
var fn = log; // Support `videojs.log.foo` calls.
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
if (log.hasOwnProperty(args[0]) && typeof log[args[0]] === 'function') {
fn = log[args.shift()];
}
fn.apply(void 0, ["overlay#" + this.id() + ": "].concat(args));
};
/**
* Overrides the inherited method to perform some event binding
*
* @return {Overlay}
*/
_proto.hide = function hide() {
_Component.prototype.hide.call(this);
this.debug('hidden');
this.debug("bound `startListener_` to \"" + this.startEvent_ + "\""); // Overlays without an "end" are valid.
if (this.endEvent_) {
this.debug("unbound `endListener_` from \"" + this.endEvent_ + "\"");
this.off(this.player(), this.endEvent_, this.endListener_);
}
this.on(this.player(), this.startEvent_, this.startListener_);
return this;
};
/**
* Determine whether or not the overlay should hide.
*
* @param {Number} time
* The current time reported by the player.
* @param {String} type
* An event type.
* @return {Boolean}
*/
_proto.shouldHide_ = function shouldHide_(time, type) {
var end = this.options_.end;
return isNumber(end) ? time >= end : end === type;
};
/**
* Overrides the inherited method to perform some event binding
*
* @return {Overlay}
*/
_proto.show = function show() {
_Component.prototype.show.call(this);
this.off(this.player(), this.startEvent_, this.startListener_);
this.debug('shown');
this.debug("unbound `startListener_` from \"" + this.startEvent_ + "\""); // Overlays without an "end" are valid.
if (this.endEvent_) {
this.debug("bound `endListener_` to \"" + this.endEvent_ + "\"");
this.on(this.player(), this.endEvent_, this.endListener_);
}
return this;
};
/**
* Determine whether or not the overlay should show.
*
* @param {Number} time
* The current time reported by the player.
* @param {String} type
* An event type.
* @return {Boolean}
*/
_proto.shouldShow_ = function shouldShow_(time, type) {
var start = this.options_.start;
var end = this.options_.end;
if (isNumber(start)) {
if (isNumber(end)) {
return time >= start && time < end; // In this case, the start is a number and the end is a string. We need
// to check whether or not the overlay has shown since the last seek.
} else if (!this.hasShownSinceSeek_) {
this.hasShownSinceSeek_ = true;
return time >= start;
} // In this case, the start is a number and the end is a string, but
// the overlay has shown since the last seek. This means that we need
// to be sure we aren't re-showing it at a later time than it is
// scheduled to appear.
return Math.floor(time) === start;
}
return start === type;
};
/**
* Event listener that can trigger the overlay to show.
*
* @param {Event} e
*/
_proto.startListener_ = function startListener_(e) {
var time = this.player().currentTime();
if (this.shouldShow_(time, e.type)) {
this.show();
}
};
/**
* Event listener that can trigger the overlay to show.
*
* @param {Event} e
*/
_proto.endListener_ = function endListener_(e) {
var time = this.player().currentTime();
if (this.shouldHide_(time, e.type)) {
this.hide();
}
};
/**
* Event listener that can looks for rewinds - that is, backward seeks
* and may hide the overlay as needed.
*
* @param {Event} e
*/
_proto.rewindListener_ = function rewindListener_(e) {
var time = this.player().currentTime();
var previous = this.previousTime_;
var start = this.options_.start;
var end = this.options_.end; // Did we seek backward?
if (time < previous) {
this.debug('rewind detected'); // The overlay remains visible if two conditions are met: the end value
// MUST be an integer and the the current time indicates that the
// overlay should NOT be visible.
if (isNumber(end) && !this.shouldShow_(time)) {
this.debug("hiding; " + end + " is an integer and overlay should not show at this time");
this.hasShownSinceSeek_ = false;
this.hide(); // If the end value is an event name, we cannot reliably decide if the
// overlay should still be displayed based solely on time; so, we can
// only queue it up for showing if the seek took us to a point before
// the start time.
} else if (hasNoWhitespace(end) && time < start) {
this.debug("hiding; show point (" + start + ") is before now (" + time + ") and end point (" + end + ") is an event");
this.hasShownSinceSeek_ = false;
this.hide();
}
}
this.previousTime_ = time;
};
return Overlay;
}(Component);
videojs.registerComponent('Overlay', Overlay);
/**
* Initialize the plugin.
*
* @function plugin
* @param {Object} [options={}]
*/
var plugin = function plugin(options) {
var _this2 = this;
var settings = videojs.mergeOptions(defaults, options); // De-initialize the plugin if it already has an array of overlays.
if (Array.isArray(this.overlays_)) {
this.overlays_.forEach(function (overlay) {
_this2.removeChild(overlay);
if (_this2.controlBar) {
_this2.controlBar.removeChild(overlay);
}
overlay.dispose();
});
}
var overlays = settings.overlays; // We don't want to keep the original array of overlay options around
// because it doesn't make sense to pass it to each Overlay component.
delete settings.overlays;
this.overlays_ = overlays.map(function (o) {
var mergeOptions = videojs.mergeOptions(settings, o);
var attachToControlBar = typeof mergeOptions.attachToControlBar === 'string' || mergeOptions.attachToControlBar === true;
if (!_this2.controls() || !_this2.controlBar) {
return _this2.addChild('overlay', mergeOptions);
}
if (attachToControlBar && mergeOptions.align.indexOf('bottom') !== -1) {
var referenceChild = _this2.controlBar.children()[0];
if (_this2.controlBar.getChild(mergeOptions.attachToControlBar) !== undefined) {
referenceChild = _this2.controlBar.getChild(mergeOptions.attachToControlBar);
}
if (referenceChild) {
var referenceChildIndex = _this2.controlBar.children().indexOf(referenceChild);
var controlBarChild = _this2.controlBar.addChild('overlay', mergeOptions, referenceChildIndex);
return controlBarChild;
}
}
var playerChild = _this2.addChild('overlay', mergeOptions);
_this2.el().insertBefore(playerChild.el(), _this2.controlBar.el());
return playerChild;
});
};
plugin.VERSION = version;
registerPlugin('overlay', plugin);
module.exports = plugin;

View File

@ -0,0 +1 @@
.video-js .vjs-overlay{color:#fff;position:absolute;text-align:center}.video-js .vjs-overlay-no-background{max-width:33%}.video-js .vjs-overlay-background{background-color:#646464;background-color:rgba(255,255,255,0.4);border-radius:3px;padding:10px;width:33%}.video-js .vjs-overlay-top-left{top:5px;left:5px}.video-js .vjs-overlay-top{left:50%;margin-left:-16.5%;top:5px}.video-js .vjs-overlay-top-right{right:5px;top:5px}.video-js .vjs-overlay-right{right:5px;top:50%;transform:translateY(-50%)}.video-js .vjs-overlay-bottom-right{bottom:3.5em;right:5px}.video-js .vjs-overlay-bottom{bottom:3.5em;left:50%;margin-left:-16.5%}.video-js .vjs-overlay-bottom-left{bottom:3.5em;left:5px}.video-js .vjs-overlay-left{left:5px;top:50%;transform:translateY(-50%)}.video-js .vjs-overlay-center{left:50%;margin-left:-16.5%;top:50%;transform:translateY(-50%)}.video-js .vjs-no-flex .vjs-overlay-left,.video-js .vjs-no-flex .vjs-overlay-center,.video-js .vjs-no-flex .vjs-overlay-right{margin-top:-15px}

View File

@ -0,0 +1,2 @@
/*! @name videojs-overlay @version 2.1.5 @license Apache-2.0 */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js"),require("global/window")):"function"==typeof define&&define.amd?define(["video.js","global/window"],e):t.videojsOverlay=e(t.videojs,t.window)}(this,function(t,e){"use strict";function n(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}t=t&&t.hasOwnProperty("default")?t.default:t,e=e&&e.hasOwnProperty("default")?e.default:e;var i={align:"top-left",class:"",content:"This overlay will show up while the video is playing",debug:!1,showBackground:!0,attachToControlBar:!1,overlays:[{start:"playing",end:"paused"}]},r=t.getComponent("Component"),o=t.dom||t,s=t.registerPlugin||t.plugin,a=function(t){return"number"==typeof t&&t==t},h=function(t){return"string"==typeof t&&/^\S+$/.test(t)},d=function(i){var r,s;function d(t,e){var r;return r=i.call(this,t,e)||this,["start","end"].forEach(function(t){var e=r.options_[t];if(a(e))r[t+"Event_"]="timeupdate";else if(h(e))r[t+"Event_"]=e;else if("start"===t)throw new Error('invalid "start" option; expected number or string')}),["endListener_","rewindListener_","startListener_"].forEach(function(t){r[t]=function(e){return d.prototype[t].call(n(n(r)),e)}}),"timeupdate"===r.startEvent_&&r.on(t,"timeupdate",r.rewindListener_),r.debug('created, listening to "'+r.startEvent_+'" for "start" and "'+(r.endEvent_||"nothing")+'" for "end"'),r.hide(),r}s=i,(r=d).prototype=Object.create(s.prototype),r.prototype.constructor=r,r.__proto__=s;var l=d.prototype;return l.createEl=function(){var t=this.options_,n=t.content,i=t.showBackground?"vjs-overlay-background":"vjs-overlay-no-background",r=o.createEl("div",{className:"\n vjs-overlay\n vjs-overlay-"+t.align+"\n "+t.class+"\n "+i+"\n vjs-hidden\n "});return"string"==typeof n?r.innerHTML=n:n instanceof e.DocumentFragment?r.appendChild(n):o.appendContent(r,n),r},l.debug=function(){if(this.options_.debug){for(var e=t.log,n=e,i=arguments.length,r=new Array(i),o=0;o<i;o++)r[o]=arguments[o];e.hasOwnProperty(r[0])&&"function"==typeof e[r[0]]&&(n=e[r.shift()]),n.apply(void 0,["overlay#"+this.id()+": "].concat(r))}},l.hide=function(){return i.prototype.hide.call(this),this.debug("hidden"),this.debug('bound `startListener_` to "'+this.startEvent_+'"'),this.endEvent_&&(this.debug('unbound `endListener_` from "'+this.endEvent_+'"'),this.off(this.player(),this.endEvent_,this.endListener_)),this.on(this.player(),this.startEvent_,this.startListener_),this},l.shouldHide_=function(t,e){var n=this.options_.end;return a(n)?t>=n:n===e},l.show=function(){return i.prototype.show.call(this),this.off(this.player(),this.startEvent_,this.startListener_),this.debug("shown"),this.debug('unbound `startListener_` from "'+this.startEvent_+'"'),this.endEvent_&&(this.debug('bound `endListener_` to "'+this.endEvent_+'"'),this.on(this.player(),this.endEvent_,this.endListener_)),this},l.shouldShow_=function(t,e){var n=this.options_.start,i=this.options_.end;return a(n)?a(i)?t>=n&&t<i:this.hasShownSinceSeek_?Math.floor(t)===n:(this.hasShownSinceSeek_=!0,t>=n):n===e},l.startListener_=function(t){var e=this.player().currentTime();this.shouldShow_(e,t.type)&&this.show()},l.endListener_=function(t){var e=this.player().currentTime();this.shouldHide_(e,t.type)&&this.hide()},l.rewindListener_=function(t){var e=this.player().currentTime(),n=this.previousTime_,i=this.options_.start,r=this.options_.end;e<n&&(this.debug("rewind detected"),a(r)&&!this.shouldShow_(e)?(this.debug("hiding; "+r+" is an integer and overlay should not show at this time"),this.hasShownSinceSeek_=!1,this.hide()):h(r)&&e<i&&(this.debug("hiding; show point ("+i+") is before now ("+e+") and end point ("+r+") is an event"),this.hasShownSinceSeek_=!1,this.hide())),this.previousTime_=e},d}(r);t.registerComponent("Overlay",d);var l=function(e){var n=this,r=t.mergeOptions(i,e);Array.isArray(this.overlays_)&&this.overlays_.forEach(function(t){n.removeChild(t),n.controlBar&&n.controlBar.removeChild(t),t.dispose()});var o=r.overlays;delete r.overlays,this.overlays_=o.map(function(e){var i=t.mergeOptions(r,e),o="string"==typeof i.attachToControlBar||!0===i.attachToControlBar;if(!n.controls()||!n.controlBar)return n.addChild("overlay",i);if(o&&-1!==i.align.indexOf("bottom")){var s=n.controlBar.children()[0];if(void 0!==n.controlBar.getChild(i.attachToControlBar)&&(s=n.controlBar.getChild(i.attachToControlBar)),s){var a=n.controlBar.children().indexOf(s);return n.controlBar.addChild("overlay",i,a)}}var h=n.addChild("overlay",i);return n.el().insertBefore(h.el(),n.controlBar.el()),h})};return l.VERSION="2.1.5",s("overlay",l),l});

View File

@ -0,0 +1,7 @@
dist/video.min.js
dist/video-js.min.css
dist/videojs-overlay.min.js
dist/videojs-overlay.min.css
dist/video-js-skin.min.css
dist/videojs-license.min.js
dist/videojs-license.min.css

View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{description}}">
<meta name="author" content="datarhei restreamer">
<title>{{name}}</title>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<link rel="alternate" type="application/json+oembed" href="channels/{{channelid}}/oembed.json" title="{{name}}">
<link rel="alternate" type="text/xml+oembed" href="channels/{{channelid}}/oembed.xml" title="{{name}}">
<script src="channels/{{channelid}}/config.js"></script>
<link href="player/videojs/dist/video-js.min.css" rel="stylesheet">
<link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet">
<style>
.player-poster[data-poster] .poster-background[data-poster] {
height: initial !important;
}
</style>
</head>
<body>
<div style="position:absolute;top:0;right:0;bottom:0;left:0">
<video id="player" class="vjs-public video-js" controls playsinline></video>
</div>
<script src="player/videojs/dist/video.min.js"></script>
<script src="player/videojs/dist/videojs-overlay.min.js"></script>
<script src="player/videojs/dist/videojs-license.min.js"></script>
<script>
function getQueryParam(key, defaultValue) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for(var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if(pair[0] == key) {
return pair[1];
}
}
return defaultValue;
}
function convertBoolParam(key, defaultValue) {
var val = getQueryParam(key, defaultValue);
return val === true || val === "true" || val === "1" || val === "yes" || val === "on";
}
function convertColorParam(parameter, defaultColor) {
var re = new RegExp("^#([0-9a-f]{3}|[0-9a-f]{6})$");
var c = getQueryParam(parameter, defaultColor);
// decode color as # has to be represented by %23
var c = decodeURIComponent(c);
// if color was given without leading #, prepend it
if (!String(c).startsWith("#")) c = "#" + c;
if (re.test(c)) {
return c;
} else {
return defaultColor;
}
}
var autoplay = convertBoolParam("autoplay", playerConfig.autoplay);
var mute = convertBoolParam("mute", playerConfig.mute);
var statistics = convertBoolParam("stats", playerConfig.statistics);
var color = convertColorParam("color", playerConfig.color.buttons);
var config = {
controls: true,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
autoplay: autoplay ? 'muted' : false,
muted: mute,
liveui: true,
responsive: true,
fluid: true,
sources: [{ src: playerConfig.source, type: 'application/x-mpegURL' }],
plugins: {
license: playerConfig.license
}
};
var player = videojs('player', config);
player.ready(function() {
if(playerConfig.logo.image.length != 0) {
var overlay = null;
var imgTag = new Image();
imgTag.onLoad = function () {
imgTag.setAttribute('width', this.width);
imgTag.setAttribute('height'.this.height);
};
imgTag.src = playerConfig.logo.image + '?' + Math.random();
if (playerConfig.logo.link.length !== 0) {
var aTag = document.createElement('a');
aTag.setAttribute('href', playerConfig.logo.link);
aTag.setAttribute('target', '_blank');
aTag.appendChild(imgTag);
overlay = aTag.outerHTML;
} else {
overlay = imgTag.outerHTML;
}
player.overlay({
align: playerConfig.logo.position,
overlays: [
{
showBackground: false,
content: overlay,
start: 'playing',
end: 'pause',
},
],
});
}
if (autoplay === true) {
// https://videojs.com/blog/autoplay-best-practices-with-video-js/
player.play();
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,10 @@
# Player Templates
Affected files: `index.html`.
The templates are interpreted with [handlebars](https://handlebarsjs.com/).
The following placeholders will be replaced by their respective value:
Placeholder | Description
------------|------------

View File

@ -0,0 +1,67 @@
var plugins = [];
if (statistics == true) {
plugins.push(ClapprNerdStats);
plugins.push(ClapprStats);
}
var config = {
source: playerConfig.source,
parentId: '#player',
baseUrl: 'clappr/',
persistConfig: false,
plugins: plugins,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
mediacontrol: {
seekbar: playerConfig.color.seekbar,
buttons: color,
},
height: '100%',
width: '100%',
disableCanAutoPlay: true,
autoPlay: autoplay,
mimeType: 'application/vnd.apple.mpegurl',
actualLiveTime: false,
exitFullscreenOnEnd: false,
mute: mute,
playback: {
controls: false,
playInline: true,
recycleVideo: Clappr.Browser.isMobile,
hlsjsConfig: {
enableWorker: false,
capLevelToPlayerSize: true,
capLevelOnFPSDrop: true,
maxBufferHole: 1,
highBufferWatchdogPeriod: 1,
},
},
hlsPlayback: {
preload: false,
},
visibilityEnableIcon: false,
clapprStats: {
runEach: 1000,
onReport: (metrics) => {},
},
clapprNerdStats: {
shortcut: ['command+shift+s', 'ctrl+shift+s'],
iconPosition: 'top-right',
},
};
if (playerConfig.logo.image.length != 0) {
config.watermark = playerConfig.logo.image;
config.position = playerConfig.logo.position;
if (playerConfig.logo.link.length != 0) {
config.watermarkLink = playerConfig.logo.link;
}
}
var player = new window.Clappr.Player(config);
var posterPlugin = player.core.mediaControl.container.getPlugin('poster');
player.on(window.Clappr.Events.PLAYER_STOP, function updatePoster() {
posterPlugin.options.poster = playerConfig.poster + '?t=' + String(new Date().getTime());
posterPlugin.render();
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,54 @@
var config = {
controls: true,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
autoplay: autoplay ? 'muted' : false,
muted: mute,
liveui: true,
responsive: true,
fluid: true,
sources: [{ src: playerConfig.source, type: 'application/x-mpegURL' }],
plugins: {
license: playerConfig.license,
},
};
var player = videojs('player', config);
player.ready(function () {
if (playerConfig.logo.image.length != 0) {
var overlay = null;
var imgTag = new Image();
imgTag.onLoad = function () {
imgTag.setAttribute('width', this.width);
imgTag.setAttribute('height'.this.height);
};
imgTag.src = playerConfig.logo.image + '?' + Math.random();
if (playerConfig.logo.link.length !== 0) {
var aTag = document.createElement('a');
aTag.setAttribute('href', playerConfig.logo.link);
aTag.setAttribute('target', '_blank');
aTag.appendChild(imgTag);
overlay = aTag.outerHTML;
} else {
overlay = imgTag.outerHTML;
}
player.overlay({
align: playerConfig.logo.position,
overlays: [
{
showBackground: false,
content: overlay,
start: 'playing',
end: 'pause',
},
],
});
}
if (autoplay === true) {
// https://videojs.com/blog/autoplay-best-practices-with-video-js/
player.play();
}
});

BIN
public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/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" />
<title>Restreamer</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "Restreamer",
"name": "Restreamer",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

413
src/Footer.js Normal file
View File

@ -0,0 +1,413 @@
import React from 'react';
import { isMobile } from 'react-device-detect';
import { Trans } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
//import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Popover from '@mui/material/Popover';
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 Duration from './misc/Duration';
import Logo from './misc/Logo';
import Number from './misc/Number';
const useStyles = makeStyles((theme) => ({
footer: {
zIndex: '2',
position: 'fixed',
bottom: 0,
width: '100%',
height: 60,
background: `-webkit-linear-gradient(left, ${theme.palette.background.footer1} 0, ${theme.palette.background.footer2} 100%)`,
color: theme.palette.text.secondary,
'& .footerLeft': {
textOverflow: 'ellipsis',
overflow: 'hidden !important',
whiteSpace: 'nowrap',
marginLeft: 40,
},
'& .footerRight': {
marginLeft: 20,
marginRight: 40,
},
'& .footerVersion': {
textOverflow: 'ellipsis',
overflow: 'hidden !important',
whiteSpace: 'nowrap',
},
'@media (max-width: 599px)': {
'& .footerLeft': {
marginRight: 20,
},
'& .footerRight': {
marginLeft: 20,
},
'& .footerVersion': {
display: 'none',
},
},
},
warningIcon: {
fontSize: '1.1rem',
marginTop: -1,
marginRight: 5,
},
subheader: {
color: `${theme.palette.service.main}`,
textTransform: 'uppercase',
fontWeight: 'bold',
},
}));
function Resources(props) {
const classes = useStyles();
const [$popover, setPopover] = React.useState(null);
const [$resources, setResources] = React.useState(null);
const handlePopoverOpen = (event) => {
setPopover(event.currentTarget);
};
const handlePopoverClose = () => {
setPopover(null);
};
const open = Boolean($popover);
useInterval(async () => {
await update();
}, 2000);
React.useEffect(() => {
(async () => {
await update();
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const update = async () => {
const resources = await props.resources();
if (resources === null) {
return;
}
resources.system.mem_used = (resources.system.mem_used_bytes / resources.system.mem_total_bytes) * 100;
resources.core.disk_used = -1;
if (resources.core.disk_limit_bytes > 0) {
resources.core.disk_used = (resources.core.disk_used_bytes / resources.core.disk_limit_bytes) * 100;
}
resources.core.memfs_used = -1;
if (resources.core.memfs_limit_bytes > 0) {
resources.core.memfs_used = (resources.core.memfs_used_bytes / resources.core.memfs_limit_bytes) * 100;
}
resources.core.net_used = -1;
if (resources.core.net_limit_kbit > 0) {
resources.core.net_used = (resources.core.net_used_kbit / resources.core.net_limit_kbit) * 100;
}
resources.core.sessions = -1;
if (resources.core.session_limit > 0) {
resources.core.sessions = (resources.core.session_used / resources.core.session_limit) * 100;
}
setResources(resources);
};
if ($resources === null) {
return null;
}
const system = $resources.system;
const core = $resources.core;
return (
<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" />
)}
<Typography variant="button" noWrap aria-owns={open ? 'mouse-over-popover' : undefined} aria-haspopup="true" onMouseOver={handlePopoverOpen}>
{system.cpu_used.toFixed(0)}% CPU / {system.mem_used.toFixed(0)}% Mem
</Typography>
<Popover
id="mouse-over-popover"
open={open}
onClose={handlePopoverClose}
anchorEl={$popover}
disableRestoreFocus
disableScrollLock
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
PaperProps={{
onMouseLeave: isMobile ? null : handlePopoverClose,
}}
>
<List
size="small"
subheader={
<ListSubheader className={classes.subheader}>
<Trans>Uptime</Trans>
</ListSubheader>
}
>
<ListItem divider>
<ListItemText
primary={
<Typography variant="body3">
<Duration seconds={$resources.uptime_seconds} />
</Typography>
}
secondary={``}
/>
</ListItem>
</List>
<List
size="small"
subheader={
<ListSubheader className={classes.subheader}>
<Trans>System</Trans>
</ListSubheader>
}
>
<ListItem divider selected={system.cpu_used >= 75}>
<ListItemText
primary={<Typography variant="body3">{system.cpu_used.toFixed(0)}% CPU</Typography>}
secondary={
<Typography variant="body2">
{system.cpu_ncores} <Trans>Cores</Trans>
</Typography>
}
/>
</ListItem>
<ListItem divider selected={system.mem_used >= 75}>
<ListItemText
primary={
<Typography variant="body3">
{system.mem_used.toFixed(0)}% <Trans>Memory</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={system.mem_used_bytes / 1024 / 1024} /> / <Number value={system.mem_total_bytes / 1024 / 1024} />{' '}
<Trans>MB</Trans>
</Typography>
}
/>
</ListItem>
</List>
<List
size="small"
subheader={
<ListSubheader className={classes.subheader}>
<Trans>Application</Trans>
</ListSubheader>
}
>
<ListItem divider>
{core.sessions >= 0 ? (
<ListItemText
primary={
<Typography variant="body3">
{core.sessions.toFixed(0)}% <Trans>Viewer</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.session_used} /> / <Number value={core.session_limit} /> <Trans>Viewer</Trans>
</Typography>
}
/>
) : (
<ListItemText
primary={
<Typography variant="body3">
<Trans>Sessions</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.session_used} /> <Trans>Viewer</Trans>
</Typography>
}
/>
)}
</ListItem>
<ListItem divider>
{core.net_used >= 0 ? (
<ListItemText
primary={
<Typography variant="body3">
{core.net_used.toFixed(0)}% <Trans>Bandwidth</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.net_used_kbit / 1024} /> / <Number value={core.net_limit_kbit / 1024} /> Mbit/s
</Typography>
}
/>
) : (
<ListItemText
primary={
<Typography variant="body3">
<Trans>Bandwidth</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.net_used_kbit / 1024} /> Mbit/s
</Typography>
}
/>
)}
</ListItem>
<ListItem divider>
{core.memfs_used >= 0 ? (
<ListItemText
primary={
<Typography variant="body3">
{core.memfs_used.toFixed(0)}% <Trans>In-memory storage</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.memfs_used_bytes / 1024 / 1024} /> / <Number value={core.memfs_limit_bytes / 1024 / 1024} /> MB
</Typography>
}
/>
) : (
<ListItemText
primary={
<Typography variant="body3">
<Trans>In-memory storage</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.memfs_used_bytes / 1024 / 1024} /> MB
</Typography>
}
/>
)}
</ListItem>
{/* <ListItem divider> */}
<ListItem>
{core.disk_used >= 0 ? (
<ListItemText
primary={
<Typography variant="body3">
{core.disk_used.toFixed(0)}% <Trans>Disk storage</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.disk_used_bytes / 1024 / 1024} /> / <Number value={core.disk_limit_bytes / 1024 / 1024} /> MB
</Typography>
}
/>
) : (
<ListItemText
primary={
<Typography variant="body3">
<Trans>Disk storage</Trans>
</Typography>
}
secondary={
<Typography variant="body2">
<Number value={core.disk_used_bytes / 1024 / 1024} /> MB
</Typography>
}
/>
)}
</ListItem>
{/* <ListItem divider>
<Button variant="service" color="primary" fullWidth size="large" component="a" href="https://service.datarhei.com" target="blank">
<Trans>More details</Trans>
</Button>
</ListItem> */}
</List>
</Popover>
</Stack>
);
}
Resources.defaultProps = {
resources: () => {
return null;
},
};
const initVersion = (initialVersion) => {
if (!initialVersion) {
initialVersion = {};
}
const version = {
number: 0,
arch: 'unknown',
...initialVersion,
};
return version;
};
export default function Footer(props) {
const classes = useStyles();
const version = initVersion(props.version);
if (props.expand === true) {
return (
<Grid container className={classes.footer} spacing={0} direction="row" alignItems="center">
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={0}>
<Stack className="footerLeft" direction="row" alignItems="center" spacing={0}>
<Logo className={classes.logo} />
<Typography className="footerVersion">
{props.app} v{version.number} ({version.arch}) {props.name ? '- ' + props.name : ''}
</Typography>
</Stack>
<Resources resources={props.resources} />
</Stack>
</Grid>
</Grid>
);
} else {
return (
<Grid container className={classes.footer} spacing={0} direction="row" alignItems="center">
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={0}>
<Stack className="footerLeft" direction="row" alignItems="center" spacing={0}>
<Logo className={classes.logo} />
</Stack>
</Stack>
</Grid>
</Grid>
);
}
}
Footer.defaultProps = {
expand: false,
app: '',
name: '',
version: initVersion(),
resources: () => {
return null;
},
};

388
src/Header.js Normal file
View File

@ -0,0 +1,388 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import { Trans } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import BugReportIcon from '@mui/icons-material/BugReport';
import Divider from '@mui/material/Divider';
import Fab from '@mui/material/Fab';
import Grid from '@mui/material/Grid';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import LayersIcon from '@mui/icons-material/Layers';
import Link from '@mui/material/Link';
import ListItemIcon from '@mui/material/ListItemIcon';
import Logout from '@mui/icons-material/Logout';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import Modal from '@mui/material/Modal';
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
import Settings from '@mui/icons-material/Settings';
import Stack from '@mui/material/Stack';
import TranslateIcon from '@mui/icons-material/Translate';
import Typography from '@mui/material/Typography';
import VideocamIcon from '@mui/icons-material/Videocam';
import WebIcon from '@mui/icons-material/Web';
import * as Storage from './utils/storage';
import * as Version from './version';
import welcomeImage from './assets/images/welcome.png';
import LanguageSelect from './misc/LanguageSelect';
import Logo from './misc/Logo/rsLogo';
import ModalContent from './misc/ModalContent';
import PaperThumb from './misc/PaperThumb';
const useStyles = makeStyles((theme) => ({
header: {
width: '100%',
height: 132,
lineHeight: '132px',
backgroundColor: 'transparent',
color: theme.palette.text.secondary,
'& .headerRight': {
float: 'right',
marginRight: 42,
},
'& .headerFab': {
height: 60,
width: 60,
marginLeft: '1em',
boxShadow: 'unset',
'& .fabIcon': {
fontSize: 30,
},
'&:hover': {
backgroundColor: theme.palette.background.box_default,
},
},
'& .headerFabHighlight': {
height: 60,
width: 60,
marginLeft: '1em',
boxShadow: 'unset',
border: `3px solid ${theme.palette.secondary.main}`,
'& .fabIcon': {
fontSize: 30,
},
'&:hover': {
backgroundColor: theme.palette.background.box_default,
},
},
'& .headerLeft': {
fontSize: '3.5rem',
fontWeight: 300,
marginLeft: 40,
},
'& .headerTitle': {
fontFamily: '"Dosis", "Roboto", "Helvetica", "Arial", sans-serif',
fontSize: '3rem',
fontWeight: 300,
marginLeft: 10,
marginBottom: '0.2em',
},
'@media (max-width: 599px)': {
'& .headerRight': {
marginRight: 20,
},
'& .headerLeft': {
marginLeft: 20,
},
'& .headerTitle': {
fontSize: '2.4rem',
},
},
'@media (max-width: 415px)': {
'& .headerRight': {
marginRight: 20,
},
'& .headerLeft': {
marginLeft: 20,
},
'& .headerTitle': {
display: 'none',
},
},
},
modalPaper: {
padding: '1em 1.5em 1.3em 1.5em',
width: '95%',
maxWidth: 350,
maxHeight: '95%',
overflow: 'scroll',
backgroundColor: theme.palette.background.modal,
color: theme.palette.text.primary,
},
aboutImage: {
paddingLeft: '1em',
},
colorHighlight: {
color: `${theme.palette.secondary.main}!important`,
},
}));
const StyledMenu = styled((props) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
{...props}
/>
))(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 5,
marginTop: theme.spacing(1),
minWidth: 180,
boxShadow:
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
'& .MuiMenu-list': {
padding: '4px 0',
backgroundColor: theme.palette.background.paper,
},
'& .MuiMenuItem-root': {
'& .MuiSvgIcon-root': {
fontSize: 18,
color: theme.palette.common.white,
marginRight: theme.spacing(1.5),
},
'&:active': {
backgroundColor: theme.palette.background.box_default,
},
'&:hover': {
backgroundColor: theme.palette.background.box_default,
},
},
},
}));
function AboutModal(props) {
const classes = useStyles();
return (
<Modal open={props.open} onClose={props.onClose} className="modal">
<ModalContent title="About datarhei Restreamer" onClose={props.onClose} className={classes.modalPaper}>
<Grid container spacing={1}>
<Grid item xs={12} className={classes.aboutImage}>
<PaperThumb image={welcomeImage} title="Welcome to Restreamer v2" height="200px" />
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
This is the frontend and a part of a free open source livestreaming solution for video data. The second part is the{' '}
<Link color="secondary" href="https://github.com/datarhei/core" target="_blank">
datarhei Core
</Link>{' '}
which can be operated separately.
</Typography>
</Grid>
<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>
</Typography>
<Typography>
<strong>Repo</strong>:{' '}
<Link color="secondary" target="_blank" href="https://github.com/datarhei/restreamer">
github.com/datarhei/restreamer
</Link>
</Typography>
<Typography>
<strong>Licence</strong>:{' '}
<Link color="secondary" target="_blank" href="https://github.com/datarhei/restreamer/blob/master/LICENSE">
Apache License 2.0
</Link>
</Typography>
<Typography>
<strong>Donation</strong>:{' '}
<Link color="secondary" target="_blank" href="https://patreon.com/datarhei/">
patreon.com/datarhei
</Link>
</Typography>
<Typography>
<strong>Website</strong>:{' '}
<Link color="secondary" target="_blank" href="https://datarhei.com">
datarhei.com
</Link>
</Typography>
</Grid>
</Grid>
</ModalContent>
</Modal>
);
}
AboutModal.defaultProps = {
open: false,
onClose: () => {},
};
function HeaderMenu(props) {
const classes = useStyles();
const [$anchorEl, setAnchorEl] = React.useState(null);
const [$about, setAbout] = React.useState(false);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleLanguageChange = (language) => {
Storage.Set('language', language);
};
if (props.expand === true) {
return (
<React.Fragment>
<Fab className="headerFab" color="primary" onClick={props.onChannel}>
<VideocamIcon className="fabIcon" />
</Fab>
<Fab className={props.hasUpdates ? 'headerFabHighlight' : 'headerFab'} color="primary" onClick={handleMenuOpen}>
<MenuOpenIcon className="fabIcon" />
</Fab>
<StyledMenu anchorEl={$anchorEl} open={$anchorEl !== null} onClose={handleMenuClose} onClick={handleMenuClose} disableScrollLock>
{props.hasService === true && (
<React.Fragment>
<MenuItem component="a" href="https://service.datarhei.com" target="blank">
<ListItemIcon>
<LayersIcon fontSize="small" />
</ListItemIcon>
<Trans>Service</Trans>
</MenuItem>
<Divider />
</React.Fragment>
)}
{props.showPlayersite === true && (
<MenuItem onClick={props.onPlayersite}>
<ListItemIcon size="large">
<WebIcon fontSize="small" size="large" />
</ListItemIcon>
<Trans>Playersite</Trans>
</MenuItem>
)}
{props.showSettings === true && (
<MenuItem onClick={props.onSettings}>
<ListItemIcon>
<Settings fontSize="small" className={props.hasUpdates ? classes.colorHighlight : ''} />
</ListItemIcon>
<Trans>System</Trans>
</MenuItem>
)}
<Divider />
<MenuItem onClick={() => setAbout(true)}>
<ListItemIcon>
<RocketLaunchIcon fontSize="small" />
</ListItemIcon>
<Trans>About</Trans>
</MenuItem>
<MenuItem component="a" href="https://docs.datarhei.com/restreamer" target="blank">
<ListItemIcon>
<HelpOutlineIcon fontSize="small" />
</ListItemIcon>
<Trans>Docs</Trans>
</MenuItem>
<MenuItem component="a" href="https://github.com/datarhei/restreamer/issues" target="blank">
<ListItemIcon>
<BugReportIcon fontSize="small" />
</ListItemIcon>
<Trans>Issue alert</Trans>
</MenuItem>
<MenuItem>
<ListItemIcon>
<TranslateIcon fontSize="small" />
</ListItemIcon>
<LanguageSelect onChange={handleLanguageChange} />
</MenuItem>
<MenuItem onClick={props.onLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
<Trans>Logout</Trans>
</MenuItem>
</StyledMenu>
<AboutModal open={$about} onClose={() => setAbout(false)} />
</React.Fragment>
);
} else {
return (
<React.Fragment>
<Fab className="headerFab" color="primary" onClick={handleMenuOpen}>
<MenuOpenIcon className="fabIcon" />
</Fab>
<StyledMenu anchorEl={$anchorEl} open={$anchorEl !== null} onClose={handleMenuClose} onClick={handleMenuClose}>
<MenuItem onClick={() => setAbout(true)}>
<ListItemIcon>
<RocketLaunchIcon fontSize="small" />
</ListItemIcon>
<Trans>About</Trans>
</MenuItem>
<MenuItem component="a" href="https://docs.datarhei.com/restreamer" target="blank">
<ListItemIcon>
<HelpOutlineIcon fontSize="small" />
</ListItemIcon>
<Trans>Docs</Trans>
</MenuItem>
<MenuItem component="a" href="https://github.com/datarhei/restreamer/issues" target="blank">
<ListItemIcon>
<BugReportIcon fontSize="small" />
</ListItemIcon>
<Trans>Issue alert</Trans>
</MenuItem>
<MenuItem>
<ListItemIcon>
<TranslateIcon fontSize="small" />
</ListItemIcon>
<LanguageSelect onChange={handleLanguageChange} />
</MenuItem>
</StyledMenu>
<AboutModal open={$about} onClose={() => setAbout(false)} />
</React.Fragment>
);
}
}
HeaderMenu.defaultProps = {
onChannel: () => {},
onPlayersite: () => {},
onSettings: () => {},
onLogout: () => {},
expand: false,
showPlayersite: false,
showSettings: false,
hasUpdates: false,
hasService: false,
};
export default function Header(props) {
const classes = useStyles();
return (
<Grid container className={classes.header} spacing={0} direction="row" alignItems="center">
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={0}>
<Stack direction="row" alignItems="center" spacing={0} className="headerLeft">
<Logo className="fabIcon" />
<Typography className="headerTitle">Restreamer</Typography>
</Stack>
<Stack className="headerRight" direction="row" alignItems="center" spacing={0}>
<HeaderMenu {...props}></HeaderMenu>
</Stack>
</Stack>
</Grid>
</Grid>
);
}
Header.defaultProps = {
expand: false,
};

60
src/I18n.js Normal file
View File

@ -0,0 +1,60 @@
import React from 'react';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import * as plurals from 'make-plural/plurals';
import { messages as EN } from './locales/en/messages.js';
import { messages as DE } from './locales/de/messages.js';
import { messages as 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 * as Storage from './utils/storage';
i18n.loadLocaleData('en', { plurals: plurals.en });
i18n.loadLocaleData('de', { plurals: plurals.de });
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.load({
en: EN,
de: DE,
fr: FR,
it: IT,
pt: PT,
es: ES,
});
const getLanguage = (defaultLanguage, supportedLanguages) => {
let lang = Storage.Get('language');
if (supportedLanguages.indexOf(lang) === -1) {
lang = getBrowserLanguage(defaultLanguage);
}
if (supportedLanguages.indexOf(lang) === -1) {
lang = defaultLanguage;
}
Storage.Set('language', lang);
return lang;
};
const getBrowserLanguage = (defaultLanguage) => {
let lang = window.navigator.language;
const match = lang.match(/^[a-z]+/);
if (!match) {
return defaultLanguage;
}
return match[0].toLowerCase();
};
i18n.activate(getLanguage('en', ['en', 'de', 'fr', 'it', 'pt', 'es']));
export default function Provider(props) {
return <I18nProvider i18n={i18n}>{props.children}</I18nProvider>;
}

438
src/RestreamerUI.js Normal file
View File

@ -0,0 +1,438 @@
import React from 'react';
import makeStyles from '@mui/styles/makeStyles';
import Alert from '@mui/material/Alert';
import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import Snackbar from '@mui/material/Snackbar';
import { NotifyProvider } from './contexts/Notify';
import * as auth0 from './utils/auth0';
import useInterval from './hooks/useInterval';
import ChannelList from './misc/ChannelList';
import Footer from './Footer';
import I18n from './I18n';
import Header from './Header';
import Restreamer from './utils/restreamer';
import Router from './Router';
import Views from './views';
const useStyles = makeStyles((theme) => ({
MainHeader: {
height: '132px',
},
// todo: one layer
MainContent: {
height: '100%',
'& .MainContent-container': {
minHeight: 'calc(100vh - 230px)',
},
'& .MainContent-item': {
maxWidth: '980px',
},
},
}));
export default function RestreamerUI(props) {
const classes = useStyles();
const [$state, setState] = React.useState({
initialized: false,
valid: false,
connected: false,
compatibility: { compatible: false },
ingest: false,
password: false,
updates: false,
service: false,
});
const [$ready, setReady] = React.useState(false);
const [$snack, setSnack] = React.useState({
open: false,
message: '',
severity: 'info',
});
const [$channelList, setChannelList] = React.useState({
open: false,
channelid: '',
channels: [],
});
const restreamer = React.useRef(null);
React.useEffect(() => {
(async () => {
await handleMount();
})();
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useInterval(() => {
setState({
...$state,
updates: restreamer.current.HasUpdates(),
});
}, 1000 * 60);
const notify = (severity, type, message) => {
setSnack({
...$snack,
open: true,
message: message,
severity: severity,
});
if (severity === 'success') {
if (type === 'save:ingest') {
setState({
...$state,
ingest: true,
});
}
} else if (severity === 'error') {
if (type === 'network') {
setState({
...$state,
initialized: true,
valid: false,
});
} else if (type === 'auth') {
(async () => {
await handleLogout();
})();
}
}
};
const handleMount = async () => {
restreamer.current = new Restreamer(props.address);
restreamer.current.AddListener((event) => {
notify(event.severity, event.type, event.message);
});
// Try if there's still an auth0 session
if (auth0.init() === true) {
if (await auth0.isAuthenticated()) {
const token = await auth0.getToken();
await restreamer.current.LoginWithToken(token);
} else {
const result = await auth0.handleRedirectCallback();
if (result.initialized === true) {
if (result.error === true) {
notify('error', 'auth0', 'Auth0: ' + result.description);
}
}
}
}
const valid = await restreamer.current.Validate();
setState({
...$state,
initialized: true,
valid: valid,
connected: restreamer.current.IsConnected(),
compatibility: restreamer.current.Compatibility(),
ingest: restreamer.current.HasIngest(),
password: restreamer.current.Auths().length === 0 && !restreamer.current.ConfigOverrides('api.auth.enable'),
updates: restreamer.current.HasUpdates(),
service: restreamer.current.HasService(),
});
setReady(true);
};
const handleLogin = async (username, password) => {
const connected = await restreamer.current.Login(username, password);
setState({
...$state,
connected: connected,
compatibility: restreamer.current.Compatibility(),
ingest: restreamer.current.HasIngest(),
});
return connected;
};
const handleAuth0 = async () => {
const token = await auth0.getToken();
const connected = await restreamer.current.LoginWithToken(token);
setState({
...$state,
connected: connected,
compatibility: restreamer.current.Compatibility(),
});
};
const handleLogout = async () => {
setState({
...$state,
initialized: false,
connected: false,
});
restreamer.current.Logout();
if (await auth0.isAuthenticated()) {
await auth0.logout();
}
restreamer.current.Reset();
const valid = await restreamer.current.Validate();
setState({
...$state,
initialized: true,
valid: valid,
connected: restreamer.current.IsConnected(),
compatibility: restreamer.current.Compatibility(),
ingest: restreamer.current.HasIngest(),
});
};
const handlePasswordReset = async (username, password) => {
const [, err] = await restreamer.current.ConfigSet({
api: {
auth: {
enable: true,
username: username,
password: password,
},
},
});
if (err !== null) {
notify('error', 'save:settings', `There was an error resetting the password.`);
return 'ERROR';
}
const res = await restreamer.current.ConfigReload();
if (res === false) {
notify('error', 'restart', `Restarting the application failed.`);
return 'ERROR';
}
restreamer.current.IgnoreAPIErrors(true);
const waitFor = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
let restarted = false;
const key = restreamer.current.CreatedAt().toISOString();
for (let retries = 0; retries <= 60; retries++) {
await waitFor(1000);
const about = await restreamer.current.About();
if (about === null) {
// Restarted API not yet available
continue;
}
if (about.created_at?.toISOString() === key) {
// API did not yet restart
continue;
}
restarted = true;
break;
}
if (restarted === true) {
// After the restart the API requires a login and this means the restart happened
await restreamer.current.Validate();
await restreamer.current.Login(username, password);
window.location.reload();
} else {
return 'TIMEOUT';
}
return 'OK';
};
const handlePlayersite = () => {
document.location.hash = '#/playersite';
};
const handleSettings = () => {
document.location.hash = '#/settings';
};
const handleChannelList = () => {
const channelid = restreamer.current.GetCurrentChannelID();
const channels = restreamer.current.ListChannels();
setChannelList({
...$channelList,
open: true,
channelid: channelid,
channels: channels,
});
};
const handleSelectChannel = (channelid) => {
restreamer.current.SelectChannel(channelid);
handleChannelList();
document.location.hash = `#/${channelid}`;
};
const handleCloseChannelList = () => {
setChannelList({
...$channelList,
open: false,
});
};
const handleAddChannel = (name) => {
const channelid = restreamer.current.CreateChannel(name);
restreamer.current.SelectChannel(channelid);
setChannelList({
...$channelList,
open: false,
});
document.location.hash = `#/${channelid}/edit/wizard`;
};
const handleStateChannel = async (channelids) => {
const processes = await restreamer.current.ListProcesses(['state'], channelids);
const states = {};
for (let p of processes) {
states[p.id] = p.progress.state;
}
return states;
};
const handleCloseSnack = () => {
setSnack({
...$snack,
open: false,
});
};
const handleResources = async () => {
return await restreamer.current.Resources();
};
if ($ready === false) {
return (
<Backdrop open={true}>
<CircularProgress color="inherit" />
</Backdrop>
);
}
let version = {};
let app = '';
let name = '';
if ($state.initialized === true) {
version = restreamer.current.Version();
app = restreamer.current.App();
name = restreamer.current.Name();
}
let resources = () => {
return null;
};
let view = <Views.Initializing />;
if ($state.valid === false) {
view = <Views.Invalid address={restreamer.current.Address()} />;
} else if ($state.connected === false) {
view = (
<Views.Login
onLogin={handleLogin}
auths={restreamer.current.Auths()}
hasService={$state.service}
address={restreamer.current.Address()}
onAuth0={handleAuth0}
/>
);
} else if ($state.compatibility.compatible === false) {
if ($state.compatibility.core.compatible === false) {
view = <Views.Incompatible type="core" have={$state.compatibility.core.have} want={$state.compatibility.core.want} />;
} else if ($state.compatibility.ffmpeg.compatible === false) {
view = <Views.Incompatible type="ffmpeg" have={$state.compatibility.ffmpeg.have} want={$state.compatibility.ffmpeg.want} />;
}
} else if ($state.password === true) {
view = <Views.Password onReset={handlePasswordReset} />;
} else {
view = <Router restreamer={restreamer.current} />;
resources = handleResources;
}
const expand = $state.connected && $state.compatibility.compatible && !$state.password;
return (
<I18n>
<NotifyProvider value={{ Dispatch: notify }}>
<Grid container direction="column" justifyContent="flex-start" alignItems="stretch" spacing={0}>
<Grid className={classes.MainHeader}>
<Header
expand={expand}
showPlayersite={$state.ingest}
showSettings={$state.compatibility.compatible}
hasUpdates={$state.updates}
hasService={$state.service}
onChannel={handleChannelList}
onPlayersite={handlePlayersite}
onSettings={handleSettings}
onLogout={handleLogout}
/>
</Grid>
<Grid item className={classes.MainContent}>
<Grid container className="MainContent-container" justifyContent="center" alignItems="center" spacing={0}>
<Grid item sm={1}></Grid>
<Grid item xs={12} sm={10} className="MainContent-item">
{view}
</Grid>
<Grid item sm={1}></Grid>
</Grid>
</Grid>
</Grid>
<Footer expand={$state.connected} app={app} version={version} name={name} resources={resources} />
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={$snack.open}
autoHideDuration={6000}
onClose={handleCloseSnack}
>
<Alert variant="filled" elevation={6} onClose={handleCloseSnack} severity={$snack.severity}>
{$snack.message}
</Alert>
</Snackbar>
{expand && (
<ChannelList
open={$channelList.open}
channels={$channelList.channels}
channelid={$channelList.channelid}
onClose={handleCloseChannelList}
onClick={handleSelectChannel}
onAdd={handleAddChannel}
onState={handleStateChannel}
/>
)}
</NotifyProvider>
</I18n>
);
}
RestreamerUI.defaultProps = {
address: '',
};

35
src/Router.js Normal file
View File

@ -0,0 +1,35 @@
import React from 'react';
import { Route, Navigate, Routes, HashRouter } from 'react-router-dom';
import Views from './views';
export default function Router(props) {
if (props.restreamer === null) {
return null;
}
const channelid = props.restreamer.GetCurrentChannelID();
return (
<HashRouter>
<Routes>
<Route path="/" element={<Views.ChannelSelect restreamer={props.restreamer} />} />
<Route path="/playersite" element={<Views.Playersite restreamer={props.restreamer} />} />
<Route path="/settings" element={<Views.Settings restreamer={props.restreamer} />} />
<Route path="/settings/:tab" element={<Views.Settings restreamer={props.restreamer} />} />
<Route path="/:channelid" element={<Views.Main key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/edit" element={<Views.Edit key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/edit/wizard" element={<Views.Wizard key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/edit/:tab" element={<Views.Edit key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/publication" element={<Views.AddService key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/publication/player" element={<Views.EditPlayer key={channelid} restreamer={props.restreamer} />} />
<Route path="/:channelid/publication/:service/:index" element={<Views.EditService key={channelid} restreamer={props.restreamer} />} />
<Route path="*" render={() => <Navigate to="/" replace />} />
</Routes>
</HashRouter>
);
}
Router.defaultProps = {
restreamer: null,
};

View File

@ -0,0 +1,3 @@
https://de.freepik.com/vektoren-kostenlos/weltraum-mission-konzept_2873583.htm
https://de.freepik.com/vektoren-kostenlos/vintage-monochrome-raumetiketten-mit-raumschiffen-ufo-astronauten-raketenantenne-helm-wissenschaftliche-station-kometen-meteore-isoliert_10056106.htm
https://de.freepik.com/vektoren-kostenlos/vintage-farbige-raumplakate-mit-raumschiffen-ufo-planeten-astronauten-asteroiden-mars-kolonisation-und-forschung-isoliert_9647352.htm

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

10
src/contexts/Notify.js Normal file
View File

@ -0,0 +1,10 @@
import React from 'react';
const NotifyContext = React.createContext({
Dispatch: () => {},
});
export const NotifyProvider = NotifyContext.Provider;
export const NotifyConsumer = NotifyContext.Consumer;
export default NotifyContext;

24
src/hooks/useInterval.js Normal file
View File

@ -0,0 +1,24 @@
import React from 'react';
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
export default function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

27
src/index.js Normal file
View File

@ -0,0 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
import '@fontsource/dosis';
import '@fontsource/roboto';
import CssBaseline from '@mui/material/CssBaseline';
import theme from './theme';
import RestreamerUI from './RestreamerUI';
let address = window.location.protocol + '//' + window.location.host;
const urlParams = new URLSearchParams(window.location.search.substring(1));
if (urlParams.has('address') === true) {
address = urlParams.get('address');
}
ReactDOM.render(
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
<RestreamerUI address={address} />
</ThemeProvider>
</StyledEngineProvider>,
document.getElementById('root')
);

View File

@ -0,0 +1,5 @@
{
"Settings": {
"origin": [["src/Footer.js", 14]]
}
}

View File

@ -0,0 +1,17 @@
{
"Connecting ...": {
"origin": [["src/misc/ActionButton.js", 11]]
},
"Disconnecting ...": {
"origin": [["src/misc/ActionButton.js", 14]]
},
"Disconnect": {
"origin": [["src/misc/ActionButton.js", 17]]
},
"Connect": {
"origin": [["src/misc/ActionButton.js", 20]]
},
"Reconnect": {
"origin": [["src/misc/ActionButton.js", 23]]
}
}

View File

@ -0,0 +1,17 @@
{
"Uptime": {
"origin": [["src/misc/Progress.js", 31]]
},
"FPS": {
"origin": [["src/misc/Progress.js", 38]]
},
"kbit/s": {
"origin": [["src/misc/Progress.js", 45]]
},
"Quality": {
"origin": [["src/misc/Progress.js", 52]]
},
"Speed": {
"origin": [["src/misc/Progress.js", 59]]
}
}

View File

@ -0,0 +1,8 @@
{
"Segment length (seconds)": {
"origin": [["src/misc/controls/HLS.js", 32]]
},
"List size (segments)": {
"origin": [["src/misc/controls/HLS.js", 35]]
}
}

View File

@ -0,0 +1,14 @@
{
"Reconnect Delay (seconds)": {
"origin": [["src/misc/controls/Process.js", 41]]
},
"Stale Timeout (seconds)": {
"origin": [["src/misc/controls/Process.js", 44]]
},
"Autostart": {
"origin": [["src/misc/controls/Process.js", 47]]
},
"Reconnect": {
"origin": [["src/misc/controls/Process.js", 50]]
}
}

View File

@ -0,0 +1,8 @@
{
"Enable Snapshots": {
"origin": [["src/misc/controls/Snapshot.js", 39]]
},
"Interval (seconds)": {
"origin": [["src/misc/controls/Snapshot.js", 42]]
}
}

View File

@ -0,0 +1,8 @@
{
"Debug Log": {
"origin": [["src/misc/modals/Debug.js", 22]]
},
"Close": {
"origin": [["src/misc/modals/Debug.js", 34]]
}
}

View File

@ -0,0 +1,8 @@
{
"Process Details": {
"origin": [["src/misc/modals/Process.js", 31]]
},
"Close": {
"origin": [["src/misc/modals/Process.js", 53]]
}
}

View File

@ -0,0 +1,11 @@
{
"Bitrate": {
"origin": [["src/views/Edit/Coders/settings/Audio.js", 13]]
},
"Layout": {
"origin": [["src/views/Edit/Coders/settings/Audio.js", 30]]
},
"Sampling": {
"origin": [["src/views/Edit/Coders/settings/Audio.js", 44]]
}
}

View File

@ -0,0 +1,20 @@
{
"Preset": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 13]]
},
"Bitrate": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 33]]
},
"FPS": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 55]]
},
"Profile": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 73]]
},
"Tune": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 88]]
},
"Entropy Coder": {
"origin": [["src/views/Edit/Coders/settings/Video.js", 106]]
}
}

View File

@ -0,0 +1,8 @@
{
"Video:": {
"origin": [["src/views/Edit/Profile.js", 40]]
},
"Audio:": {
"origin": [["src/views/Edit/Profile.js", 44]]
}
}

View File

@ -0,0 +1,11 @@
{
"Codec": {
"origin": [
["src/views/Edit/SelectStream.js", 139],
["src/views/Edit/SelectStream.js", 193]
]
},
"Stream": {
"origin": [["src/views/Edit/SelectStream.js", 216]]
}
}

View File

@ -0,0 +1,38 @@
{
"Source": {
"origin": [["src/views/Edit/Source.js", 237]]
},
"Select your source:": {
"origin": [["src/views/Edit/Source.js", 244]]
},
"Network Device": {
"origin": [["src/views/Edit/Source.js", 253]]
},
"USB Device": {
"origin": [["src/views/Edit/Source.js", 263]]
},
"ALSA": {
"origin": [["src/views/Edit/Source.js", 273]]
},
"AVFoundation": {
"origin": [["src/views/Edit/Source.js", 283]]
},
"Virtual Device": {
"origin": [["src/views/Edit/Source.js", 293]]
},
"Show probe details": {
"origin": [["src/views/Edit/Source.js", 304]]
},
"Save Source": {
"origin": [["src/views/Edit/Source.js", 321]]
},
"Abort": {
"origin": [["src/views/Edit/Source.js", 324]]
},
"Probe Details": {
"origin": [["src/views/Edit/Source.js", 339]]
},
"Close": {
"origin": [["src/views/Edit/Source.js", 351]]
}
}

View File

@ -0,0 +1,17 @@
{
"Network Device": {
"origin": [["src/views/Edit/SourceList.js", 51]]
},
"Video4Linux": {
"origin": [["src/views/Edit/SourceList.js", 54]]
},
"ALSA": {
"origin": [["src/views/Edit/SourceList.js", 57]]
},
"AVFoundation": {
"origin": [["src/views/Edit/SourceList.js", 60]]
},
"Virtual Device": {
"origin": [["src/views/Edit/SourceList.js", 63]]
}
}

View File

@ -0,0 +1,26 @@
{
"Audio Device": {
"origin": [["src/views/Edit/Sources/ALSA.js", 85]]
},
"Select a device:": {
"origin": [["src/views/Edit/Sources/ALSA.js", 94]]
},
"Device": {
"origin": [["src/views/Edit/Sources/ALSA.js", 104]]
},
"Subdevice": {
"origin": [["src/views/Edit/Sources/ALSA.js", 107]]
},
"Sampling": {
"origin": [["src/views/Edit/Sources/ALSA.js", 114]]
},
"Channels": {
"origin": [["src/views/Edit/Sources/ALSA.js", 117]]
},
"Delay (ms)": {
"origin": [["src/views/Edit/Sources/ALSA.js", 120]]
},
"Probe": {
"origin": [["src/views/Edit/Sources/ALSA.js", 125]]
}
}

View File

@ -0,0 +1,35 @@
{
"Video device": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 104]]
},
"Audio device": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 121]]
},
"Select a device:": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 130]]
},
"Custom audio index": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 139]]
},
"Custom video index": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 151]]
},
"Format": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 157]]
},
"Framerate": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 160]]
},
"Size": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 163]]
},
"Capture cursor": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 166]]
},
"Capture clicks": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 169]]
},
"Probe": {
"origin": [["src/views/Edit/Sources/AVFoundation.js", 174]]
}
}

View File

@ -0,0 +1,32 @@
{
"Enter the URL of your network device:": {
"origin": [["src/views/Edit/Sources/Network.js", 188]]
},
"Address": {
"origin": [["src/views/Edit/Sources/Network.js", 191]]
},
"UDP Transport": {
"origin": [["src/views/Edit/Sources/Network.js", 194]]
},
"Probe": {
"origin": [
["src/views/Edit/Sources/Network.js", 197],
["src/views/Edit/Sources/Network.js", 224]
]
},
"Stream name": {
"origin": [["src/views/Edit/Sources/Network.js", 210]]
},
"Send stream to:": {
"origin": [["src/views/Edit/Sources/Network.js", 213]]
},
"Public:": {
"origin": [["src/views/Edit/Sources/Network.js", 216]]
},
"Local:": {
"origin": [["src/views/Edit/Sources/Network.js", 220]]
},
"Pull or recieve the data:": {
"origin": [["src/views/Edit/Sources/Network.js", 234]]
}
}

View File

@ -0,0 +1,26 @@
{
"Video device": {
"origin": [["src/views/Edit/Sources/V4L.js", 77]]
},
"Select a device:": {
"origin": [["src/views/Edit/Sources/V4L.js", 86]]
},
"Device": {
"origin": [["src/views/Edit/Sources/V4L.js", 95]]
},
"Format": {
"origin": [["src/views/Edit/Sources/V4L.js", 101]]
},
"Framerate": {
"origin": [["src/views/Edit/Sources/V4L.js", 104]]
},
"Width": {
"origin": [["src/views/Edit/Sources/V4L.js", 107]]
},
"Height": {
"origin": [["src/views/Edit/Sources/V4L.js", 110]]
},
"Probe": {
"origin": [["src/views/Edit/Sources/V4L.js", 115]]
}
}

View File

@ -0,0 +1,60 @@
{
"Select Audio Source:": {
"origin": [["src/views/Edit/Sources/Virtual.js", 162]]
},
"Source": {
"origin": [
["src/views/Edit/Sources/Virtual.js", 165],
["src/views/Edit/Sources/Virtual.js", 243]
]
},
"Layout": {
"origin": [
["src/views/Edit/Sources/Virtual.js", 175],
["src/views/Edit/Sources/Virtual.js", 193]
]
},
"Sampling": {
"origin": [
["src/views/Edit/Sources/Virtual.js", 181],
["src/views/Edit/Sources/Virtual.js", 199],
["src/views/Edit/Sources/Virtual.js", 224]
]
},
"Color": {
"origin": [["src/views/Edit/Sources/Virtual.js", 207]]
},
"Amplitude": {
"origin": [["src/views/Edit/Sources/Virtual.js", 217]]
},
"Frequency": {
"origin": [["src/views/Edit/Sources/Virtual.js", 232]]
},
"Beep Factor": {
"origin": [["src/views/Edit/Sources/Virtual.js", 235]]
},
"Select Video Source:": {
"origin": [["src/views/Edit/Sources/Virtual.js", 240]]
},
"FPS": {
"origin": [
["src/views/Edit/Sources/Virtual.js", 259],
["src/views/Edit/Sources/Virtual.js", 277]
]
},
"Size": {
"origin": [
["src/views/Edit/Sources/Virtual.js", 270],
["src/views/Edit/Sources/Virtual.js", 291]
]
},
"Rule": {
"origin": [["src/views/Edit/Sources/Virtual.js", 288]]
},
"Scale": {
"origin": [["src/views/Edit/Sources/Virtual.js", 294]]
},
"Probe": {
"origin": [["src/views/Edit/Sources/Virtual.js", 301]]
}
}

View File

@ -0,0 +1,29 @@
{
"Edit": {
"origin": [["src/views/Edit/index.js", 540]]
},
"Sources:": {
"origin": [["src/views/Edit/index.js", 547]]
},
"Add Source": {
"origin": [["src/views/Edit/index.js", 551]]
},
"Profile:": {
"origin": [["src/views/Edit/index.js", 554]]
},
"HLS Settings:": {
"origin": [["src/views/Edit/index.js", 560]]
},
"Control:": {
"origin": [["src/views/Edit/index.js", 570]]
},
"Snapshot:": {
"origin": [["src/views/Edit/index.js", 580]]
},
"Save": {
"origin": [["src/views/Edit/index.js", 592]]
},
"Abort": {
"origin": [["src/views/Edit/index.js", 594]]
}
}

View File

@ -0,0 +1,8 @@
{
"Error": {
"origin": [["src/views/Invalid.js", 16]]
},
"Can't connect to Restreamer-API": {
"origin": [["src/views/Invalid.js", 19]]
}
}

View File

@ -0,0 +1,20 @@
{
"Login": {
"origin": [
["src/views/Login.js", 49],
["src/views/Login.js", 68]
]
},
"Username": {
"origin": [["src/views/Login.js", 55]]
},
"Password": {
"origin": [["src/views/Login.js", 58]]
},
"Reset": {
"origin": [["src/views/Login.js", 67]]
},
"News": {
"origin": [["src/views/Login.js", 77]]
}
}

View File

@ -0,0 +1,8 @@
{
"Unknown": {
"origin": [["src/views/Main/Egress.js", 17]]
},
"Self-Hosted": {
"origin": [["src/views/Main/Egress.js", 21]]
}
}

View File

@ -0,0 +1,11 @@
{
"Publication Services": {
"origin": [["src/views/Main/Publication.js", 152]]
},
"Viewer": {
"origin": [["src/views/Main/Publication.js", 167]]
},
"kbit/s": {
"origin": [["src/views/Main/Publication.js", 180]]
}
}

View File

@ -0,0 +1,41 @@
{
"Loading": {
"origin": [["src/views/Main/index.js", 150]]
},
"Retrieving stream data ...": {
"origin": [["src/views/Main/index.js", 153]]
},
"No channel defined": {
"origin": [["src/views/Main/index.js", 166]]
},
"Main channel": {
"origin": [["src/views/Main/index.js", 182]]
},
"No Video": {
"origin": [["src/views/Main/index.js", 192]]
},
"Connecting ...": {
"origin": [["src/views/Main/index.js", 202]]
},
"Uptime": {
"origin": [["src/views/Main/index.js", 237]]
},
"kbit/s": {
"origin": [["src/views/Main/index.js", 243]]
},
"FPS": {
"origin": [["src/views/Main/index.js", 249]]
},
"Process Details": {
"origin": [
["src/views/Main/index.js", 257],
["src/views/Main/index.js", 263]
]
},
"Process Debug": {
"origin": [
["src/views/Main/index.js", 258],
["src/views/Main/index.js", 264]
]
}
}

View File

@ -0,0 +1,20 @@
{
"Add Publication Service": {
"origin": [["src/views/Publication/Add.js", 124]]
},
"Abort": {
"origin": [["src/views/Publication/Add.js", 136]]
},
"Service Name": {
"origin": [["src/views/Publication/Add.js", 147]]
},
"Back": {
"origin": [["src/views/Publication/Add.js", 158]]
},
"Close": {
"origin": [["src/views/Publication/Add.js", 159]]
},
"Save": {
"origin": [["src/views/Publication/Add.js", 160]]
}
}

View File

@ -0,0 +1,20 @@
{
"Edit Publication Service": {
"origin": [["src/views/Publication/Edit.js", 211]]
},
"Service Name": {
"origin": [["src/views/Publication/Edit.js", 221]]
},
"Process Details": {
"origin": [["src/views/Publication/Edit.js", 238]]
},
"Close": {
"origin": [["src/views/Publication/Edit.js", 244]]
},
"Save": {
"origin": [["src/views/Publication/Edit.js", 245]]
},
"Delete": {
"origin": [["src/views/Publication/Edit.js", 246]]
}
}

View File

@ -0,0 +1,17 @@
{
"Connecting ...": {
"origin": [["src/views/Publication/Process.js", 41]]
},
"Disconnecting ...": {
"origin": [["src/views/Publication/Process.js", 48]]
},
"Connected since <0/>": {
"origin": [["src/views/Publication/Process.js", 55]]
},
"Error": {
"origin": [["src/views/Publication/Process.js", 64]]
},
"Error: {0}": {
"origin": [["src/views/Publication/Process.js", 67]]
}
}

View File

@ -0,0 +1,53 @@
{
"Self-Hosted / Player Settings": {
"origin": [["src/views/Publication/Selfhosted.js", 136]]
},
"Colors": {
"origin": [["src/views/Publication/Selfhosted.js", 143]]
},
"Seekbar Color": {
"origin": [["src/views/Publication/Selfhosted.js", 146]]
},
"Button Color": {
"origin": [["src/views/Publication/Selfhosted.js", 149]]
},
"Logo:": {
"origin": [["src/views/Publication/Selfhosted.js", 152]]
},
"Image URL": {
"origin": [["src/views/Publication/Selfhosted.js", 155]]
},
"Position": {
"origin": [["src/views/Publication/Selfhosted.js", 158]]
},
"Link": {
"origin": [["src/views/Publication/Selfhosted.js", 166]]
},
"Google Analytics": {
"origin": [["src/views/Publication/Selfhosted.js", 169]]
},
"Google Analytics ID": {
"origin": [["src/views/Publication/Selfhosted.js", 172]]
},
"Statistics": {
"origin": [["src/views/Publication/Selfhosted.js", 175]]
},
"Playback": {
"origin": [["src/views/Publication/Selfhosted.js", 178]]
},
"Autostart": {
"origin": [["src/views/Publication/Selfhosted.js", 181]]
},
"Mute": {
"origin": [["src/views/Publication/Selfhosted.js", 184]]
},
"IFrame Code:": {
"origin": [["src/views/Publication/Selfhosted.js", 207]]
},
"Save": {
"origin": [["src/views/Publication/Selfhosted.js", 217]]
},
"Close": {
"origin": [["src/views/Publication/Selfhosted.js", 219]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Facebook.js", 52]]
},
"Facebook Live": {
"origin": [["src/views/Publication/Services/Facebook.js", 70]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Instagram.js", 52]]
},
"Instagram": {
"origin": [["src/views/Publication/Services/Instagram.js", 70]]
}
}

View File

@ -0,0 +1,11 @@
{
"Protocol": {
"origin": [["src/views/Publication/Services/RTMP.js", 54]]
},
"Address": {
"origin": [["src/views/Publication/Services/RTMP.js", 57]]
},
"Custom RTMP": {
"origin": [["src/views/Publication/Services/RTMP.js", 77]]
}
}

View File

@ -0,0 +1,11 @@
{
"Protocol": {
"origin": [["src/views/Publication/Services/SRT.js", 54]]
},
"Address": {
"origin": [["src/views/Publication/Services/SRT.js", 57]]
},
"SRT": {
"origin": [["src/views/Publication/Services/SRT.js", 77]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Twitch.js", 52]]
},
"Twitch": {
"origin": [["src/views/Publication/Services/Twitch.js", 70]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Twitter.js", 52]]
},
"Twitter/Periscope": {
"origin": [["src/views/Publication/Services/Twitter.js", 70]]
}
}

View File

@ -0,0 +1,11 @@
{
"Protocol": {
"origin": [["src/views/Publication/Services/UDP.js", 54]]
},
"Address": {
"origin": [["src/views/Publication/Services/UDP.js", 57]]
},
"Custom UDP": {
"origin": [["src/views/Publication/Services/UDP.js", 77]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Vimeo.js", 52]]
},
"Vimeo": {
"origin": [["src/views/Publication/Services/Vimeo.js", 70]]
}
}

View File

@ -0,0 +1,8 @@
{
"Stream Key": {
"origin": [["src/views/Publication/Services/Youtube.js", 52]]
},
"YouTube Live": {
"origin": [["src/views/Publication/Services/Youtube.js", 70]]
}
}

View File

@ -0,0 +1,59 @@
{
"Error": {
"origin": [["src/views/Settings.js", 248]]
},
"Settings": {
"origin": [["src/views/Settings.js", 266]]
},
"General": {
"origin": [["src/views/Settings.js", 277]]
},
"ID": {
"origin": [["src/views/Settings.js", 280]]
},
"Public Hostnames": {
"origin": [["src/views/Settings.js", 283]]
},
"Enable Automatic Certificate from Let's Encrypt": {
"origin": [["src/views/Settings.js", 286]]
},
"Storage": {
"origin": [["src/views/Settings.js", 293]]
},
"Memory": {
"origin": [["src/views/Settings.js", 296]]
},
"Enable basic auth for PUT, POST, and DELETE requests to /memfs": {
"origin": [["src/views/Settings.js", 299]]
},
"Username": {
"origin": [["src/views/Settings.js", 302]]
},
"Password": {
"origin": [["src/views/Settings.js", 307]]
},
"Enable RTMP Server": {
"origin": [["src/views/Settings.js", 317]]
},
"Address": {
"origin": [["src/views/Settings.js", 320]]
},
"App": {
"origin": [["src/views/Settings.js", 323]]
},
"Token": {
"origin": [["src/views/Settings.js", 326]]
},
"Allow all origins": {
"origin": [["src/views/Settings.js", 333]]
},
"All changes will be applied after restarting the Restreamer-API": {
"origin": [["src/views/Settings.js", 357]]
},
"Save": {
"origin": [["src/views/Settings.js", 361]]
},
"Abort": {
"origin": [["src/views/Settings.js", 363]]
}
}

File diff suppressed because one or more lines are too long

2646
src/locales/de/messages.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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