Add v1.0.0
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.gitignore
|
||||
README.md
|
||||
node_modules/
|
||||
.yarn/cache
|
||||
.eslintcache
|
||||
.github
|
||||
.github_build
|
||||
.build
|
||||
23
.editorconfig
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
src/locales
|
||||
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": true,
|
||||
"jsxSingleQuote": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
4
.yarnrc.yml
Normal file
@ -0,0 +1,4 @@
|
||||
packageExtensions:
|
||||
react-scripts@*:
|
||||
peerDependencies:
|
||||
eslint-config-react-app: "*"
|
||||
11
Dockerfile
Normal 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" ]
|
||||
21
README.md
@ -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
@ -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
@ -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`.
|
||||
5
public/_player/clappr/dist/clappr-nerd-stats.min.js
vendored
Normal file
1
public/_player/clappr/dist/clappr-stats.min.js
vendored
Normal file
85
public/_player/clappr/dist/clappr.min.js
vendored
Normal file
1
public/_player/clappr/dist/clappr.min.js.map
vendored
Normal file
4
public/_player/clappr/files.txt
Normal 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
|
||||
112
public/_player/clappr/player.html
Normal 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>
|
||||
17
public/_player/oembed.json.in
Normal 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}}
|
||||
}
|
||||
18
public/_player/oembed.xml.in
Normal 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>
|
||||
169
public/_player/videojs/dist/video-js-skin.css
vendored
Normal 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;
|
||||
}
|
||||
|
||||
1
public/_player/videojs/dist/video-js-skin.min.css
vendored
Normal 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
1
public/_player/videojs/dist/video-js.min.css
vendored
Normal file
66670
public/_player/videojs/dist/video.js
vendored
Normal file
28
public/_player/videojs/dist/video.min.js
vendored
Normal file
2
public/_player/videojs/dist/videojs-license.css
vendored
Normal file
453
public/_player/videojs/dist/videojs-license.js
vendored
Normal 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;
|
||||
|
||||
}));
|
||||
1
public/_player/videojs/dist/videojs-license.min.css
vendored
Normal file
2
public/_player/videojs/dist/videojs-license.min.js
vendored
Normal file
386
public/_player/videojs/dist/videojs-overlay.js
vendored
Normal 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;
|
||||
1
public/_player/videojs/dist/videojs-overlay.min.css
vendored
Normal 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}
|
||||
2
public/_player/videojs/dist/videojs-overlay.min.js
vendored
Normal 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});
|
||||
7
public/_player/videojs/files.txt
Normal 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
|
||||
125
public/_player/videojs/player.html
Normal 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>
|
||||
10
public/_playersite/README.md
Normal 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
|
||||
------------|------------
|
||||
67
public/_playersite/clappr.js
Normal 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();
|
||||
});
|
||||
829
public/_playersite/index.html
Normal file
54
public/_playersite/videojs.js
Normal 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
|
After Width: | Height: | Size: 15 KiB |
17
public/index.html
Normal 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
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/logo512.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
25
public/manifest.json
Normal 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
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
413
src/Footer.js
Normal 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
@ -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
@ -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
@ -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
@ -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,
|
||||
};
|
||||
3
src/assets/images/licence.txt
Normal 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
|
||||
BIN
src/assets/images/livesource.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/playersite.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/images/settings.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/images/universe-4609408.jpg
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
src/assets/images/welcome.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
10
src/contexts/Notify.js
Normal 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
@ -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
@ -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')
|
||||
);
|
||||
5
src/locales/_build/src/Footer.js.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"Settings": {
|
||||
"origin": [["src/Footer.js", 14]]
|
||||
}
|
||||
}
|
||||
17
src/locales/_build/src/misc/ActionButton.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
17
src/locales/_build/src/misc/Progress.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/misc/controls/HLS.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
14
src/locales/_build/src/misc/controls/Process.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/misc/controls/Snapshot.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Enable Snapshots": {
|
||||
"origin": [["src/misc/controls/Snapshot.js", 39]]
|
||||
},
|
||||
"Interval (seconds)": {
|
||||
"origin": [["src/misc/controls/Snapshot.js", 42]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/misc/modals/Debug.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Debug Log": {
|
||||
"origin": [["src/misc/modals/Debug.js", 22]]
|
||||
},
|
||||
"Close": {
|
||||
"origin": [["src/misc/modals/Debug.js", 34]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/misc/modals/Process.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Process Details": {
|
||||
"origin": [["src/misc/modals/Process.js", 31]]
|
||||
},
|
||||
"Close": {
|
||||
"origin": [["src/misc/modals/Process.js", 53]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/views/Edit/Profile.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Video:": {
|
||||
"origin": [["src/views/Edit/Profile.js", 40]]
|
||||
},
|
||||
"Audio:": {
|
||||
"origin": [["src/views/Edit/Profile.js", 44]]
|
||||
}
|
||||
}
|
||||
11
src/locales/_build/src/views/Edit/SelectStream.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
38
src/locales/_build/src/views/Edit/Source.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
17
src/locales/_build/src/views/Edit/SourceList.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
26
src/locales/_build/src/views/Edit/Sources/ALSA.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
32
src/locales/_build/src/views/Edit/Sources/Network.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
26
src/locales/_build/src/views/Edit/Sources/V4L.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
60
src/locales/_build/src/views/Edit/Sources/Virtual.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
29
src/locales/_build/src/views/Edit/index.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/views/Invalid.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Error": {
|
||||
"origin": [["src/views/Invalid.js", 16]]
|
||||
},
|
||||
"Can't connect to Restreamer-API": {
|
||||
"origin": [["src/views/Invalid.js", 19]]
|
||||
}
|
||||
}
|
||||
20
src/locales/_build/src/views/Login.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
8
src/locales/_build/src/views/Main/Egress.js.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Unknown": {
|
||||
"origin": [["src/views/Main/Egress.js", 17]]
|
||||
},
|
||||
"Self-Hosted": {
|
||||
"origin": [["src/views/Main/Egress.js", 21]]
|
||||
}
|
||||
}
|
||||
11
src/locales/_build/src/views/Main/Publication.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
41
src/locales/_build/src/views/Main/index.js.json
Normal 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]
|
||||
]
|
||||
}
|
||||
}
|
||||
20
src/locales/_build/src/views/Publication/Add.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
20
src/locales/_build/src/views/Publication/Edit.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
17
src/locales/_build/src/views/Publication/Process.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
53
src/locales/_build/src/views/Publication/Selfhosted.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Facebook.js", 52]]
|
||||
},
|
||||
"Facebook Live": {
|
||||
"origin": [["src/views/Publication/Services/Facebook.js", 70]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Instagram.js", 52]]
|
||||
},
|
||||
"Instagram": {
|
||||
"origin": [["src/views/Publication/Services/Instagram.js", 70]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Twitch.js", 52]]
|
||||
},
|
||||
"Twitch": {
|
||||
"origin": [["src/views/Publication/Services/Twitch.js", 70]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Twitter.js", 52]]
|
||||
},
|
||||
"Twitter/Periscope": {
|
||||
"origin": [["src/views/Publication/Services/Twitter.js", 70]]
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Vimeo.js", 52]]
|
||||
},
|
||||
"Vimeo": {
|
||||
"origin": [["src/views/Publication/Services/Vimeo.js", 70]]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Stream Key": {
|
||||
"origin": [["src/views/Publication/Services/Youtube.js", 52]]
|
||||
},
|
||||
"YouTube Live": {
|
||||
"origin": [["src/views/Publication/Services/Youtube.js", 70]]
|
||||
}
|
||||
}
|
||||
59
src/locales/_build/src/views/Settings.js.json
Normal 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]]
|
||||
}
|
||||
}
|
||||