├── src ├── webserver │ ├── public │ │ ├── robots.txt │ │ ├── help │ │ │ ├── source.html │ │ │ └── optionalOutput.html │ │ ├── favicon.ico │ │ ├── images │ │ │ └── bg.png │ │ ├── libs │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ └── scripts │ │ │ │ ├── 38861cba61c66739c1452c3a71e39852.ttf │ │ │ │ ├── angular-translate-loader-static-files-2.18.1.min.js │ │ │ │ ├── html5shiv-3.7.2.min.js │ │ │ │ └── respond-1.4.2.min.js │ │ ├── debug │ │ │ └── README.txt │ │ ├── scripts │ │ │ ├── Login │ │ │ │ ├── LoginModule.js │ │ │ │ └── LoginController.js │ │ │ ├── Main │ │ │ │ ├── MainModule.js │ │ │ │ └── MainController.js │ │ │ ├── Footer │ │ │ │ ├── FooterModule.js │ │ │ │ ├── FooterDirective.js │ │ │ │ └── FooterController.js │ │ │ ├── StreamingInterface │ │ │ │ ├── StreamingInterfaceModule.js │ │ │ │ ├── StreamingStatusDirective.js │ │ │ │ └── StreamingStatusController.js │ │ │ ├── Header │ │ │ │ ├── HeaderDirective.js │ │ │ │ ├── HeaderModule.js │ │ │ │ └── HeaderController.js │ │ │ ├── App.Config.js │ │ │ ├── App.js │ │ │ └── Shared │ │ │ │ ├── LoggerService.js │ │ │ │ └── WebsocketsService.js │ │ ├── crossdomain.xml │ │ ├── views │ │ │ ├── footer.html │ │ │ ├── login.html │ │ │ ├── header.html │ │ │ └── status.html │ │ ├── locales │ │ │ ├── lang-pl_PL.json │ │ │ ├── lang-en_US.json │ │ │ ├── lang-es_VE.json │ │ │ ├── lang-it_IT.json │ │ │ ├── lang-sl_SI.json │ │ │ ├── lang-de_DE.json │ │ │ ├── lang-pt_PT.json │ │ │ └── lang-fr_FR.json │ │ ├── index.prod.html │ │ ├── player.html │ │ ├── index.dev.html │ │ └── css │ │ │ └── restreamer.css │ ├── middleware │ │ └── expressLogger.js │ ├── controllers │ │ ├── index.js │ │ └── api │ │ │ └── v1.js │ └── app.js ├── classes │ ├── WebsocketsController.js │ ├── EnvVar.js │ ├── Nginxrtmp.js │ ├── Logger.js │ └── RestreamerData.js └── start.js ├── .eslintignore ├── .dockerignore ├── .editorconfig ├── .csslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── docker-compose.yml ├── contrib └── ffmpeg │ └── bitrate.patch ├── package.json ├── conf ├── nginx.conf ├── nginx_ssl.conf ├── jsondb_v1_schema.json └── live-rpi.json ├── README.md ├── Dockerfile-arm32v6 ├── Dockerfile-arm32v7 ├── Dockerfile-arm64v8 ├── Dockerfile ├── Dockerfile-rpi ├── gruntfile.js ├── CHANGELOG.md ├── .eslintrc.json ├── run.sh └── LICENSE /src/webserver/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /src/webserver/public/help/source.html: -------------------------------------------------------------------------------- 1 |
2 | source help 3 |
4 | -------------------------------------------------------------------------------- /src/webserver/public/help/optionalOutput.html: -------------------------------------------------------------------------------- 1 |
2 | optional output help 3 |
4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/webserver/public/dist 3 | src/webserver/public/libs 4 | *.min.js 5 | -------------------------------------------------------------------------------- /src/webserver/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/favicon.ico -------------------------------------------------------------------------------- /src/webserver/public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/images/bg.png -------------------------------------------------------------------------------- /src/webserver/public/libs/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/libs/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/webserver/public/libs/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/libs/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/webserver/public/libs/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/libs/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/webserver/public/libs/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/libs/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/webserver/public/libs/scripts/38861cba61c66739c1452c3a71e39852.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/restreamer/master/src/webserver/public/libs/scripts/38861cba61c66739c1452c3a71e39852.ttf -------------------------------------------------------------------------------- /src/webserver/public/debug/README.txt: -------------------------------------------------------------------------------- 1 | You can enable debug reporting by the enviroment variable "RS_DEBUG=true". 2 | 3 | WARNING: 4 | Logging can consume a lot of disk space! Don not use this option in productive use! 5 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Login/LoginModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Login', []); 9 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Main/MainModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Main', []); 9 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Footer/FooterModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Footer', []); 9 | -------------------------------------------------------------------------------- /src/webserver/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/StreamingInterface/StreamingInterfaceModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('StreamingInterface', []); 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore dockerfiles 2 | .dockerignore 3 | Dockerfile* 4 | .git 5 | .editorconfig 6 | .gitignore 7 | docs 8 | node_modules 9 | bin 10 | db 11 | *.log 12 | _site 13 | Gemfile.lock 14 | src/webserver/public/config.js 15 | src/webserver/public/images/live.jpg 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | spaces_around_brackets = outside 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Footer/FooterDirective.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Footer').directive('footer', () => { 9 | return { 10 | 'restrict': 'A', 11 | 'replace': true, 12 | 'templateUrl': 'views/footer.html', 13 | 'controller': 'footerController' 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Header/HeaderDirective.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Header').directive('header', () => { 9 | return { 10 | 'restrict': 'A', 11 | 'replace': true, 12 | 'templateUrl': 'views/header.html', 13 | 'controller': 'headerController' 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Header/HeaderModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Header', []); 9 | 10 | window.angular.module('Header').config(['$translateProvider', ($translateProvider) => { 11 | $translateProvider.useStaticFilesLoader({ 12 | 'prefix': 'locales/lang-', 13 | 'suffix': '.json' 14 | }); 15 | $translateProvider.useSanitizeValueStrategy('escape'); 16 | $translateProvider.preferredLanguage('en_US'); 17 | }]); 18 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false, 19 | "zero-units": false 20 | } 21 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/StreamingInterface/StreamingStatusDirective.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('StreamingInterface').directive('streamingStatus', () => { 9 | return { 10 | 'scope': { 11 | 'data': '=', 12 | 'name': '@name' 13 | }, 14 | 'restrict': 'E', 15 | 'replace': true, 16 | 'templateUrl': 'views/status.html', 17 | 'controller': 'streamingStatusController' 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Login/LoginController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Login').controller('loginController', 9 | ['$scope', '$http', '$rootScope', function loginController ($scope, $http, $rootScope) { 10 | $scope.submit = function submit () { 11 | $http.post('login', {'user': $scope.user, 'pass': $scope.pass}).then((response) => { 12 | $scope.message = response.data.message; 13 | $rootScope.loggedIn = response.data.success; 14 | }); 15 | }; 16 | }] 17 | ); 18 | -------------------------------------------------------------------------------- /src/webserver/public/views/footer.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: datarhei 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://datarhei.github.io/restreamer/donate.html'] 13 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Footer/FooterController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Footer').controller('footerController', 9 | ['ws', '$scope', '$http', '$rootScope', 'config', (ws, $scope, $http, $rootScope, config) => { 10 | $http.get('v1/version').then((response) => { 11 | $scope.version = response.data.version; 12 | $rootScope.checkForAppUpdatesResult = response.data.update; 13 | $scope.config = config; 14 | }); 15 | $scope.logout = () => { 16 | $http.get('logout').then(() => { 17 | $rootScope.loggedIn = false; 18 | }); 19 | }; 20 | }] 21 | ); 22 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/App.Config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // Global config for frontend 10 | const config = { 11 | 'urls': { 12 | // project 13 | 'issueTracker': 'https://github.com/datarhei/restreamer/issues/new', 14 | 'projectPage': 'https://github.com/datarhei/restreamer', 15 | 'updatePage': 'https://datarhei.github.io/restreamer/docs/guides-updates.html', 16 | 17 | // help 18 | 'embedPlayerHelp': 'https://datarhei.github.io/restreamer/docs/guides-embedding.html', 19 | 'portForwardingHelp': 'https://datarhei.github.io/restreamer/wiki/portforwarding.html', 20 | 'rtmpServerHelp': 'https://datarhei.github.io/restreamer/docs/guides-external-rtmp.html' 21 | 22 | } 23 | }; 24 | 25 | window.angular.module('app').constant('config', config); 26 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Header/HeaderController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | window.angular.module('Header').controller('headerController', 9 | ['$scope', '$translate', 'loggerService', ($scope, $translate, loggerService) => { 10 | $scope.currentLocale = $translate.preferredLanguage(); 11 | $scope.switchLanguage = (locale) => { 12 | $scope.currentLocale = locale; 13 | $translate.use(locale).then( 14 | () => { 15 | loggerService.info('Switched language to ' + locale); 16 | }, 17 | (error) => { 18 | loggerService.error('INFO', 'Switching language to ' + locale + ' failed: ' + error); 19 | }); 20 | }; 21 | $scope.langIs = (locale) => { 22 | return locale === $scope.currentLocale; 23 | }; 24 | }] 25 | ); 26 | -------------------------------------------------------------------------------- /src/webserver/middleware/expressLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file express logger, that logs in the format of the Restreamer logger 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | /* eslint vars-on-top: 0 */ 9 | 10 | const logger = require.main.require('./classes/Logger')('Webserver'); 11 | 12 | module.exports = (req, res, next) => { 13 | req._startTime = new Date(); 14 | var log = () => { 15 | let code = res.statusCode; 16 | let len = parseInt(res.getHeader('Content-Length'), 10); 17 | let duration = new Date() - req._startTime; 18 | let url = (req.originalUrl || req.url); 19 | let method = req.method; 20 | 21 | if (isNaN(len)) { 22 | len = '-'; 23 | } 24 | 25 | logger.debug(method + ' "' + url + '" ' + code + ' ' + duration + 'ms ' + req.ip + ' ' + len); 26 | }; 27 | 28 | res.on('finish', log); 29 | res.on('close', log); 30 | next(); 31 | }; 32 | -------------------------------------------------------------------------------- /src/webserver/public/views/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 |
11 |
12 | 18 |
19 |
{{message | translate}}
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/webserver/public/views/header.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | EN 4 | / 5 | DE 6 | / 7 | ES 8 | / 9 | PL 10 | / 11 | FR 12 | / 13 | IT 14 | / 15 | PT 16 | / 17 | SI 18 |

19 |

Restreamer

20 |
21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | logs 26 | build/logs 27 | *.log 28 | *.sql 29 | *.sqlite 30 | 31 | # OS generated files # 32 | ###################### 33 | .DS_Store 34 | .DS_Store? 35 | ._* 36 | .Spotlight-V100 37 | .Trashes 38 | ehthumbs.db 39 | Thumbs.db 40 | 41 | # IDE / Editor files # 42 | ###################### 43 | .idea 44 | .settings 45 | nbproject 46 | 47 | # Grunt # 48 | ######### 49 | .grunt 50 | 51 | # Vagrant / Docker # 52 | #################### 53 | .vagrant 54 | 55 | # Node # 56 | ######## 57 | node_modules 58 | bin 59 | # Git # 60 | ####### 61 | *.orig 62 | 63 | # Project files # 64 | ################# 65 | src/webserver/public/dist/** 66 | src/webserver/public/config.js 67 | src/webserver/public/images/live.jpg 68 | db/** 69 | heapdump 70 | Gemfile.lock 71 | _site/** 72 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | var app = window.angular.module('app', [ 9 | 'ui.router', 10 | 'ui.bootstrap', 11 | 'pascalprecht.translate', 12 | 'Footer', 13 | 'Header', 14 | 'Login', 15 | 'Main', 16 | 'StreamingInterface' 17 | ]); 18 | 19 | app.config(($stateProvider) => { 20 | $stateProvider 21 | .state('login', { 22 | 'controller': 'loginController', 23 | 'templateUrl': 'views/login.html' 24 | }) 25 | .state('logged-in', { 26 | 'controller': 'mainController', 27 | 'templateUrl': 'views/main.html' 28 | }); 29 | }); 30 | 31 | app.controller('appController', 32 | ['$rootScope', '$state', '$http', ($rootScope, $state, $http) => { 33 | $http.get('authenticated').then((response) => { 34 | $rootScope.loggedIn = response.data; 35 | }); 36 | $rootScope.$watch('loggedIn', (value) => { 37 | $state.go(value ? 'logged-in' : 'login'); 38 | }); 39 | }] 40 | ); 41 | -------------------------------------------------------------------------------- /src/webserver/public/libs/scripts/angular-translate-loader-static-files-2.18.1.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-translate - v2.18.1 - 2018-05-19 3 | * 4 | * Copyright (c) 2018 The angular-translate team, Pascal Precht; Licensed MIT 5 | */ 6 | !function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f 2 | 3 |
4 | {{'process_init' | translate}} 5 |
6 | 7 | 8 |
9 | {{'process_success' | translate}} 10 |
11 | {{fps()}} FPS 12 | / 13 | {{kbps()}} Kb/s 14 |
15 |
16 | 17 | 18 |
19 | {{'process_failed' | translate}} 20 |
21 | {{message}} 22 |
23 |
24 | 25 | 26 |
27 | {{'process_stopped' | translate}} 28 |
29 | 30 | 31 |
32 | {{'process_input_invalid' | translate}} 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | restreamer: 4 | image: datarhei/restreamer 5 | container_name: restreamer 6 | ports: 7 | - 1935:1935 8 | - 8080:8080 9 | networks: 10 | - frontend 11 | volumes: 12 | - "~/db:/restreamer/db" 13 | environment: 14 | - RS_USERNAME=admin 15 | - RS_PASSWORD=datarhei 16 | - RS_LOGLEVEL=4 17 | - RS_TIMEZONE=Europe/Berlin 18 | - RS_SNAPSHOT_INTERVAL=1m 19 | deploy: 20 | replicas: 1 21 | restart_policy: 22 | condition: any 23 | delay: 5s 24 | window: 10s 25 | 26 | fblive: 27 | image: dweomer/stunnel 28 | container_name: fblive 29 | expose: 30 | - 1935 31 | networks: 32 | - frontend 33 | restart: always 34 | environment: 35 | - STUNNEL_SERVICE=fb-live 36 | - STUNNEL_CLIENT=yes 37 | - STUNNEL_ACCEPT=1935 38 | - STUNNEL_CONNECT=live-api-s.facebook.com:443 39 | - STUNNEL_VERIFY_CHAIN=NO 40 | 41 | https-portal: 42 | image: steveltn/https-portal:1 43 | container_name: https-portal 44 | ports: 45 | - 80:80 46 | - 443:443 47 | restart: always 48 | networks: 49 | - frontend 50 | environment: 51 | DOMAINS: localhost -> http://restreamer:8080 52 | STAGE: local 53 | # DOMAINS: yourdomain.com -> http://restreamer:8080 54 | # STAGE: production 55 | 56 | networks: 57 | frontend: 58 | -------------------------------------------------------------------------------- /src/classes/WebsocketsController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file holds the code for the class WebsocketsController 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | const logger = require.main.require('./classes/Logger')('WSController'); 10 | const app = require.main.require('./webserver/app').app; 11 | 12 | /** 13 | * static class websocket controller, that helps communicating through websockets to different namespaces and ensures 14 | * that websocket events are bound, if the websocket server has been initialized (through promise made on app start) 15 | */ 16 | class WebsocketsController { 17 | 18 | /** 19 | * emit an event to WS 20 | * @param {string} event name of the event 21 | * @param {object} data data to emit to the client event listener 22 | */ 23 | static emit (event, data) { 24 | app.get('websocketsReady').promise.then((io) => { 25 | logger.debug('Emitting ' + event); 26 | io.sockets.emit(event, data); 27 | }); 28 | } 29 | 30 | /** 31 | * add callback on WS connection 32 | * @param {function} callback 33 | */ 34 | static setConnectCallback (callback) { 35 | app.get('websocketsReady').promise.then((io) => { 36 | io.on('connection', (socket) => { 37 | callback(socket); 38 | }); 39 | }); 40 | } 41 | } 42 | 43 | module.exports = WebsocketsController; 44 | -------------------------------------------------------------------------------- /contrib/ffmpeg/bitrate.patch: -------------------------------------------------------------------------------- 1 | diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c 2 | index 2e9448e..bbae300 100644 3 | --- a/fftools/ffmpeg.c 4 | +++ b/fftools/ffmpeg.c 5 | @@ -1635,8 +1635,7 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti 6 | { 7 | AVBPrint buf, buf_script; 8 | OutputStream *ost; 9 | - AVFormatContext *oc; 10 | - int64_t total_size; 11 | + int64_t total_size = 0; 12 | AVCodecContext *enc; 13 | int frame_number, vid, i; 14 | double bitrate; 15 | @@ -1664,13 +1663,6 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti 16 | 17 | t = (cur_time-timer_start) / 1000000.0; 18 | 19 | - 20 | - oc = output_files[0]->ctx; 21 | - 22 | - total_size = avio_size(oc->pb); 23 | - if (total_size <= 0) // FIXME improve avio_size() so it works with non seekable output too 24 | - total_size = avio_tell(oc->pb); 25 | - 26 | vid = 0; 27 | av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC); 28 | av_bprint_init(&buf_script, 0, AV_BPRINT_SIZE_AUTOMATIC); 29 | @@ -1745,6 +1737,9 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti 30 | ost->st->time_base, AV_TIME_BASE_Q)); 31 | if (is_last_report) 32 | nb_frames_drop += ost->last_dropped; 33 | + 34 | + total_size += ost->data_size; 35 | + total_size += ost->enc_ctx->extradata_size; 36 | } 37 | 38 | secs = FFABS(pts) / AV_TIME_BASE; 39 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-pl_PL.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Pomoc", 3 | "issue_tracker": "Zgłaszanie błędów", 4 | "project_page": "Pomoc", 5 | "update_btn": "Aktualizacja dostępna!", 6 | "login_username": "Login", 7 | "login_password": "Hasło", 8 | "login_invalid": "Nieprawidłowy login lub hasło", 9 | "login_btn": "Zaloguj", 10 | "logout": "Wyloguj", 11 | "button_start": "Start", 12 | "button_stop": "Stop", 13 | "input_title": "Źródło wideo RTMP/RTSP/HLS", 14 | "input_example": "rtsp://login:hasło@adres_IP:port_RTSP/h264/ch1/main/av_stream", 15 | "rtsp_tcp": "RTSP po TCP", 16 | "process_input_invalid": "Błędny adres.", 17 | "process_success": "Przesyłanie strumienia w toku.", 18 | "process_failed": "Strumień wideo nie jest dostępny.", 19 | "process_failed_retry": "Spróbuj ponownie.", 20 | "process_init": "Przesyłanie sturmienia... prosze czekać...", 21 | "process_stopped": "Zatrzymywanie strumienia... Proszę czekać...", 22 | "output_optional": "Streaming do YouTube i innych.", 23 | "output_optional_example_rtmp": "rtmp://a.rtmp.youtube.com/live2/klucz_streamingu", 24 | "output_optional_example_hls": "https://.../live/stream.m3u8", 25 | "player_link_title": "Podgląd", 26 | "player_logo_example": "https://example.com/logo.png", 27 | "player_logolink_example": "https://example.com/", 28 | "player_modal_help_title": "Kod iFrame do umieszczenia na stronie www:", 29 | "player_modal_help_content": "Pamiętaj o przekierowaniu odpowiedniego portu na routerze." 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Help", 3 | "issue_tracker": "Issue alert", 4 | "project_page": "Help", 5 | "update_btn": "Update available", 6 | "login_username": "Username", 7 | "login_password": "Password", 8 | "login_invalid": "Username or Password is invalid", 9 | "login_btn": "Login", 10 | "logout": "Logout", 11 | "button_start": "Start", 12 | "button_stop": "Stop", 13 | "input_title": "RTMP/RTSP/HLS Video Source", 14 | "input_example": "e.g. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP over TCP", 16 | "process_input_invalid": "Invalid stream address, please check your input", 17 | "process_success": "Streaming is successfully initiated.", 18 | "process_failed": "Your stream wasn't accessible.", 19 | "process_failed_retry": "Retry count", 20 | "process_init": "Streaming process is initiating. Please wait...", 21 | "process_stopped": "Stopping streaming process. Please wait ...", 22 | "output_optional": "External Streaming-Server", 23 | "output_optional_example_rtmp": "e.g. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "e.g. https://.../live/stream.m3u8", 25 | "player_link_title": "Open player", 26 | "player_logo_example": "e.g. https://example.com/logo.png", 27 | "player_logolink_example": "e.g. https://example.com/", 28 | "player_modal_help_title": "iFrame code and preview of image to embed in website:", 29 | "player_modal_help_content": "In addition the port of the examples have to be forwarded from the router to IP address of the computer" 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Restreamer", 3 | "version": "0.6.8", 4 | "description": "Allows you to do h.264 video streaming on your website without a streaming provider", 5 | "author": "datarhei.org", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/datarhei/restreamer.git" 9 | }, 10 | "license": "Apache-2.0", 11 | "scripts": { 12 | "start": "node ./src/start" 13 | }, 14 | "dependencies": { 15 | "ajv": "^6.5.4", 16 | "ajv-keywords": "^3.2.0", 17 | "body-parser": "^1.18.3", 18 | "compression": "^1.7.3", 19 | "cookie": "0.3.1", 20 | "cookie-parser": "1.4.3", 21 | "express": "^4.16.3", 22 | "express-session": "^1.15.6", 23 | "fluent-ffmpeg": "^2.1.2", 24 | "jsonschema": "1.2.0", 25 | "moment-timezone": "0.5.13", 26 | "node-json-db": "0.7.3", 27 | "printf": "^0.6.1", 28 | "ps-find": "1.1.0", 29 | "public-ip": "^2.4.0", 30 | "q": "1.5.0", 31 | "request": "^2.81.0", 32 | "request-promise": "^4.2.1", 33 | "semver": "5.4.1", 34 | "socket.io": "^2.4.0" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "6.26.0", 38 | "babel-loader": "7.1.2", 39 | "babel-preset-env": "^1.7.0", 40 | "coffeescript": "^2.3.2", 41 | "eslint": "4.18.2", 42 | "grunt": "^1.3.0", 43 | "grunt-babel": "7.0.0", 44 | "grunt-contrib-csslint": "2.0.0", 45 | "grunt-contrib-cssmin": "2.2.1", 46 | "grunt-contrib-uglify": "3.0.1", 47 | "grunt-contrib-watch": "^1.1.0", 48 | "grunt-ng-annotate": "3.0.0", 49 | "grunt-shell": "2.1.0", 50 | "load-grunt-tasks": "3.5.2", 51 | "webpack": "^4.41.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-es_VE.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Ayuda", 3 | "issue_tracker": "Reportar problema", 4 | "project_page": "Ayuda", 5 | "update_btn": "Actualización disponible", 6 | "login_username": "Usuario", 7 | "login_password": "Contraseña", 8 | "login_invalid": "Usuario o contraseña inválidos", 9 | "login_btn": "Iniciar sesión", 10 | "logout": "Cerrar sesión", 11 | "button_start": "Iniciar", 12 | "button_stop": "Detener", 13 | "input_title": "Fuente de video RTMP/RTSP/HLS", 14 | "input_example": "ej. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP sobre TCP", 16 | "process_input_invalid": "Dirección del stream inválida, favor revisar la fuente", 17 | "process_success": "El streaming ha iniciado con éxito.", 18 | "process_failed": "Tu stream no es accesible.", 19 | "process_failed_retry": "Número de reintentos", 20 | "process_init": "Streaming iniciando. Por favor espera ...", 21 | "process_stopped": "Streaming detenido. Por favor espera ...", 22 | "output_optional": "Servidor de streaming externo", 23 | "output_optional_example_rtmp": "ej. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "ej. https://.../live/stream.m3u8", 25 | "player_link_title": "Abrir reproductor", 26 | "player_logo_example": "ej. https://example.com/logo.png", 27 | "player_logolink_example": "ej. https://example.com/", 28 | "player_modal_help_title": "Código iFrame e imagen de la vista previa:", 29 | "player_modal_help_content": "Adicionalmente, el puerto de los ejemplos debe ser redrigido del router a la IP de la computadora (Port forwarding)" 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-it_IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Aiuto", 3 | "issue_tracker": "Avviso di problema", 4 | "project_page": "Aiuto", 5 | "update_btn": "Aggiornamento disponibile", 6 | "login_username": "Nome utente", 7 | "login_password": "Password", 8 | "login_invalid": "Nome utente o password non valida", 9 | "login_btn": "Login", 10 | "logout": "Logout", 11 | "button_start": "Start", 12 | "button_stop": "Stop", 13 | "input_title": "RTMP/RTSP/HLS Video Source", 14 | "input_example": "e.g. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP su TCP", 16 | "process_input_invalid": "Indirizzo di flusso non valido, controlla il tuo input.", 17 | "process_success": "Lo streaming è avviato con successo.", 18 | "process_failed": "Il tuo flusso non era accessibile.", 19 | "process_failed_retry": "Retry count", 20 | "process_init": "Il processo di streaming è in fase di avvio. Attendere...", 21 | "process_stopped": "Interruzione del processo di streaming. Attendere per favore...", 22 | "output_optional": "Streaming-Server esterno", 23 | "output_optional_example_rtmp": "e.g. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "e.g. https://.../live/stream.m3u8", 25 | "player_link_title": "Giocatore aperto", 26 | "player_logo_example": "e.g. https://example.com/logo.png", 27 | "player_logolink_example": "e.g. https://example.com/", 28 | "player_modal_help_title": "Codice iFrame e anteprima dell'immagine da incorporare nel sito web:", 29 | "player_modal_help_content": "Inoltre la porta degli esempi deve essere inoltrata dal router all'indirizzo IP del computer." 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-sl_SI.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Pomoč", 3 | "issue_tracker": "Sporoči napako", 4 | "project_page": "Pomoč", 5 | "update_btn": "Posodobitev je na voljo", 6 | "login_username": "Uporabniško ime", 7 | "login_password": "Geslo", 8 | "login_invalid": "Uporabniško ime ali Geslo je nepravilno", 9 | "login_btn": "Prijava", 10 | "logout": "Odjava", 11 | "button_start": "Začni", 12 | "button_stop": "Končaj", 13 | "input_title": "RTMP/RTSP/HLS video vir", 14 | "input_example": "e.g. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP preko TCP protokola", 16 | "process_input_invalid": "Neveljaven naslov za video prenos, preverite vnos", 17 | "process_success": "Pretakanje videa je uspešno sproženo.", 18 | "process_failed": "Vaš video prenos ni bil dostopen.", 19 | "process_failed_retry": "Ponovite postopek", 20 | "process_init": "Postopek pretakanja videa se je začelo. Prosimo počakajte...", 21 | "process_stopped": "Zaključevanje video prenosa. Prosimo počakajte ...", 22 | "output_optional": "Zunanji video-strežnik", 23 | "output_optional_example_rtmp": "e.g. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "e.g. https://.../live/stream.m3u8", 25 | "player_link_title": "Odpri video predvajalnik", 26 | "player_logo_example": "e.g. https://example.com/logo.png", 27 | "player_logolink_example": "e.g. https://example.com/", 28 | "player_modal_help_title": "iFrame koda in predogled slike za vdelavo v spletno stran:", 29 | "player_modal_help_content": "Poleg tega je potrebno vrata (port) preusmeriti z usmerjevalnika na IP naslov računalnika" 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Hilfe", 3 | "issue_tracker": "Fehler melden", 4 | "project_page": "Hilfe", 5 | "update_btn": "Update verfügbar", 6 | "login_username": "Benutzername", 7 | "login_password": "Passwort", 8 | "login_invalid": "Benutzername oder Passwort ist ungültig", 9 | "login_btn": "Anmelden", 10 | "logout": "Ausloggen", 11 | "button_start": "Start", 12 | "button_stop": "Stop", 13 | "input_title": "RTMP/RTSP/HLS Video Quelle", 14 | "input_example": "z.B. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP über TCP", 16 | "process_input_invalid": "Die Stream-Adresse ist nicht valide. Bitte überprüfe die Eingabe", 17 | "process_success": "Das Streaming wurde erfolgreich aufgebaut.", 18 | "process_failed": "Auf den Stream konnte nicht zugegriffen werden.", 19 | "process_failed_retry": "Versuch", 20 | "process_init": "Der Streaming-Prozess wird erstellt. Bitte warten...", 21 | "process_stopped": "Der Streaming-Prozess wird angehalten. Bitte warten ...", 22 | "output_optional": "Externer Streaming-Server", 23 | "output_optional_example_rtmp": "z.B. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "z.B. https://.../live/stream.m3u8", 25 | "player_link_title": "Player öffnen", 26 | "player_logo_example": "z.B. https://example.com/logo.png", 27 | "player_logolink_example": "z.B. https://example.com/", 28 | "player_modal_help_title": "Der iFrame-Code und ein Preview-Image zur Einbettung in der Webseite:", 29 | "player_modal_help_content": "Zusätzlich muss der Port aus den Beispielen auf die IP-Adresse des Rechners weitergeleitet werden" 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-pt_PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Ajuda", 3 | "issue_tracker": "Alerta de problema", 4 | "project_page": "Ajuda", 5 | "update_btn": "Atualização disponível", 6 | "login_username": "Nome de usuário", 7 | "login_password": "Senha", 8 | "login_invalid": "O nome de usuário ou senha é inválido", 9 | "login_btn": "Login", 10 | "logout": "Logout", 11 | "button_start": "Iniciar", 12 | "button_stop": "Parar", 13 | "input_title": "Fonte de Vídeo RTMP/RTSP/HLSe", 14 | "input_example": "e.g. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP sobre TCP", 16 | "process_input_invalid": "Endereço de fluxo inválido, por favor verifique a sua entrada.", 17 | "process_success": "Streaming é iniciado com sucesso.", 18 | "process_failed": "O seu stream não estava acessível.", 19 | "process_failed_retry": "Contagem de novas tentativas", 20 | "process_init": "Streaming process is initiating. Por favor aguarde...", 21 | "process_stopped": "Parando o processo de streaming. Por favor aguarde...", 22 | "output_optional": "External Streaming-Server", 23 | "output_optional_example_rtmp": "e.g. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "e.g. https://.../live/stream.m3u8", 25 | "player_link_title": "Abrir jogador", 26 | "player_logo_example": "e.g. https://example.com/logo.png", 27 | "player_logolink_example": "e.g. https://example.com/", 28 | "player_modal_help_title": "iFrame código e visualização da imagem para incorporar no site:", 29 | "player_modal_help_content": "Além disso, a porta dos exemplos tem de ser encaminhada do router para o endereço IP do computador." 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/public/locales/lang-fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "help_title": "Aide", 3 | "issue_tracker": "Alerte de problème", 4 | "project_page": "Aide", 5 | "update_btn": "Mise à jour disponible", 6 | "login_username": "Nom d'utilisateur", 7 | "login_password": "Mot de passe", 8 | "login_invalid": "Nom d'utilisateur ou mot de passe invalide", 9 | "login_btn": "Connexion", 10 | "logout": "Déconnexion", 11 | "button_start": "Démarrer", 12 | "button_stop": "Arrêt", 13 | "input_title": "Source vidéo RTMP/RTSP/HLS", 14 | "input_example": "e.g. rtsp://192.168.57.100/media.amp", 15 | "rtsp_tcp": "RTSP sur TCP", 16 | "process_input_invalid": "Adresse de streaming invalide, veuillez vérifier votre saisie.", 17 | "process_success": "Öe streaming est lancé avec succès.", 18 | "process_failed": "Votre ruisseau n'était pas accessible.", 19 | "process_failed_retry": "Réessayer de compter", 20 | "process_init": "Le processus de streaming est en cours. Veuillez patienter...", 21 | "process_stopped": "Arrêt du processus de streaming. Veuillez patienter...", 22 | "output_optional": "Serveur Streaming externe", 23 | "output_optional_example_rtmp": "e.g. rtmp://a.rtmp.youtube.com/live2/...", 24 | "output_optional_example_hls": "e.g. https://.../live/stream.m3u8", 25 | "player_link_title": "Ouvrir le joueur", 26 | "player_logo_example": "e.g. https://example.com/logo.png", 27 | "player_logolink_example": "e.g. https://example.com/", 28 | "player_modal_help_title": "Code iFrame et prévisualisation de l'image à intégrer dans le site:", 29 | "player_modal_help_content": "En outre, le port des exemples doit être transféré du routeur vers l'adresse IP de l'ordinateur." 30 | } 31 | -------------------------------------------------------------------------------- /src/webserver/controllers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file controller for routing from / 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | /* eslint no-unused-vars: 0 */ 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | var auth = require(require('path').join(global.__base, 'conf', 'live.json')).auth; 12 | 13 | module.exports = (app) => { 14 | /* Handle Login POST */ 15 | app.post('/login', (req, res, next) => { 16 | var username = process.env.RS_USERNAME || auth.username; 17 | var password = process.env.RS_PASSWORD || auth.password; 18 | var success = false; 19 | var message = ''; 20 | if (req.body.user === username && req.body.pass === password) { 21 | req.session.authenticated = true; 22 | success = true; 23 | } else { 24 | message = 'login_invalid'; 25 | req.session.destroy(); 26 | success = false; 27 | } 28 | res.json({ 29 | 'success': success, 30 | 'message': message 31 | }); 32 | }); 33 | app.get('/authenticated', (req, res) => { 34 | res.json(req.session.authenticated === true); 35 | }); 36 | app.get('/logout', (req, res) => { 37 | req.session.destroy(); 38 | res.end(); 39 | }); 40 | /* Handle NGINX-RTMP token */ 41 | app.get('/token', (req, res) => { 42 | var token = process.env.RS_TOKEN || auth.token; 43 | if (token != '') { 44 | if (req.query.token == token) { 45 | res.writeHead(200, { 46 | 'Content-Type': 'text/plain' 47 | }); 48 | res.end('Authorized'); 49 | } else { 50 | res.writeHead(401, { 51 | 'Content-Type': 'text/plain' 52 | }); 53 | res.end('Unauthorized'); 54 | } 55 | } else { 56 | res.writeHead(200, { 57 | 'Content-Type': 'text/plain' 58 | }); 59 | res.end('Authorized'); 60 | } 61 | }); 62 | }; 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/StreamingInterface/StreamingStatusController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | /** 9 | * Streaming Status Controller 10 | * 11 | * controls the display of the streaming status (fps, status) 12 | */ 13 | window.angular.module('StreamingInterface').controller('streamingStatusController', 14 | ['$scope', ($scope) => { 15 | /** 16 | * extract statusName 17 | * @returns {string} 18 | */ 19 | const statusName = () => { 20 | return $scope.data.states[$scope.name].type; 21 | }; 22 | 23 | /** 24 | * check if the status is connecting 25 | * @returns {boolean} 26 | */ 27 | $scope.connecting = () => { 28 | return statusName() === 'connecting'; 29 | }; 30 | 31 | /** 32 | * check if the status is connected 33 | * @returns {boolean} 34 | */ 35 | $scope.connected = () => { 36 | return statusName() === 'connected'; 37 | }; 38 | 39 | /** 40 | * check if the status is stopped 41 | * @returns {boolean} 42 | */ 43 | $scope.stopped = () => { 44 | return statusName() === 'stopped'; 45 | }; 46 | 47 | /** 48 | * check if the status is error 49 | * @returns {boolean} 50 | */ 51 | $scope.error = () => { 52 | $scope.message = $scope.data.states[$scope.name].message; 53 | return statusName() === 'error'; 54 | }; 55 | 56 | /** 57 | * @returns {number} current FPS 58 | */ 59 | $scope.fps = () => { 60 | return $scope.data.progresses ? $scope.data.progresses[$scope.name].currentFps : 0; 61 | }; 62 | 63 | /** 64 | * @returns {number} current bit rate 65 | */ 66 | $scope.kbps = () => { 67 | var bitrate = $scope.data.progresses ? $scope.data.progresses[$scope.name].currentKbps : 0; 68 | return bitrate.toFixed(1); 69 | }; 70 | }]); 71 | -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | 3 | error_log stderr error; 4 | 5 | worker_processes 1; 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | rtmp { 11 | server { 12 | listen [::]:1935 ipv6only=off; 13 | chunk_size 4000; 14 | 15 | application live { 16 | live on; 17 | idle_streams off; 18 | on_publish http://localhost:3000/token; 19 | notify_method get; 20 | } 21 | 22 | application hls { 23 | live on; 24 | hls on; 25 | hls_type live; 26 | hls_playlist_length 60s; 27 | hls_fragment 2s; 28 | hls_sync 2ms; 29 | hls_path /tmp/hls; 30 | idle_streams off; 31 | on_publish http://localhost:3000/token; 32 | notify_method get; 33 | } 34 | } 35 | } 36 | 37 | http { 38 | sendfile off; 39 | tcp_nopush on; 40 | access_log off; 41 | gzip on; 42 | gzip_vary on; 43 | gzip_min_length 1000; 44 | gzip_types text/css application/javascript; 45 | server { 46 | listen 8080; 47 | listen [::]:8080; 48 | root /restreamer/src/webserver/public; 49 | include /usr/local/nginx/conf/mime.types; 50 | location / { 51 | try_files $uri @node; 52 | add_header Access-Control-Allow-Origin *; 53 | add_header Cache-Control no-cache; 54 | } 55 | location @node { 56 | add_header Access-Control-Allow-Origin *; 57 | add_header Cache-Control no-cache; 58 | proxy_pass http://localhost:3000; 59 | proxy_http_version 1.1; 60 | proxy_set_header Upgrade $http_upgrade; 61 | proxy_set_header Connection "upgrade"; 62 | proxy_set_header Host $host; 63 | } 64 | location /hls { 65 | types { 66 | application/vnd.apple.mpegurl m3u8; 67 | video/mp2t ts; 68 | } 69 | root /tmp; 70 | add_header Cache-Control no-cache; 71 | add_header Access-Control-Allow-Origin *; 72 | } 73 | location /debug { 74 | autoindex on; 75 | } 76 | location = /ping { 77 | add_header Content-Type text/plain; 78 | return 200 "pong"; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Shared/LoggerService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 7 | /* eslint no-console: 0*/ 8 | 'use strict'; 9 | 10 | // styles of the logging output 11 | const INFO = 'color: #0000FF; font-weight: bold'; 12 | const DEBUG = 'color: #AABBCC; font-weight: bold'; 13 | const ERROR = 'color: #FF0011; font-weight: bold'; 14 | const WEBSOCKETS_IN = 'color: #00BFFF; font-weight: bold'; 15 | const WEBSOCKETS_OUT = 'color: #00BF00; font-weight: bold'; 16 | const WEBSOCKETS_NAMESPACE = 'color: #00BF00; font-weight: bold'; 17 | 18 | const LoggerService = function loggerService () { 19 | /** 20 | * log an info message 21 | * @param {string} message 22 | */ 23 | this.info = (message) => { 24 | this.log(INFO, message, 'INFO'); 25 | }; 26 | 27 | /** 28 | * log an debug message 29 | * @param {string} message 30 | */ 31 | this.debug = (message) => { 32 | this.log(DEBUG, message, 'DEBUG'); 33 | }; 34 | 35 | /** 36 | * log an error message 37 | * @param {string} message 38 | */ 39 | this.error = (message) => { 40 | this.log(ERROR, message, 'ERROR'); 41 | }; 42 | 43 | /** 44 | * log an websocket in message 45 | * @param {string} message 46 | */ 47 | this.websocketsIn = (message) => { 48 | this.log(WEBSOCKETS_IN, message, 'WS_IN'); 49 | }; 50 | 51 | /** 52 | * log an websocket out message 53 | * @param {string} message 54 | */ 55 | this.websocketsOut = (message) => { 56 | this.log(WEBSOCKETS_OUT, message, 'WS_OUT'); 57 | }; 58 | 59 | /** 60 | * log an websocket namespace message 61 | * @param {string} message 62 | */ 63 | this.websocketsNamespace = (message) => { 64 | this.log(WEBSOCKETS_NAMESPACE, message, 'WS_CONNECT'); 65 | }; 66 | 67 | /** 68 | * log a message with style 69 | * @param {string} style 70 | * @param {string} message 71 | * @param {string} type 72 | */ 73 | this.log = (style, message, type) => { 74 | console.log('%c [' + type + '] ' + message, style); 75 | }; 76 | }; 77 | 78 | // configure loggerService as AngularJS Service 79 | window.angular.module('app').factory('loggerService', () => { 80 | return new LoggerService(); 81 | }); 82 | -------------------------------------------------------------------------------- /src/webserver/controllers/api/v1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file controller for routing from /v1 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | const express = require('express'); 10 | const router = new express.Router(); 11 | const version = require(require('path').join(global.__base, 'package.json')).version; 12 | 13 | // TODO: solve the circular dependency problem and place Restreamer require here 14 | 15 | router.get('/version', (req, res) => { 16 | res.json({ 17 | 'version': version, 18 | 'update': require.main.require('./classes/Restreamer').data.updateAvailable 19 | }); 20 | }); 21 | router.get('/ip', (req, res) => { 22 | res.end(require.main.require('./classes/Restreamer').data.publicIp); 23 | }); 24 | router.get('/states', (req, res) => { 25 | const states = require.main.require('./classes/Restreamer').data.states; 26 | 27 | const response = { 28 | 'repeat_to_local_nginx': { 29 | type: states.repeatToLocalNginx.type, 30 | message: states.repeatToLocalNginx.message.replace(/\?token=[^\s]+/, '?token=***'), 31 | }, 32 | 'repeat_to_optional_output': { 33 | type: states.repeatToOptionalOutput.type, 34 | message: states.repeatToOptionalOutput.message.replace(/\?token=[^\s]+/, '?token=***'), 35 | }, 36 | }; 37 | 38 | res.json(response); 39 | }); 40 | router.get('/progresses', (req, res) => { 41 | const progresses = require.main.require('./classes/Restreamer').data.progresses; 42 | 43 | res.json({ 44 | 'repeat_to_local_nginx': { 45 | 'frames': progresses.repeatToLocalNginx.frames, 46 | 'current_fps': progresses.repeatToLocalNginx.currentFps, 47 | 'current_kbps': progresses.repeatToLocalNginx.currentKbps, 48 | 'target_size': progresses.repeatToLocalNginx.targetSize, 49 | 'timemark': progresses.repeatToLocalNginx.timemark 50 | }, 51 | 'repeat_to_optional_output': { 52 | 'frames': progresses.repeatToOptionalOutput.frames, 53 | 'current_fps': progresses.repeatToOptionalOutput.currentFps, 54 | 'current_kbps': progresses.repeatToOptionalOutput.currentKbps, 55 | 'target_size': progresses.repeatToOptionalOutput.targetSize, 56 | 'timemark': progresses.repeatToOptionalOutput.timemark 57 | } 58 | }); 59 | }); 60 | 61 | module.exports = router; 62 | -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file this file is loaded on application start and initializes the application 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | const path = require('path'); 9 | 10 | global.__src = __dirname; 11 | global.__base = path.join(__dirname, '..'); 12 | global.__public = path.join(__dirname, 'webserver', 'public'); 13 | 14 | const config = require(path.join(global.__base, 'conf', 'live.json')); 15 | const env = require('./classes/EnvVar'); 16 | 17 | // setup environment vars 18 | env.init(config); 19 | 20 | const packageJson = require(path.join('..', 'package.json')); 21 | const logger = require('./classes/Logger')('start'); 22 | const nginxrtmp = require('./classes/Nginxrtmp')(config); 23 | const Q = require('q'); 24 | const Restreamer = require('./classes/Restreamer'); 25 | const RestreamerData = require('./classes/RestreamerData'); 26 | const restreamerApp = require('./webserver/app'); 27 | 28 | if(process.env.RS_DEBUG == "true") { 29 | logger.info('Debugging enabled. Check the /debug path in the web interface.', false); 30 | } 31 | 32 | // show start message 33 | logger.info(' _ _ _ _ ', false); 34 | logger.info(' __| | __ _| |_ __ _ _ __| |___ ___(_)', false); 35 | logger.info(' / _ |/ _ | __/ _ | __| _ |/ _ | |', false); 36 | logger.info('| (_| | (_| | || (_| | | | | | | __/| |', false); 37 | logger.info('|_____|_____|_||_____|_| |_| |_|____||_|', false); 38 | logger.info('', false); 39 | logger.info('Restreamer v' + packageJson.version, false); 40 | logger.info('', false); 41 | logger.info('ENVIRONMENTS', false); 42 | logger.info('More information in our Docs', false); 43 | logger.info('', false); 44 | 45 | // list environment variables 46 | env.list(logger); 47 | 48 | // bail out if there are errors 49 | if(env.hasErrors()) { 50 | process.exit(); 51 | } 52 | 53 | // start the app 54 | nginxrtmp.start(process.env.RS_HTTPS == "true") 55 | .then(() => { 56 | return RestreamerData.checkJSONDb(); 57 | }) 58 | .then(() => { 59 | Restreamer.checkForUpdates(); 60 | Restreamer.getPublicIp(); 61 | Restreamer.bindWebsocketEvents(); 62 | return restreamerApp.startWebserver(); 63 | }) 64 | .then(() => { 65 | return Q.fcall(Restreamer.restoreProcesses); 66 | }) 67 | .catch((error) => { 68 | logger.error('Error starting webserver and nginx for application: ' + error); 69 | }); 70 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Shared/WebsocketsService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Service to handle websocket connections and events 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 8 | /* eslint no-undef: 0*/ 9 | 'use strict'; 10 | 11 | const WebsocketsService = function websocketsService ($rootScope, loggerService) { 12 | this.$rootScope = $rootScope; 13 | this.loggerService = loggerService; 14 | this.socket = null; 15 | 16 | $rootScope.$watch('loggedIn', (loggedIn) => { 17 | if (loggedIn) { 18 | this.socket = io('/', {path: (window.location.pathname + '/socket.io').replace(/\/+/g, "/")}); 19 | this.loggerService.websocketsNamespace('WS connected'); 20 | } else if (this.socket !== null) { 21 | this.socket.disconnect(); 22 | this.loggerService.websocketsNamespace('WS disconnected'); 23 | } 24 | }); 25 | 26 | /** 27 | * emit an event to socket 28 | * @param event 29 | * @param data 30 | * @returns {websocketsService} 31 | */ 32 | this.emit = (event, data) => { 33 | if (this.socket) { 34 | this.loggerService.websocketsOut(`emit event "${event}"`); 35 | this.socket.emit(event, data); 36 | } 37 | return this; 38 | }; 39 | 40 | /** 41 | * react on an event to socket with callback 42 | * @param event 43 | * @param {function} callback 44 | * @returns {websocketsService} 45 | */ 46 | this.on = (event, callback) => { 47 | var self = this; 48 | if (this.socket) { 49 | this.loggerService.websocketsIn(`got event "${event}"`); 50 | this.socket.on(event, function woEvent () { 51 | var args = arguments; 52 | self.$rootScope.$apply(function weApply () { 53 | callback.apply(null, args); 54 | }); 55 | }); 56 | } 57 | return this; 58 | }; 59 | 60 | /** 61 | * disable an event on socket 62 | * @param event 63 | * @param callback 64 | */ 65 | this.off = (event, callback) => { 66 | if (this.socket) { 67 | this.socket.removeListener(event, callback); 68 | } 69 | }; 70 | }; 71 | 72 | // connect service to angular.js 73 | window.angular.module('app').factory('ws', ['$rootScope', 'loggerService', ($rootScope, loggerService) => { 74 | return new WebsocketsService($rootScope, loggerService); 75 | }]); 76 | -------------------------------------------------------------------------------- /src/webserver/public/libs/scripts/html5shiv-3.7.2.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.2",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b)}(this,document); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restreamer 2 | 3 | Datarhei/Restreamer offers smart free video streaming. Stream H.264 video of IP cameras live to your website. Upload your live video on [YouTube-Live](https://www.youtube.com/), [IBM Watson](https://video.ibm.com/), [Twitch](https://www.twitch.tv/), [Vimeo](https://livestream.com/) or any other streaming solutions e.g. [Wowza-Streaming-Engine](https://www.wowza.com/). Our [Docker-Image](https://hub.docker.com/search/?q=restreamer&page=1&isAutomated=0&isOfficial=0&starCount=0&pullCount=0) is easy to install and runs on Linux, MacOS and Windows. Datarhei/Restreamer can be perfectly combined with single-board computers like [Raspberry Pi](https://www.raspberrypi.org/) and [Odroid](http://www.hardkernel.com/main/main.php). It is free (licensed under Apache 2.0) and you can use it for any purpose, private or commercial. 4 | 5 | ## Features 6 | 7 | - User-Interface including login-security 8 | - JSON / HTTP-API 9 | - FFmpeg streaming/encoding the video/camera-stream, creating snapshots or pushing to a external streaming-endpoint 10 | - NGINX incl. RTMP-Module as streaming-backend and hls server 11 | - Clappr-Player to embed your stream on your website 12 | - Docker and Kitematic (Docker-Toolbox) optimizations and very easy installation 13 | 14 | ## Documentation 15 | 16 | Documentation is available on [Datarhei/Restreamer GitHub pages](https://datarhei.github.io/restreamer/). 17 | We give you a lot of of informations from setting up a camera, embedding your player upon your website and streaming to services like e.g. YouTube-Live, Ustream and Livestream.com and many more things. 18 | 19 | More additional informations about streaming, cameras and so on you can find in our [Wiki](https://datarhei.github.com/restreamer/wiki). 20 | 21 | ## Development 22 | 23 | #### Building your own Docker-Image: 24 | 25 | ```sh 26 | $ git clone https://github.com/datarhei/restreamer 27 | $ docker build -t restreamer . 28 | ``` 29 | 30 | *Required Docker version >= 17.05* 31 | 32 | ## Help / Bugs 33 | 34 | If you have problems or found a bug feel free to create a new issue upon the Github issue management. 35 | 36 | Want to talk to us? Write an email to open@datarhei.org or ask a question in our (Forum) on Google Groups. 37 | 38 | ## Authors 39 | 40 | The Datarhei/Restreamer was created by [Julius Eitzen](https://github.com/jeitzen), [Sven Erbeck](https://github.com/svenerbeck), [Christoph Johannsdotter](https://github.com/christophjohannsdotter) and [Jan Stabenow](https://github.com/jstabenow). 41 | 42 | Special thanks for supporting this project [Andrew Shulgin](https://github.com/andrew-shulgin). 43 | 44 | ## Copyright 45 | 46 | Code released under the [Apache license](LICENSE). Images are copyrighted by datarhei.org 47 | -------------------------------------------------------------------------------- /conf/nginx_ssl.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | 3 | error_log stderr error; 4 | 5 | worker_processes 1; 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | rtmp { 11 | server { 12 | listen [::]:1935 ipv6only=off; 13 | chunk_size 4000; 14 | 15 | application live { 16 | live on; 17 | idle_streams off; 18 | on_publish http://localhost:3000/token; 19 | notify_method get; 20 | } 21 | 22 | application hls { 23 | live on; 24 | hls on; 25 | hls_type live; 26 | hls_playlist_length 60s; 27 | hls_fragment 2s; 28 | hls_sync 2ms; 29 | hls_path /tmp/hls; 30 | idle_streams off; 31 | on_publish http://localhost:3000/token; 32 | notify_method get; 33 | } 34 | } 35 | } 36 | 37 | http { 38 | sendfile off; 39 | tcp_nopush on; 40 | access_log off; 41 | gzip on; 42 | gzip_vary on; 43 | gzip_min_length 1000; 44 | gzip_types text/css application/javascript; 45 | 46 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 47 | 48 | ssl_session_cache shared:SSL:10m; 49 | ssl_session_timeout 10m; 50 | ssl_session_tickets off; 51 | ssl_ecdh_curve secp384r1; 52 | 53 | ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; 54 | ssl_prefer_server_ciphers on; 55 | 56 | # openssl dhparam -out dhparam.pem 2048 57 | #ssl_dhparam ../db/dhparam.pem; 58 | 59 | #ssl_stapling on; 60 | #ssl_stapling_verify on; 61 | #resolver 8.8.4.4 8.8.8.8 valid=300s; 62 | #resolver_timeout 3s; 63 | 64 | ssl_certificate ../db/ssl/cert.pem; 65 | ssl_certificate_key ../db/ssl/key.pem; 66 | 67 | server { 68 | listen 8080; 69 | listen [::]:8080; 70 | listen 8181 ssl http2; 71 | listen [::]:8181 ssl http2; 72 | root /restreamer/src/webserver/public; 73 | include /usr/local/nginx/conf/mime.types; 74 | location / { 75 | try_files $uri @node; 76 | add_header Access-Control-Allow-Origin *; 77 | add_header Cache-Control no-cache; 78 | } 79 | location @node { 80 | add_header Access-Control-Allow-Origin *; 81 | add_header Cache-Control no-cache; 82 | proxy_pass http://localhost:3000; 83 | proxy_http_version 1.1; 84 | proxy_set_header Upgrade $http_upgrade; 85 | proxy_set_header Connection "upgrade"; 86 | proxy_set_header Host $host; 87 | } 88 | location /hls { 89 | types { 90 | application/vnd.apple.mpegurl m3u8; 91 | video/mp2t ts; 92 | } 93 | root /tmp; 94 | add_header Cache-Control no-cache; 95 | add_header Access-Control-Allow-Origin *; 96 | } 97 | location /debug { 98 | autoindex on; 99 | } 100 | location = /ping { 101 | add_header Content-Type text/plain; 102 | return 200 "pong"; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/webserver/public/index.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Restreamer 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/classes/EnvVar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file holds the code for the class EnvVar 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | const logBlacklist = ['RS_PASSWORD']; 10 | 11 | /** 12 | * Class for environment variables with default values 13 | */ 14 | class EnvVar { 15 | constructor() { 16 | this.reset(); 17 | } 18 | 19 | log(message, level) { 20 | this.messages.push({ 21 | level: level, 22 | message: message 23 | }); 24 | } 25 | 26 | init(config) { 27 | // Cycle through all defined environment variables 28 | for(let envVar of config.envVars) { 29 | // Check if the environment variable is set. If not, cycle through the aliases. 30 | if(!(envVar.name in process.env)) { 31 | for(let i in envVar.alias) { 32 | let alias = envVar.alias[i]; 33 | // If the alias exists, copy it to the actual name and delete it. 34 | if(alias in process.env) { 35 | this.log('The use of ' + alias + ' is deprecated. Please use ' + envVar.name + ' instead', 'warn'); 36 | process.env[envVar.name] = process.env[alias]; 37 | delete process.env[alias]; 38 | } 39 | } 40 | } 41 | 42 | // Check if the environment variable is set and display it, if it is not set 43 | // apply the default value. In case the environment variable is required and 44 | // not set, stop the process. 45 | if(envVar.name in process.env) { 46 | // Adjust the given value to the required type 47 | switch(envVar.type) { 48 | case 'int': 49 | process.env[envVar.name] = parseInt(process.env[envVar.name], 10); 50 | break; 51 | case 'bool': 52 | process.env[envVar.name] = process.env[envVar.name] == 'true'; 53 | break; 54 | default: // keep strings 55 | break; 56 | } 57 | 58 | // Cover blacklisted values 59 | let value = process.env[envVar.name]; 60 | if(logBlacklist.indexOf(envVar.name) != -1) { 61 | value = '******'; 62 | } 63 | 64 | this.log(envVar.name + ' = ' + value + ' - ' + envVar.description, 'info'); 65 | } 66 | else { 67 | if(envVar.required == true) { 68 | this.log(envVar.name + ' not set, but required', 'error'); 69 | this.errors = true; 70 | } 71 | else { 72 | this.log(envVar.name + ' = ' + envVar.defaultValue + ' (using default) - ' + envVar.description, 'info'); 73 | process.env[envVar.name] = envVar.defaultValue; 74 | } 75 | } 76 | } 77 | } 78 | 79 | list(logger) { 80 | for(let i = 0; i < this.messages.length; i++) { 81 | let m = this.messages[i]; 82 | switch(m.level) { 83 | case 'info': 84 | logger.info(m.message, 'ENV'); 85 | break; 86 | case 'warn': 87 | logger.warn(m.message, 'ENV'); 88 | break; 89 | case 'error': 90 | logger.error(m.message, 'ENV'); 91 | break; 92 | default: 93 | break; 94 | } 95 | } 96 | 97 | this.messages = []; 98 | } 99 | 100 | hasErrors() { 101 | return this.errors; 102 | } 103 | 104 | reset() { 105 | this.messages = []; 106 | this.errors = false; 107 | } 108 | } 109 | 110 | module.exports = new EnvVar; 111 | -------------------------------------------------------------------------------- /src/webserver/public/player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Restreamer 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 |
23 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/webserver/public/libs/scripts/respond-1.4.2.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b} 36 | */ 37 | async start(useSSL) { 38 | this.logger.info('Starting ...'); 39 | let timeout = 250; 40 | let abort = false; 41 | 42 | if(useSSL == false) { 43 | this.process = spawn(this.config.nginx.command, this.config.nginx.args); 44 | } 45 | else { 46 | this.logger.info('Enabling HTTPS'); 47 | this.process = spawn(this.config.nginx.command, this.config.nginx.args_ssl); 48 | } 49 | 50 | this.process.stdout.on('data', (data) => { 51 | let lines = data.toString().split(/[\r\n]+/); 52 | 53 | for(let i = 0; i < lines.length; i++) { 54 | let line = lines[i].replace(/^.*\]/, '').trim(); 55 | if(line.length == 0) { 56 | continue; 57 | } 58 | 59 | this.logger.info(line); 60 | } 61 | }); 62 | 63 | this.process.stderr.on('data', (data) => { 64 | let lines = data.toString().split(/[\r\n]+/); 65 | 66 | for(let i = 0; i < lines.length; i++) { 67 | let line = lines[i].replace(/^.*\]/, '').trim(); 68 | if(line.length == 0) { 69 | continue; 70 | } 71 | 72 | this.logger.error(line); 73 | } 74 | }); 75 | 76 | this.process.on('close', (code) => { 77 | abort = true; 78 | 79 | this.logger.error('Exited with code: ' + code); 80 | 81 | if(code < 0) { 82 | return; 83 | } 84 | 85 | if(this.allowRestart == true) { 86 | let self = this; 87 | setTimeout(() => { 88 | self.logger.info('Trying to restart ...'); 89 | self.start(useSSL); 90 | }, timeout); 91 | } 92 | }); 93 | 94 | this.process.on('error', (err) => { 95 | this.logger.error('Failed to spawn process: ' + err.name + ': ' + err.message); 96 | }); 97 | 98 | let running = false; 99 | 100 | while(running == false){ 101 | running = await this.isRunning(timeout); 102 | if(abort == true) { 103 | break; 104 | } 105 | } 106 | 107 | if(running == false) { 108 | this.process = null; 109 | throw new Error('Failed to start'); 110 | } 111 | else { 112 | this.allowRestart = true; 113 | this.logger.info('Successfully started'); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | /** 120 | * Get current state of the NGINX server 121 | * @returns {Promise.} 122 | */ 123 | async isRunning(delay) { 124 | const url = "http://" + config.nginx.streaming.ip + ":" + config.nginx.streaming.http_port + config.nginx.streaming.http_health_path; 125 | 126 | try { 127 | await Q.delay(delay); // delay the state detection by the given amount of milliseconds 128 | const response = await rp(url); 129 | return (response == 'pong'); 130 | } catch(error) { 131 | return false; 132 | } 133 | } 134 | } 135 | 136 | module.exports = (config) => { 137 | return new Nginxrtmp(config); 138 | }; 139 | -------------------------------------------------------------------------------- /Dockerfile-arm32v6: -------------------------------------------------------------------------------- 1 | ARG IMAGE=balenalib/raspberry-pi-debian:buster-run-20200502 2 | 3 | FROM $IMAGE as builder 4 | 5 | MAINTAINER datarhei 6 | 7 | ARG LAME_VERSION=3.100 8 | ARG FFMPEG_VERSION=4.3.1 9 | ARG NGINX_VERSION=1.18.0 10 | ARG NGINXRTMP_VERSION=1.2.1 11 | ARG NODE_VERSION=10.20.1 12 | 13 | ENV SRC="/usr/local/" \ 14 | LD_LIBRARY_PATH="/usr/local/lib" \ 15 | PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" 16 | 17 | RUN apt-get update && \ 18 | apt-get install -y \ 19 | pkg-config \ 20 | curl \ 21 | libpcre3-dev \ 22 | libtool \ 23 | libssl-dev \ 24 | zlib1g-dev \ 25 | libasound2-dev \ 26 | build-essential 27 | 28 | # x264 29 | RUN mkdir -p /dist && cd /dist && \ 30 | curl -OL https://code.videolan.org/videolan/x264/-/archive/stable/x264-stable.tar.bz2 && \ 31 | tar -xvj -f x264-stable.tar.bz2 && \ 32 | cd x264-stable && \ 33 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-shared && \ 34 | make -j$(nproc) && \ 35 | make install 36 | 37 | # libmp3lame 38 | RUN mkdir -p /dist && cd /dist && \ 39 | curl -OL "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ 40 | tar -xvz -f lame-${LAME_VERSION}.tar.gz && \ 41 | cd lame-${LAME_VERSION} && \ 42 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static && \ 43 | make -j$(nproc) && \ 44 | make install 45 | 46 | # ffmpeg && patch 47 | COPY ./contrib/ffmpeg /dist/restreamer/contrib/ffmpeg 48 | 49 | RUN mkdir -p /dist && cd /dist && \ 50 | curl -OL "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ 51 | tar -xvz -f ffmpeg-${FFMPEG_VERSION}.tar.gz && \ 52 | cd ffmpeg-${FFMPEG_VERSION} && \ 53 | patch -p1 < /dist/restreamer/contrib/ffmpeg/bitrate.patch && \ 54 | ./configure \ 55 | --bindir="${SRC}/bin" \ 56 | --extra-cflags="-I${SRC}/include" \ 57 | --extra-ldflags="-L${SRC}/lib" \ 58 | --prefix="${SRC}" \ 59 | --enable-nonfree \ 60 | --enable-gpl \ 61 | --enable-version3 \ 62 | --enable-libmp3lame \ 63 | --enable-libx264 \ 64 | --enable-openssl \ 65 | --enable-postproc \ 66 | --enable-small \ 67 | --enable-static \ 68 | --disable-debug \ 69 | --disable-doc \ 70 | --disable-shared && \ 71 | make -j$(nproc) && \ 72 | make install 73 | 74 | # nginx-rtmp 75 | RUN mkdir -p /dist && cd /dist && \ 76 | curl -OL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 77 | tar -xvz -f "nginx-${NGINX_VERSION}.tar.gz" && \ 78 | curl -OL "https://github.com/arut/nginx-rtmp-module/archive/v${NGINXRTMP_VERSION}.tar.gz" && \ 79 | tar -xvz -f "v${NGINXRTMP_VERSION}.tar.gz" && \ 80 | sed -i"" -e '/case ESCAPE:/i /* fall through */' nginx-rtmp-module-${NGINXRTMP_VERSION}/ngx_rtmp_eval.c && \ 81 | cd nginx-${NGINX_VERSION} && \ 82 | ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --add-module=/dist/nginx-rtmp-module-${NGINXRTMP_VERSION} && \ 83 | make -j$(nproc) && \ 84 | make install 85 | 86 | # node.js 87 | RUN mkdir -p /dist && cd /dist && \ 88 | curl -OL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-armv6l.tar.xz" && \ 89 | tar -xvJ -f "node-v${NODE_VERSION}-linux-armv6l.tar.xz" && \ 90 | cd node-v${NODE_VERSION}-linux-armv6l && \ 91 | cp -R bin /usr/local && \ 92 | cp -R lib /usr/local 93 | 94 | RUN rm -r /dist && \ 95 | apt-get remove -y \ 96 | pkg-config \ 97 | curl \ 98 | libpcre3-dev \ 99 | libtool \ 100 | libssl-dev \ 101 | zlib1g-dev \ 102 | build-essential && \ 103 | apt autoremove -y 104 | 105 | FROM $IMAGE 106 | 107 | COPY --from=builder /usr/local/bin /usr/local/bin 108 | COPY --from=builder /usr/local/nginx /usr/local/nginx 109 | COPY --from=builder /usr/local/lib /usr/local/lib 110 | 111 | RUN apt-get update && \ 112 | apt-get install -y \ 113 | ca-certificates \ 114 | git \ 115 | procps \ 116 | libpcre3 \ 117 | openssl \ 118 | libssl1.1 \ 119 | zlib1g \ 120 | v4l-utils \ 121 | libv4l-0 \ 122 | alsa-utils 123 | 124 | COPY . /restreamer 125 | WORKDIR /restreamer 126 | 127 | RUN cd /restreamer && \ 128 | npm install -g grunt-cli nodemon eslint && \ 129 | npm install && \ 130 | grunt build && \ 131 | npm prune --production && \ 132 | npm cache verify && \ 133 | npm uninstall -g grunt-cli nodemon eslint && \ 134 | npm prune --production && \ 135 | apt-get remove -y \ 136 | git \ 137 | curl && \ 138 | apt autoremove -y 139 | 140 | EXPOSE 8080 141 | EXPOSE 8181 142 | 143 | VOLUME ["/restreamer/db"] 144 | 145 | CMD ["./run.sh"] 146 | -------------------------------------------------------------------------------- /Dockerfile-arm32v7: -------------------------------------------------------------------------------- 1 | ARG IMAGE=arm32v7/debian:10.4-slim 2 | 3 | FROM $IMAGE as builder 4 | 5 | MAINTAINER datarhei 6 | 7 | ARG LAME_VERSION=3.100 8 | ARG FFMPEG_VERSION=4.3.1 9 | ARG NGINX_VERSION=1.18.0 10 | ARG NGINXRTMP_VERSION=1.2.1 11 | ARG NODE_VERSION=12.16.3 12 | 13 | ENV SRC="/usr/local/" \ 14 | LD_LIBRARY_PATH="/usr/local/lib" \ 15 | PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" 16 | 17 | RUN apt-get update && \ 18 | apt-get install -y \ 19 | pkg-config \ 20 | curl \ 21 | libpcre3-dev \ 22 | libtool \ 23 | libssl-dev \ 24 | zlib1g-dev \ 25 | libasound2-dev \ 26 | build-essential 27 | 28 | # x264 29 | RUN mkdir -p /dist && cd /dist && \ 30 | curl -OL https://code.videolan.org/videolan/x264/-/archive/stable/x264-stable.tar.bz2 && \ 31 | tar -xvj -f x264-stable.tar.bz2 && \ 32 | cd x264-stable && \ 33 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-shared && \ 34 | make -j$(nproc) && \ 35 | make install 36 | 37 | # libmp3lame 38 | RUN mkdir -p /dist && cd /dist && \ 39 | curl -OL "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ 40 | tar -xvz -f lame-${LAME_VERSION}.tar.gz && \ 41 | cd lame-${LAME_VERSION} && \ 42 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static && \ 43 | make -j$(nproc) && \ 44 | make install 45 | 46 | # ffmpeg && patch 47 | COPY ./contrib/ffmpeg /dist/restreamer/contrib/ffmpeg 48 | 49 | RUN mkdir -p /dist && cd /dist && \ 50 | curl -OL "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ 51 | tar -xvz -f ffmpeg-${FFMPEG_VERSION}.tar.gz && \ 52 | cd ffmpeg-${FFMPEG_VERSION} && \ 53 | patch -p1 < /dist/restreamer/contrib/ffmpeg/bitrate.patch && \ 54 | ./configure \ 55 | --bindir="${SRC}/bin" \ 56 | --extra-cflags="-I${SRC}/include" \ 57 | --extra-ldflags="-L${SRC}/lib" \ 58 | --prefix="${SRC}" \ 59 | --enable-nonfree \ 60 | --enable-gpl \ 61 | --enable-version3 \ 62 | --enable-libmp3lame \ 63 | --enable-libx264 \ 64 | --enable-openssl \ 65 | --enable-postproc \ 66 | --enable-small \ 67 | --enable-static \ 68 | --disable-debug \ 69 | --disable-doc \ 70 | --disable-shared && \ 71 | make -j$(nproc) && \ 72 | make install 73 | 74 | # nginx-rtmp 75 | RUN mkdir -p /dist && cd /dist && \ 76 | curl -OL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 77 | tar -xvz -f "nginx-${NGINX_VERSION}.tar.gz" && \ 78 | curl -OL "https://github.com/arut/nginx-rtmp-module/archive/v${NGINXRTMP_VERSION}.tar.gz" && \ 79 | tar -xvz -f "v${NGINXRTMP_VERSION}.tar.gz" && \ 80 | sed -i"" -e '/case ESCAPE:/i /* fall through */' nginx-rtmp-module-${NGINXRTMP_VERSION}/ngx_rtmp_eval.c && \ 81 | cd nginx-${NGINX_VERSION} && \ 82 | ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --add-module=/dist/nginx-rtmp-module-${NGINXRTMP_VERSION} && \ 83 | make -j$(nproc) && \ 84 | make install 85 | 86 | # node.js 87 | RUN mkdir -p /dist && cd /dist && \ 88 | curl -OL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-armv7l.tar.xz" && \ 89 | tar -xvJ -f "node-v${NODE_VERSION}-linux-armv7l.tar.xz" && \ 90 | cd node-v${NODE_VERSION}-linux-armv7l && \ 91 | cp -R bin /usr/local && \ 92 | cp -R lib /usr/local 93 | 94 | RUN rm -r /dist && \ 95 | apt-get remove -y \ 96 | pkg-config \ 97 | curl \ 98 | libpcre3-dev \ 99 | libtool \ 100 | libssl-dev \ 101 | zlib1g-dev \ 102 | build-essential && \ 103 | apt autoremove -y 104 | 105 | FROM $IMAGE 106 | 107 | COPY --from=builder /usr/local/bin /usr/local/bin 108 | COPY --from=builder /usr/local/nginx /usr/local/nginx 109 | COPY --from=builder /usr/local/lib /usr/local/lib 110 | 111 | RUN apt-get update && \ 112 | apt-get install -y \ 113 | ca-certificates \ 114 | git \ 115 | procps \ 116 | libpcre3 \ 117 | openssl \ 118 | libssl1.1 \ 119 | zlib1g \ 120 | v4l-utils \ 121 | libv4l-0 \ 122 | alsa-utils \ 123 | libatomic1 124 | 125 | COPY . /restreamer 126 | WORKDIR /restreamer 127 | 128 | RUN cd /restreamer && \ 129 | npm install -g grunt-cli nodemon eslint && \ 130 | npm install && \ 131 | grunt build && \ 132 | npm prune --production && \ 133 | npm cache verify && \ 134 | npm uninstall -g grunt-cli nodemon eslint && \ 135 | npm prune --production && \ 136 | apt-get remove -y \ 137 | git \ 138 | curl && \ 139 | apt autoremove -y 140 | 141 | EXPOSE 8080 142 | EXPOSE 8181 143 | 144 | VOLUME ["/restreamer/db"] 145 | 146 | CMD ["./run.sh"] 147 | -------------------------------------------------------------------------------- /Dockerfile-arm64v8: -------------------------------------------------------------------------------- 1 | ARG IMAGE=arm64v8/debian:10.4-slim 2 | 3 | FROM $IMAGE as builder 4 | 5 | MAINTAINER datarhei 6 | 7 | ARG LAME_VERSION=3.100 8 | ARG FFMPEG_VERSION=4.3.1 9 | ARG NGINX_VERSION=1.18.0 10 | ARG NGINXRTMP_VERSION=1.2.1 11 | ARG NODE_VERSION=12.16.3 12 | 13 | ENV SRC="/usr/local/" \ 14 | LD_LIBRARY_PATH="/usr/local/lib" \ 15 | PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" 16 | 17 | RUN apt-get update && \ 18 | apt-get install -y \ 19 | pkg-config \ 20 | curl \ 21 | libpcre3-dev \ 22 | libtool \ 23 | libssl-dev \ 24 | zlib1g-dev \ 25 | libasound2-dev \ 26 | build-essential 27 | 28 | # x264 29 | RUN mkdir -p /dist && cd /dist && \ 30 | curl -OL https://code.videolan.org/videolan/x264/-/archive/stable/x264-stable.tar.bz2 && \ 31 | tar -xvj -f x264-stable.tar.bz2 && \ 32 | cd x264-stable && \ 33 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-shared && \ 34 | make -j$(nproc) && \ 35 | make install 36 | 37 | # libmp3lame 38 | RUN mkdir -p /dist && cd /dist && \ 39 | curl -OL "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ 40 | tar -xvz -f lame-${LAME_VERSION}.tar.gz && \ 41 | cd lame-${LAME_VERSION} && \ 42 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static && \ 43 | make -j$(nproc) && \ 44 | make install 45 | 46 | # ffmpeg && patch 47 | COPY ./contrib/ffmpeg /dist/restreamer/contrib/ffmpeg 48 | 49 | RUN mkdir -p /dist && cd /dist && \ 50 | curl -OL "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ 51 | tar -xvz -f ffmpeg-${FFMPEG_VERSION}.tar.gz && \ 52 | cd ffmpeg-${FFMPEG_VERSION} && \ 53 | patch -p1 < /dist/restreamer/contrib/ffmpeg/bitrate.patch && \ 54 | ./configure \ 55 | --bindir="${SRC}/bin" \ 56 | --extra-cflags="-I${SRC}/include" \ 57 | --extra-ldflags="-L${SRC}/lib" \ 58 | --prefix="${SRC}" \ 59 | --enable-nonfree \ 60 | --enable-gpl \ 61 | --enable-version3 \ 62 | --enable-libmp3lame \ 63 | --enable-libx264 \ 64 | --enable-openssl \ 65 | --enable-postproc \ 66 | --enable-small \ 67 | --enable-static \ 68 | --disable-debug \ 69 | --disable-doc \ 70 | --disable-shared && \ 71 | make -j$(nproc) && \ 72 | make install 73 | 74 | # nginx-rtmp 75 | RUN mkdir -p /dist && cd /dist && \ 76 | curl -OL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 77 | tar -xvz -f "nginx-${NGINX_VERSION}.tar.gz" && \ 78 | curl -OL "https://github.com/arut/nginx-rtmp-module/archive/v${NGINXRTMP_VERSION}.tar.gz" && \ 79 | tar -xvz -f "v${NGINXRTMP_VERSION}.tar.gz" && \ 80 | sed -i"" -e '/case ESCAPE:/i /* fall through */' nginx-rtmp-module-${NGINXRTMP_VERSION}/ngx_rtmp_eval.c && \ 81 | cd nginx-${NGINX_VERSION} && \ 82 | ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --add-module=/dist/nginx-rtmp-module-${NGINXRTMP_VERSION} && \ 83 | make -j$(nproc) && \ 84 | make install 85 | 86 | # node.js 87 | RUN mkdir -p /dist && cd /dist && \ 88 | curl -OL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-arm64.tar.xz" && \ 89 | tar -xvJ -f "node-v${NODE_VERSION}-linux-arm64.tar.xz" && \ 90 | cd node-v${NODE_VERSION}-linux-arm64 && \ 91 | cp -R bin /usr/local && \ 92 | cp -R lib /usr/local 93 | 94 | RUN rm -r /dist && \ 95 | apt-get remove -y \ 96 | pkg-config \ 97 | curl \ 98 | libpcre3-dev \ 99 | libtool \ 100 | libssl-dev \ 101 | zlib1g-dev \ 102 | build-essential && \ 103 | apt autoremove -y 104 | 105 | FROM $IMAGE 106 | 107 | COPY --from=builder /usr/local/bin /usr/local/bin 108 | COPY --from=builder /usr/local/nginx /usr/local/nginx 109 | COPY --from=builder /usr/local/lib /usr/local/lib 110 | 111 | RUN apt-get update && \ 112 | apt-get install -y \ 113 | ca-certificates \ 114 | git \ 115 | procps \ 116 | libpcre3 \ 117 | openssl \ 118 | libssl1.1 \ 119 | zlib1g \ 120 | v4l-utils \ 121 | libv4l-0 \ 122 | alsa-utils \ 123 | libatomic1 124 | 125 | COPY . /restreamer 126 | WORKDIR /restreamer 127 | 128 | RUN cd /restreamer && \ 129 | npm install -g grunt-cli nodemon eslint && \ 130 | npm install && \ 131 | grunt build && \ 132 | npm prune --production && \ 133 | npm cache verify && \ 134 | npm uninstall -g grunt-cli nodemon eslint && \ 135 | npm prune --production && \ 136 | apt-get remove -y \ 137 | git \ 138 | curl && \ 139 | apt autoremove -y 140 | 141 | EXPOSE 8080 142 | EXPOSE 8181 143 | 144 | VOLUME ["/restreamer/db"] 145 | 146 | CMD ["./run.sh"] 147 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE=amd64/debian:10.4-slim 2 | 3 | FROM $IMAGE as builder 4 | 5 | MAINTAINER datarhei 6 | 7 | ARG NASM_VERSION=2.14.02 8 | ARG LAME_VERSION=3.100 9 | ARG FFMPEG_VERSION=4.3.1 10 | ARG NGINX_VERSION=1.18.0 11 | ARG NGINXRTMP_VERSION=1.2.1 12 | ARG NODE_VERSION=12.16.3 13 | 14 | ENV SRC="/usr/local/" \ 15 | LD_LIBRARY_PATH="/usr/local/lib" \ 16 | PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" 17 | 18 | RUN apt-get update && \ 19 | apt-get install -y \ 20 | pkg-config \ 21 | curl \ 22 | libpcre3-dev \ 23 | libtool \ 24 | libssl-dev \ 25 | zlib1g-dev \ 26 | libasound2-dev \ 27 | build-essential 28 | 29 | # nasm 30 | RUN mkdir -p /dist && cd /dist && \ 31 | curl -OL "https://www.nasm.us/pub/nasm/releasebuilds/${NASM_VERSION}/nasm-${NASM_VERSION}.tar.xz" && \ 32 | tar -xvJ -f nasm-${NASM_VERSION}.tar.xz && \ 33 | cd nasm-${NASM_VERSION} && \ 34 | ./configure && \ 35 | make -j$(nproc) && \ 36 | make install 37 | 38 | # x264 39 | RUN mkdir -p /dist && cd /dist && \ 40 | curl -OL https://code.videolan.org/videolan/x264/-/archive/stable/x264-stable.tar.bz2 && \ 41 | tar -xvj -f x264-stable.tar.bz2 && \ 42 | cd x264-stable && \ 43 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-shared && \ 44 | make -j$(nproc) && \ 45 | make install 46 | 47 | # libmp3lame 48 | RUN mkdir -p /dist && cd /dist && \ 49 | curl -OL "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ 50 | tar -xvz -f lame-${LAME_VERSION}.tar.gz && \ 51 | cd lame-${LAME_VERSION} && \ 52 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static --enable-nasm && \ 53 | make -j$(nproc) && \ 54 | make install 55 | 56 | # ffmpeg && patch 57 | COPY ./contrib/ffmpeg /dist/restreamer/contrib/ffmpeg 58 | 59 | RUN mkdir -p /dist && cd /dist && \ 60 | curl -OL "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ 61 | tar -xvz -f ffmpeg-${FFMPEG_VERSION}.tar.gz && \ 62 | cd ffmpeg-${FFMPEG_VERSION} && \ 63 | patch -p1 < /dist/restreamer/contrib/ffmpeg/bitrate.patch && \ 64 | ./configure \ 65 | --bindir="${SRC}/bin" \ 66 | --extra-cflags="-I${SRC}/include" \ 67 | --extra-ldflags="-L${SRC}/lib" \ 68 | --prefix="${SRC}" \ 69 | --enable-nonfree \ 70 | --enable-gpl \ 71 | --enable-version3 \ 72 | --enable-libmp3lame \ 73 | --enable-libx264 \ 74 | --enable-openssl \ 75 | --enable-postproc \ 76 | --enable-small \ 77 | --enable-static \ 78 | --disable-debug \ 79 | --disable-doc \ 80 | --disable-shared && \ 81 | make -j$(nproc) && \ 82 | make install 83 | 84 | # nginx-rtmp 85 | RUN mkdir -p /dist && cd /dist && \ 86 | curl -OL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 87 | tar -xvz -f "nginx-${NGINX_VERSION}.tar.gz" && \ 88 | curl -OL "https://github.com/arut/nginx-rtmp-module/archive/v${NGINXRTMP_VERSION}.tar.gz" && \ 89 | tar -xvz -f "v${NGINXRTMP_VERSION}.tar.gz" && \ 90 | sed -i"" -e '/case ESCAPE:/i /* fall through */' nginx-rtmp-module-${NGINXRTMP_VERSION}/ngx_rtmp_eval.c && \ 91 | cd nginx-${NGINX_VERSION} && \ 92 | ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --add-module=/dist/nginx-rtmp-module-${NGINXRTMP_VERSION} && \ 93 | make -j$(nproc) && \ 94 | make install 95 | 96 | # node.js 97 | RUN mkdir -p /dist && cd /dist && \ 98 | curl -OL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" && \ 99 | tar -xvJ -f "node-v${NODE_VERSION}-linux-x64.tar.xz" && \ 100 | cd node-v${NODE_VERSION}-linux-x64 && \ 101 | cp -R bin /usr/local && \ 102 | cp -R lib /usr/local 103 | 104 | FROM $IMAGE 105 | 106 | COPY --from=builder /usr/local/bin /usr/local/bin 107 | COPY --from=builder /usr/local/nginx /usr/local/nginx 108 | COPY --from=builder /usr/local/lib /usr/local/lib 109 | 110 | RUN apt-get update && \ 111 | apt-get install -y \ 112 | ca-certificates \ 113 | git \ 114 | procps \ 115 | libpcre3 \ 116 | openssl \ 117 | libssl1.1 \ 118 | zlib1g \ 119 | v4l-utils \ 120 | libv4l-0 \ 121 | alsa-utils 122 | 123 | COPY . /restreamer 124 | WORKDIR /restreamer 125 | 126 | RUN cd /restreamer && \ 127 | npm install -g grunt-cli nodemon eslint && \ 128 | npm install && \ 129 | grunt build && \ 130 | npm prune --production && \ 131 | npm cache verify && \ 132 | npm uninstall -g grunt-cli nodemon eslint && \ 133 | npm prune --production && \ 134 | apt-get remove -y \ 135 | git \ 136 | curl && \ 137 | apt autoremove -y 138 | 139 | EXPOSE 8080 140 | EXPOSE 8181 141 | 142 | VOLUME ["/restreamer/db"] 143 | 144 | CMD ["./run.sh"] 145 | -------------------------------------------------------------------------------- /Dockerfile-rpi: -------------------------------------------------------------------------------- 1 | ARG IMAGE=debian:buster-slim 2 | 3 | FROM $IMAGE as builder 4 | 5 | MAINTAINER datarhei 6 | 7 | ARG LAME_VERSION=3.100 8 | ARG FFMPEG_VERSION=4.1.5 9 | ARG NGINX_VERSION=1.18.0 10 | ARG NGINXRTMP_VERSION=1.2.1 11 | ARG NODE_VERSION=12.16.3 12 | 13 | ENV SRC="/usr/local/" \ 14 | LD_LIBRARY_PATH="/usr/local/lib" \ 15 | PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/opt/vc/lib/pkgconfig" 16 | 17 | RUN apt-get update && \ 18 | apt-get install -y \ 19 | pkg-config \ 20 | cmake \ 21 | libomxil-bellagio-dev \ 22 | git \ 23 | curl \ 24 | libpcre3-dev \ 25 | libtool \ 26 | libssl-dev \ 27 | zlib1g-dev \ 28 | libasound2-dev \ 29 | sudo \ 30 | xz-utils \ 31 | build-essential 32 | 33 | # Build rpi userland 34 | RUN mkdir -p /dist && cd /dist && git clone --depth 1 https://github.com/raspberrypi/userland.git 35 | RUN cd /dist/userland && ./buildme 36 | 37 | # Required to link deps 38 | RUN echo "/opt/vc/lib" > /etc/ld.so.conf.d/00-vmcs.conf 39 | RUN ldconfig 40 | 41 | # x264 42 | RUN mkdir -p /dist && cd /dist && \ 43 | curl -OL https://code.videolan.org/videolan/x264/-/archive/stable/x264-stable.tar.bz2 && \ 44 | tar -xvj -f x264-stable.tar.bz2 && \ 45 | cd x264-stable && \ 46 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-shared && \ 47 | make -j$(nproc) && \ 48 | make install 49 | 50 | # libmp3lame 51 | RUN mkdir -p /dist && cd /dist && \ 52 | curl -OL "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ 53 | tar -xvz -f lame-${LAME_VERSION}.tar.gz && \ 54 | cd lame-${LAME_VERSION} && \ 55 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static && \ 56 | make -j$(nproc) && \ 57 | make install 58 | 59 | # ffmpeg && patch 60 | COPY ./contrib/ffmpeg /dist/restreamer/contrib/ffmpeg 61 | 62 | RUN mkdir -p /dist && cd /dist && \ 63 | curl -OL "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" && \ 64 | tar -xv -f ffmpeg-${FFMPEG_VERSION}.tar.xz 65 | 66 | RUN cd /dist/ffmpeg-${FFMPEG_VERSION} && \ 67 | patch -p1 < /dist/restreamer/contrib/ffmpeg/bitrate.patch && \ 68 | ./configure \ 69 | --bindir="${SRC}/bin" \ 70 | --extra-cflags="-I${SRC}/include" \ 71 | --extra-ldflags="-L${SRC}/lib" \ 72 | --prefix="${SRC}" \ 73 | --enable-nonfree \ 74 | --enable-gpl \ 75 | --enable-version3 \ 76 | --enable-libmp3lame \ 77 | --enable-libx264 \ 78 | --enable-omx \ 79 | --enable-omx-rpi \ 80 | --enable-mmal \ 81 | --enable-openssl \ 82 | --enable-postproc \ 83 | --enable-small \ 84 | --enable-static \ 85 | --disable-debug \ 86 | --disable-doc \ 87 | --disable-shared && \ 88 | make -j$(nproc) && \ 89 | make install 90 | 91 | # nginx-rtmp 92 | RUN mkdir -p /dist && cd /dist && \ 93 | curl -OL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 94 | tar -xvz -f "nginx-${NGINX_VERSION}.tar.gz" && \ 95 | curl -OL "https://github.com/arut/nginx-rtmp-module/archive/v${NGINXRTMP_VERSION}.tar.gz" && \ 96 | tar -xvz -f "v${NGINXRTMP_VERSION}.tar.gz" && \ 97 | sed -i"" -e '/case ESCAPE:/i /* fall through */' nginx-rtmp-module-${NGINXRTMP_VERSION}/ngx_rtmp_eval.c && \ 98 | cd nginx-${NGINX_VERSION} && \ 99 | ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --add-module=/dist/nginx-rtmp-module-${NGINXRTMP_VERSION} && \ 100 | make -j$(nproc) && \ 101 | make install 102 | 103 | # node.js 104 | RUN mkdir -p /dist && cd /dist && \ 105 | curl -OL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-armv7l.tar.xz" && \ 106 | tar -xvJ -f "node-v${NODE_VERSION}-linux-armv7l.tar.xz" && \ 107 | cd node-v${NODE_VERSION}-linux-armv7l && \ 108 | cp -R bin /usr/local && \ 109 | cp -R lib /usr/local 110 | 111 | FROM $IMAGE 112 | 113 | COPY --from=builder /usr/local/bin /usr/local/bin 114 | COPY --from=builder /usr/local/nginx /usr/local/nginx 115 | COPY --from=builder /usr/local/lib /usr/local/lib 116 | 117 | RUN echo "/opt/vc/lib" > /etc/ld.so.conf.d/00-vmcs.conf 118 | RUN ldconfig 119 | 120 | RUN apt-get update && \ 121 | apt-get install -y \ 122 | ca-certificates \ 123 | libomxil-bellagio-dev \ 124 | git \ 125 | procps \ 126 | libpcre3 \ 127 | openssl \ 128 | libssl1.1 \ 129 | zlib1g \ 130 | v4l-utils \ 131 | libv4l-0 \ 132 | alsa-utils \ 133 | libatomic1 134 | 135 | COPY . /restreamer 136 | 137 | WORKDIR /restreamer 138 | 139 | RUN cd /restreamer && \ 140 | npm install -g grunt-cli nodemon eslint && \ 141 | npm install && \ 142 | grunt build && \ 143 | npm prune --production && \ 144 | npm cache verify && \ 145 | npm uninstall -g grunt-cli nodemon eslint && \ 146 | npm prune --production && \ 147 | apt-get remove -y \ 148 | git \ 149 | curl && \ 150 | apt autoremove -y 151 | 152 | ENV DEVICE=raspi 153 | 154 | EXPOSE 8080 155 | EXPOSE 8181 156 | 157 | VOLUME ["/restreamer/db"] 158 | 159 | CMD ["./run.sh"] 160 | -------------------------------------------------------------------------------- /src/webserver/public/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Restreamer 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // path to store the transpiled es6 files 4 | const transpiledPath = "src/webserver/transpiled/"; 5 | 6 | const files = { 7 | //workaround to keep correct order 8 | transpiledFrontendJs: [ 9 | `${transpiledPath}/webserver/public/scripts/App.js`, 10 | `${transpiledPath}/webserver/public/scripts/App.Config.js`, 11 | 12 | `${transpiledPath}/webserver/public/scripts/Main/MainModule.js`, 13 | `${transpiledPath}/webserver/public/scripts/Main/MainController.js`, 14 | 15 | `${transpiledPath}/webserver/public/scripts/Login/LoginModule.js`, 16 | `${transpiledPath}/webserver/public/scripts/Login/LoginController.js`, 17 | 18 | `${transpiledPath}/webserver/public/scripts/Header/HeaderModule.js`, 19 | `${transpiledPath}/webserver/public/scripts/Header/HeaderController.js`, 20 | `${transpiledPath}/webserver/public/scripts/Header/HeaderDirective.js`, 21 | 22 | `${transpiledPath}/webserver/public/scripts/Footer/FooterModule.js`, 23 | `${transpiledPath}/webserver/public/scripts/Footer/FooterController.js`, 24 | `${transpiledPath}/webserver/public/scripts/Footer/FooterDirective.js`, 25 | 26 | `${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingInterfaceModule.js`, 27 | `${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingStatusController.js`, 28 | `${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingStatusDirective.js`, 29 | 30 | `${transpiledPath}/webserver/public/scripts/Shared/LoggerService.js`, 31 | `${transpiledPath}/webserver/public/scripts/Shared/WebsocketsService.js` 32 | ], 33 | es6Src: [ 34 | 'webserver/public/scripts/**/*.js' 35 | ], 36 | stylesheets: ['src/webserver/public/css/*.css'] 37 | }; 38 | 39 | module.exports = function (grunt) { 40 | 41 | // Project Configuration 42 | grunt.initConfig({ 43 | 44 | /* 45 | Config for shell commands 46 | */ 47 | shell: { 48 | start: { 49 | command: 'npm start' 50 | }, 51 | removeTempTranspilingFolder: { 52 | command: `rm -Rf ${transpiledPath}` 53 | }, 54 | createTempTranspilingFolder: { 55 | command: `mkdir ${transpiledPath}` 56 | }, 57 | eslint: { 58 | command: 'eslint src/*' 59 | } 60 | }, 61 | 62 | /* 63 | Config for Babel compiling 64 | */ 65 | babel: { 66 | options: { 67 | sourceMap: true, 68 | presets: ['env'] 69 | }, 70 | all: { 71 | files: [ 72 | { 73 | expand: true, 74 | cwd: 'src/', 75 | src: '<%= es6Src %>', 76 | dest: transpiledPath 77 | } 78 | ] 79 | } 80 | }, 81 | 82 | /* 83 | Config for eslinter 84 | */ 85 | eslint: { 86 | all: ['src/**/*.js'], 87 | options: { 88 | configFile: '.eslintrc.json' 89 | } 90 | }, 91 | 92 | /* 93 | config for css linter 94 | */ 95 | csslint: { 96 | options: { 97 | csslintrc: '.csslintrc' 98 | }, 99 | all: { 100 | src: ['src/webserver/public/css/*.css'] 101 | } 102 | }, 103 | 104 | /* 105 | uglify and minify frontend javascript 106 | */ 107 | uglify: { 108 | production: { 109 | options: { 110 | mangle: true 111 | }, 112 | files: { 113 | 'src/webserver/public/dist/application.min.js': 'src/webserver/public/dist/application.js' 114 | } 115 | } 116 | }, 117 | 118 | /* 119 | minify css files 120 | */ 121 | cssmin: { 122 | combine: { 123 | files: { 124 | 'src/webserver/public/css/restreamer.min.css': '<%= stylesheets %>' 125 | } 126 | } 127 | }, 128 | 129 | /* 130 | produces one file from all fontend javascript bewaring DI naming of angular 131 | */ 132 | ngAnnotate: { 133 | production: { 134 | files: { 135 | 'src/webserver/public/dist/application.js': '<%= transpiledFrontendJs %>' 136 | } 137 | } 138 | } 139 | }); 140 | 141 | /* 142 | Load NPM tasks 143 | */ 144 | require('load-grunt-tasks')(grunt); 145 | grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function () { 146 | grunt.config.set('es6Src', files.es6Src); 147 | grunt.config.set('transpiledFrontendJs', files.transpiledFrontendJs); 148 | grunt.config.set('stylesheets', files.stylesheets); 149 | }); 150 | grunt.loadNpmTasks('grunt-shell'); 151 | grunt.loadNpmTasks('grunt-contrib-watch'); 152 | 153 | /* 154 | Helper tasks to keep overview 155 | */ 156 | // lint 157 | grunt.registerTask('lint', ['csslint', 'shell:eslint']); 158 | // clear old transpile folder and create new one 159 | grunt.registerTask('clearOldBuild', ['shell:removeTempTranspilingFolder', 'shell:createTempTranspilingFolder']); 160 | // minify the frontend files 161 | grunt.registerTask('minifyFrontendFiles', ['cssmin', 'ngAnnotate', 'uglify']); 162 | 163 | /* 164 | Build Tasks 165 | */ 166 | grunt.registerTask('build', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles', 'shell:removeTempTranspilingFolder']); 167 | 168 | /* 169 | Just Compile 170 | */ 171 | grunt.registerTask('compile', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles']); 172 | 173 | /* 174 | Run Tasks 175 | */ 176 | grunt.registerTask('run', ['shell:start']); 177 | 178 | }; 179 | -------------------------------------------------------------------------------- /src/webserver/public/css/restreamer.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0 5 | } 6 | 7 | body { 8 | color: #fff; 9 | } 10 | 11 | .container-fluid { 12 | height: 100%; 13 | display: table; 14 | width: 100%; 15 | padding: 0; 16 | background: #3d3d39 url(../images/bg.png) no-repeat fixed center center; 17 | -webkit-background-size: cover; 18 | -moz-background-size: cover; 19 | -o-background-size: cover; 20 | background-size: cover; 21 | } 22 | 23 | .container-body { 24 | float: none; 25 | margin: 0 auto; 26 | max-width: 600px; 27 | border: 1px solid #2c2c28; 28 | border-radius: 10px; 29 | padding: 10px 30px; 30 | } 31 | 32 | .row-fluid { 33 | height: 100%; 34 | display: table-cell; 35 | vertical-align: middle; 36 | } 37 | 38 | hr { 39 | border-bottom: 1px solid #454543; 40 | border-top: 1px solid #2b2b2a; 41 | } 42 | 43 | .footer { 44 | padding-bottom: 25px; 45 | } 46 | 47 | h1, h2, h3, h4, h5, h6 { 48 | color: #3daa48; 49 | font-weight: 300; 50 | } 51 | 52 | h1 { 53 | text-shadow: 0 1px #000; 54 | font-weight: 100; 55 | margin-bottom: 30px; 56 | font-size: 46px; 57 | } 58 | 59 | a { 60 | color: #fff; 61 | text-decoration: none; 62 | cursor: pointer; 63 | } 64 | 65 | a:visited { 66 | color: #fff; 67 | text-decoration: none; 68 | } 69 | 70 | a:hover, a:active, a:focus { 71 | color: #3daa48; 72 | text-decoration: none; 73 | } 74 | 75 | table { 76 | width: 100%; 77 | margin-bottom: 22px; 78 | } 79 | 80 | th { 81 | height: 26px; 82 | width: 150px; 83 | } 84 | 85 | td { 86 | padding: 2px 0px 2px 0px; 87 | } 88 | 89 | .locales { 90 | color: #60605e; 91 | text-decoration: none; 92 | } 93 | 94 | .locales.active { 95 | color: #fff; 96 | text-decoration: none; 97 | } 98 | 99 | .locales:hover, .locales:focus { 100 | color: #3daa48; 101 | text-decoration: none; 102 | } 103 | 104 | /* buttons */ 105 | 106 | .btn { 107 | margin-top: -5px; 108 | } 109 | 110 | .btn-success { 111 | background-color: #3d8142; 112 | border-color: #3d8142; 113 | } 114 | 115 | .btn-danger { 116 | background-color: #ac5647; 117 | border-color: #ac5647; 118 | } 119 | 120 | .btn-default.disabled, .btn-default.disabled.active, .btn-default.disabled.focus, .btn-default.disabled:active, .btn-default.disabled:focus, .btn-default.disabled:hover, .btn-default[disabled], .btn-default[disabled].active, .btn-default[disabled].focus, .btn-default[disabled]:active, .btn-default[disabled]:focus, .btn-default[disabled]:hover, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default.active, fieldset[disabled] .btn-default.focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default:hover { 121 | background-color: #434341; 122 | border-color: #434341; 123 | } 124 | 125 | .btn.disabled, .btn[disabled], fieldset[disabled] .btn { 126 | cursor: not-allowed; 127 | filter: alpha(opacity=1.0); 128 | -webkit-box-shadow: none; 129 | box-shadow: none; 130 | opacity: 1.0; 131 | color: #373734; 132 | } 133 | 134 | .form-control[disabled] { 135 | color: #60605e; 136 | font-weight: bold; 137 | background-color: #373734; 138 | border: 2px solid #434341; 139 | } 140 | 141 | .input { 142 | color: #fff; 143 | background-color: #373734; 144 | border: 2px solid #434341; 145 | } 146 | 147 | .input-danger { 148 | color: #fff; 149 | background-color: #ad5544; 150 | border: 2px solid #b96e5f; 151 | } 152 | 153 | .input:hover, .input:active, .input:active:hover, .input:active:focus, .input:link, .input:focus { 154 | border-color: #3daa48; 155 | } 156 | 157 | .select { 158 | color: #fff; 159 | height: 26px; 160 | width: 100%; 161 | background-color: #373734; 162 | border: 2px solid #434341; 163 | } 164 | 165 | .width-auto { 166 | width: auto !important; 167 | } 168 | 169 | .progress-bar-success { 170 | background-color: #3d8142; 171 | color: #fff; 172 | } 173 | 174 | .progress-bar-info { 175 | background-color: #4d4d4b; 176 | color: #fff; 177 | } 178 | 179 | .progress-bar-danger { 180 | background-color: #ac5647; 181 | color: #fff; 182 | } 183 | 184 | .container .jumbotron, .container-fluid .jumbotron, .jumbotron { 185 | margin-top: -4px; 186 | margin-bottom: 18px; 187 | text-align: center; 188 | padding: 18px 20px; 189 | } 190 | 191 | .player-link { 192 | margin-bottom: -20px; 193 | } 194 | 195 | .label { 196 | display: inline-block; 197 | max-width: 100%; 198 | margin-bottom: 5px; 199 | font-weight: 200; 200 | font-size: 18px; 201 | color: #fff; 202 | } 203 | 204 | label { 205 | font-weight: normal; 206 | font-size: 14px; 207 | } 208 | 209 | .checkbox label, .radio label { 210 | font-size: 14px; 211 | } 212 | 213 | .modal-content { 214 | background-color: #373734; 215 | } 216 | 217 | .modal-header { 218 | border-bottom: 0 solid #e5e5e5; 219 | } 220 | 221 | .modal-footer { 222 | border-top: 0 solid #e5e5e5; 223 | text-align: left; 224 | } 225 | 226 | .close { 227 | float: right; 228 | font-size: 25px; 229 | font-weight: 300; 230 | line-height: 1; 231 | color: #fff; 232 | text-shadow: 0 1px 0 #000; 233 | filter: alpha(opacity=20); 234 | opacity: 1.0; 235 | } 236 | 237 | pre { 238 | display: block; 239 | padding: 10px; 240 | margin: 0 0 10px; 241 | font-size: 12px; 242 | line-height: 1.42857143; 243 | color: #fff; 244 | word-break: break-all; 245 | word-wrap: break-word; 246 | background-color: #3d3d39; 247 | border: 1px solid #2c2c28; 248 | border-radius: 4px; 249 | } 250 | 251 | .footer .links a { 252 | margin-left: 10px; 253 | } 254 | 255 | .underline { 256 | text-decoration: underline; 257 | } 258 | 259 | .icon16 { 260 | font-size: 16px; 261 | } 262 | 263 | .icon14 { 264 | font-size: 14px; 265 | } 266 | 267 | .green { 268 | color: #3daa48; 269 | } 270 | 271 | .color-picker { 272 | border-radius: 4px; 273 | background-color: #373734; 274 | border: 2px solid rgba(255,255,255,0.15); 275 | } 276 | -------------------------------------------------------------------------------- /src/webserver/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://github.com/datarhei/restreamer 3 | * @copyright 2015 datarhei.org 4 | * @license Apache-2.0 5 | */ 6 | 'use strict'; 7 | 8 | // express 9 | const express = require('express'); 10 | const session = require('express-session'); 11 | const cookie = require('cookie'); 12 | const cookieParser = require('cookie-parser'); 13 | const bodyParser = require('body-parser'); 14 | const compression = require('compression'); 15 | 16 | // other 17 | const path = require('path'); 18 | const Q = require('q'); 19 | const crypto = require('crypto'); 20 | 21 | // modules 22 | const logger = require.main.require('./classes/Logger')('Webserver'); 23 | const indexRouter = require('./controllers/index'); 24 | const apiV1 = require('./controllers/api/v1'); 25 | 26 | // middleware 27 | const expressLogger = require('./middleware/expressLogger'); 28 | 29 | /** 30 | * Class for the ReStreamer webserver, powered by express.js 31 | */ 32 | class RestreamerExpressApp { 33 | 34 | /** 35 | * constructs a new express app with prod or dev config 36 | */ 37 | constructor () { 38 | this.app = express(); 39 | this.secretKey = crypto.randomBytes(16).toString('hex'); 40 | this.sessionKey = 'restreamer-session'; 41 | this.sessionStore = new session.MemoryStore(); 42 | 43 | if (process.env.RS_NODEJS_ENV === 'dev') { 44 | this.initDev(); 45 | } else { 46 | this.initProd(); 47 | } 48 | } 49 | 50 | /** 51 | * use sessions for the express app 52 | */ 53 | useSessions () { 54 | this.app.use(session({ 55 | 'resave': true, 56 | 'saveUninitialized': false, 57 | 'key': this.sessionKey, 58 | 'secret': this.secretKey, 59 | 'unset': 'destroy', 60 | 'store': this.sessionStore 61 | })); 62 | } 63 | 64 | /** 65 | * add automatic parsers for the body 66 | */ 67 | addParsers () { 68 | this.app.use(bodyParser.json()); 69 | this.app.use(cookieParser()); 70 | } 71 | 72 | /** 73 | * add content compression on responses 74 | */ 75 | addCompression () { 76 | this.app.use(compression()); 77 | } 78 | 79 | /** 80 | * add express logger 81 | */ 82 | addExpressLogger () { 83 | this.app.use('/', expressLogger); 84 | } 85 | 86 | /** 87 | * beautify json response 88 | */ 89 | beautifyJSONResponse () { 90 | this.app.set('json spaces', 4); 91 | } 92 | 93 | /** 94 | * create a promise to check when websockets are ready for bindings 95 | */ 96 | createPromiseForWebsockets () { 97 | this.app.set('websocketsReady', Q.defer()); 98 | } 99 | 100 | /** 101 | * add the restreamer routes 102 | */ 103 | addRoutes () { 104 | indexRouter(this.app); 105 | this.app.use('/v1', apiV1); 106 | } 107 | 108 | /** 109 | * add 404 error handling on pages, that have not been found 110 | */ 111 | add404ErrorHandling () { 112 | this.app.use((req, res, next) => { 113 | var err = new Error('Not Found ' + req.url); 114 | err.status = 404; 115 | next(err); 116 | }); 117 | } 118 | 119 | /** 120 | * add ability for internal server errors 121 | */ 122 | add500ErrorHandling () { 123 | this.app.use((err, req, res, next) => { 124 | logger.error(err); 125 | res.status(err.status || 500); 126 | res.send({ 127 | 'message': err.message, 128 | 'error': {} 129 | }); 130 | }); 131 | } 132 | 133 | /** 134 | * enable websocket session validation 135 | */ 136 | secureSockets () { 137 | this.app.get('io').set('authorization', (handshakeData, accept) => { 138 | if (handshakeData.headers.cookie) { 139 | this.sessionStore.get(cookieParser.signedCookie( 140 | cookie.parse(handshakeData.headers.cookie)[this.sessionKey], this.secretKey 141 | ), (err, s) => { 142 | if (!err && s && s.authenticated) { 143 | return accept(null, true); 144 | } 145 | }); 146 | } else { 147 | return accept(null, false); 148 | } 149 | }); 150 | } 151 | 152 | /** 153 | * start the webserver and open the websocket 154 | * @returns {*|promise} 155 | */ 156 | startWebserver () { 157 | var deferred = Q.defer(); 158 | var server = null; 159 | 160 | logger.info('Starting ...'); 161 | this.app.set('port', process.env.RS_NODEJS_PORT); 162 | server = this.app.listen(this.app.get('port'), ()=> { 163 | this.app.set('io', require('socket.io')(server, {path: '/socket.io'})); 164 | this.secureSockets(); 165 | this.app.set('server', server.address()); 166 | 167 | // promise to avoid ws binding before the webserver has been started 168 | this.app.get('websocketsReady').resolve(this.app.get('io')); 169 | logger.info('Running on port ' + process.env.RS_NODEJS_PORT); 170 | deferred.resolve(server.address().port); 171 | }); 172 | 173 | return deferred.promise; 174 | } 175 | 176 | /** 177 | * stuff that have always to be added to the webapp 178 | */ 179 | initAlways () { 180 | this.useSessions(); 181 | this.addParsers(); 182 | this.addCompression(); 183 | this.addExpressLogger(); 184 | this.beautifyJSONResponse(); 185 | this.createPromiseForWebsockets(); 186 | this.addRoutes(); 187 | } 188 | 189 | /** 190 | * prod config for the express app 191 | */ 192 | initProd () { 193 | logger.debug('Init webserver with PROD environment'); 194 | this.initAlways(); 195 | this.app.get('/', (req, res)=> { 196 | res.sendFile(path.join(global.__public, 'index.prod.html')); 197 | }); 198 | this.add404ErrorHandling(); 199 | this.add500ErrorHandling(); 200 | } 201 | 202 | /** 203 | * dev config for the express app 204 | */ 205 | initDev () { 206 | logger.debug('Init webserver with DEV environment'); 207 | this.initAlways(); 208 | this.app.get('/', (req, res)=> { 209 | res.sendFile(path.join(global.__public, 'index.dev.html')); 210 | }); 211 | this.add404ErrorHandling(); 212 | this.add500ErrorHandling(); 213 | } 214 | } 215 | 216 | const restreamerApp = new RestreamerExpressApp(); 217 | 218 | module.exports = restreamerApp; 219 | -------------------------------------------------------------------------------- /src/classes/Logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file holds the code for the class Logger 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | const moment = require('moment-timezone'); 10 | const printf = require('printf'); 11 | const fs = require('fs'); 12 | 13 | const LEVEL_MUTE = 0; 14 | const LEVEL_ERROR = 1; 15 | const LEVEL_WARN = 2; 16 | const LEVEL_INFO = 3; 17 | const LEVEL_DEBUG = 4; 18 | 19 | /** 20 | * Class for logger 21 | */ 22 | class Logger { 23 | 24 | /** 25 | * check if the logger is muted 26 | * @returns {boolean} 27 | */ 28 | static isMuted () { 29 | return process.env.RS_LOGLEVEL === LEVEL_MUTE; 30 | } 31 | 32 | /** 33 | * construct a logger object 34 | * @param {string} context context of the log message (classname.methodname) 35 | */ 36 | constructor (context) { 37 | process.env.RS_LOGLEVEL = process.env.RS_LOGLEVEL || LEVEL_INFO; 38 | this.context = context; 39 | 40 | this.debuglog = null; 41 | 42 | if(process.env.RS_DEBUG == 'true') { 43 | let identifier = process.pid + '-' + process.platform + '-' + process.arch; 44 | try { 45 | this.debuglog = fs.openSync('/restreamer/src/webserver/public/debug/Restreamer-' + identifier + '.txt', 'a'); 46 | } catch(err) { 47 | this.debuglog = null; 48 | this.stdout('Error opening debug file ' + identifier + ': ' + err, context, 'INFO'); 49 | } finally { 50 | this.stdout('Enabled logging to ' + identifier, context, 'INFO'); 51 | } 52 | } 53 | } 54 | 55 | logline(message, context, type) { 56 | let time = moment().tz(process.env.RS_TIMEZONE).format('DD-MM-YYYY HH:mm:ss.SSS'); 57 | 58 | let logline = ''; 59 | if(context) { 60 | logline = printf('[%s] [%-5s] [%22s] %s', time, type, context, message); 61 | } else { 62 | logline = printf('[%s] [%-5s] %s', time, type, message); 63 | } 64 | 65 | return logline; 66 | } 67 | 68 | /** 69 | * print a message to stdout 70 | * @param {string} message 71 | * @param {string} context 72 | * @param {string} type 73 | */ 74 | stdout (message, context, type) { 75 | if(Logger.isMuted()) { 76 | return; 77 | } 78 | 79 | let logline = this.logline(message, context, type); 80 | 81 | process.stdout.write(logline + '\n'); 82 | } 83 | 84 | /** 85 | * print a message to a file 86 | * @param {string} message 87 | * @param {string} context 88 | * @param {string} type 89 | */ 90 | file (message, context, type) { 91 | let logline = this.logline(message, context, type); 92 | 93 | if(this.debuglog !== null) { 94 | fs.appendFile(this.debuglog, logline + '\n', 'utf8', (err) => { 95 | // ignore errors 96 | if(err) { 97 | return; 98 | } 99 | 100 | fs.fsync(this.debuglog, (err) => { 101 | return; 102 | }); 103 | 104 | return; 105 | }); 106 | } 107 | } 108 | 109 | /** 110 | * print an info message if LOG_LEVEL >= LEVEL_INFO 111 | * @param {string} message 112 | * @param {string=} context 113 | * @param {boolean=} alertGui 114 | */ 115 | info (message, context, alertGui) { 116 | var loggerContext = context; 117 | var loggerAlertGui = alertGui; 118 | 119 | if (typeof context === 'undefined') { 120 | loggerContext = this.context; 121 | } 122 | 123 | if (typeof alertGui === 'undefined') { 124 | loggerAlertGui = false; 125 | } 126 | 127 | if(process.env.RS_DEBUG == 'true') { 128 | this.file(message, loggerContext, 'INFO'); 129 | } 130 | 131 | if (process.env.RS_LOGLEVEL >= LEVEL_INFO) { 132 | return this.stdout(message, loggerContext, 'INFO'); 133 | } 134 | 135 | // todo: if alertGui is activated on frontend and websockets controller, insert emit here 136 | if (loggerAlertGui) { 137 | return; 138 | } 139 | } 140 | 141 | /** 142 | * print a warning message if LOG_LEVEL >= LEVEL_WARN 143 | * @param {string} message 144 | * @param {string=} context 145 | * @param {boolean=} alertGui 146 | */ 147 | warn (message, context, alertGui) { 148 | var loggerContext = context; 149 | var loggerAlertGui = alertGui; 150 | 151 | if (typeof context === 'undefined') { 152 | loggerContext = this.context; 153 | } 154 | 155 | if (typeof alertGui === 'undefined') { 156 | loggerAlertGui = false; 157 | } 158 | 159 | if(process.env.RS_DEBUG == 'true') { 160 | this.file(message, loggerContext, 'WARN'); 161 | } 162 | 163 | if (process.env.RS_LOGLEVEL >= LEVEL_WARN) { 164 | return this.stdout(message, loggerContext, 'WARN'); 165 | } 166 | 167 | // todo: if alertGui is activated on frontend and websockets controller, insert emit here 168 | if (loggerAlertGui) { 169 | return; 170 | } 171 | } 172 | 173 | /** 174 | * print a debug message if LOG_LEVEL >= LEVEL_DEBUG 175 | * @param {string} message 176 | * @param {string=} context 177 | * @param {boolean=} alertGui 178 | */ 179 | debug (message, context, alertGui) { 180 | var loggerContext = context; 181 | var loggerAlertGui = alertGui; 182 | 183 | if (typeof context === 'undefined') { 184 | loggerContext = this.context; 185 | } 186 | 187 | if (typeof alertGui === 'undefined') { 188 | loggerAlertGui = false; 189 | } 190 | 191 | if(process.env.RS_DEBUG == 'true') { 192 | this.file(message, loggerContext, 'DEBUG'); 193 | } 194 | 195 | if (process.env.RS_LOGLEVEL >= LEVEL_DEBUG) { 196 | return this.stdout(message, loggerContext, 'DEBUG'); 197 | } 198 | 199 | // todo: if alertGui is activated on frontend and websockets controller, insert emit here 200 | if (loggerAlertGui) { 201 | return; 202 | } 203 | } 204 | 205 | /** 206 | * print a debug message if LOG_LEVEL >= LEVEL_ERROR 207 | * sends a string to 208 | * @param {string} message 209 | * @param {string=} context 210 | * @param {boolean=} alertGui 211 | */ 212 | error (message, context, alertGui) { 213 | var loggerContext = context; 214 | var loggerAlertGui = alertGui; 215 | 216 | if (typeof context === 'undefined') { 217 | loggerContext = this.context; 218 | } 219 | 220 | if (typeof alertGui === 'undefined') { 221 | loggerAlertGui = false; 222 | } 223 | 224 | if(process.env.RS_DEBUG == 'true') { 225 | this.file(message, loggerContext, 'ERROR'); 226 | } 227 | 228 | if (process.env.RS_LOGLEVEL >= LEVEL_ERROR) { 229 | return this.stdout(message, loggerContext, 'ERROR'); 230 | } 231 | 232 | // todo: if alertGui is activated on frontend and websockets controller, insert emit here 233 | if (loggerAlertGui) { 234 | return; 235 | } 236 | } 237 | } 238 | 239 | // define log levels in logger class 240 | Logger.LEVEL_ERROR = LEVEL_ERROR; 241 | Logger.LEVEL_WARN = LEVEL_WARN; 242 | Logger.LEVEL_INFO = LEVEL_INFO; 243 | Logger.LEVEL_DEBUG = LEVEL_DEBUG; 244 | 245 | module.exports = (context) => { 246 | return new Logger(context); 247 | }; 248 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changes from 0.6.7 to 0.6.8 2 | 3 | * Fix probing RTSP sources 4 | 5 | ## Changes from 0.6.6 to 0.6.7 6 | 7 | * Fix error checking after probing (#200) 8 | * Fix restarting without SSL option set 9 | * Upgrade ffmpeg to version 4.3.1 10 | * Upgrade various node dependencies 11 | * Add RS_OUTPUTSTREAM (#195) (thanks to @jairbj) 12 | * Bind to IPv6 addresses (#198, #201) (thanks to @database64128) 13 | 14 | ## Changes from 0.6.5 to 0.6.6 15 | 16 | * Fix hanging ffprobe (#180) (thanks to Scott Robinson @scottgrobinson) 17 | * Fix node.js dependencies 18 | 19 | ## Changes from 0.6.4 to 0.6.5 20 | 21 | * Upgrade base Docker image to Debian "buster" 10.4 22 | * Upgrade node.js to 12.16.3 23 | * Upgrade nginx to 1.18.0 24 | * Upgrade arm32v6 Dockerfile (thanks to @rossbachp) 25 | * Add audio channels, layout, and sampling environment variables for RaspiCam and USB cameras 26 | * Add automatic HTTPS support with Docker Compose (pull request #161, thanks to @wdalmijn) 27 | 28 | ## Changes from 0.6.3 to 0.6.4 29 | 30 | * Fix compatibility of stream from USB camera 31 | 32 | ## Changes from 0.6.2 to 0.6.3 33 | 34 | * Fix timeout for RTSP streams 35 | 36 | ## Changes from 0.6.1 to 0.6.2 37 | 38 | * Fix audio stream mappings 39 | * Fix audio "auto" setting 40 | 41 | ## Changes from 0.6.0 to 0.6.1 42 | 43 | * Fix occasional reset of configuration 44 | * Fix pushing to external HLS server 45 | 46 | ## Changes from 0.5.0 to 0.6.0 47 | 48 | * Upgrade base Docker image to Debian "buster" 49 | * Upgrade node.js to 12.14.1 (LTS) 50 | * Upgrade nginx to 1.16.1 51 | * Upgrade x264 to stable branch 52 | * Optimize ffmpeg commands 53 | * Add locale sl_SI (Slovenian) to UI 54 | * Add support for pushing the stream to an external HLS server 55 | 56 | ## Changes from 0.4.0 to 0.5.0 57 | 58 | * Upgrade ffmpeg to 4.1.5 59 | * Upgrade node.js dependencies 60 | * Add locales fr_FR, it_IT, pt_PT 61 | * Customize player 62 | 63 | ## Changes from 0.3.0 to 0.4.0 64 | 65 | * Allow audio for raspicam from alsa audio device 66 | * Optionally (re-)encode the source stream to H.264 67 | * Handle multi-bitrate source streams 68 | * Add Twitch streaming guide 69 | * Introduce RS_INPUTSTREAM environment variable 70 | 71 | ## Changes from 0.2.0 to 0.3.0 72 | 73 | * Use relative paths for loading all assets in the GUI 74 | * Update FFmpeg to 4.1.3 75 | * Update NGINX to 1.14.2 76 | * Update node.js to 10.15.3 77 | * Update nasm to 2.14.02 78 | 79 | ## Changes from 0.1.3 to 0.2.0 80 | 81 | * Add HTTPS support 82 | https://datarhei.github.io/restreamer/docs/guides-https.html 83 | 84 | ## Changes from 0.1.2 to 0.1.3 85 | 86 | * Add stats switch to embeddable player 87 | * Remove bower and add dist files of required JS libraries 88 | * Add mute switch to the embeddable player 89 | * Add Slovenian translation (thanks to Sebastjan) 90 | 91 | ## Changes from 0.1.1 to 0.1.2 92 | 93 | * Improve stale stream check 94 | * Add Polish translations (thanks to Emil) 95 | * Add sample docker-compose.yml 96 | 97 | ## Changes from 0.1.0 to 0.1.1 98 | 99 | * Fix respecting the deprecated environment variables RESTREAMER_USERNAME and RESTREAMER_PASSWORD 100 | * Fix links to Github pages 101 | * Fix snapshot drift 102 | * Fix initialization of environment variables 103 | * Minor fixes in logging output 104 | * Improve RS_AUDIO setting handling 105 | * Rename misleading armhf Dockerfile to arm32v7 106 | * Add Dockerfile for arm32v6 107 | 108 | ## Changes from 0.1.0-rc7 to 0.1.0 109 | 110 | * Update FFmpeg to 4.1 111 | * Update NGINX to 1.14.1 112 | * Update NGINXRTMP module to 1.2.1 113 | * Update node.js to 10.13.0 114 | * Update NPM packages 115 | * Fix detection of public IP 116 | * Disable FFmpeg error detection for a VLC-like behaviour 117 | * Add optional RTMP (RS_TOKEN environment variable) token authentification 118 | * Add health check to NGINX 119 | * Allow HTTP/HTTPS (HLS) and RTMPT/RTMPS addresses as video source 120 | * Add optional autostart for the player 121 | * Add Spanish translation for the GUI 122 | * Add aarch64/arm64v8 Dockerfile 123 | * Reduce docker image size 124 | * Fix snapshot interval parsing 125 | * Allow to disable snapshot 126 | * Remove stream connection retry limits 127 | * Add stale stream detection with automatic restart 128 | * Add RS_DEBUG environment variable to store debug output to /debug 129 | * Add RS_AUDIO environment variable to control audio processing 130 | * Redesign logging output and logging messages 131 | * Rename environment variables (old environment variables are still supported but will be deprecated in the future) 132 | * RS_NODE_PORT to RS_NODEJS_PORT 133 | * RS_NODE_ENV to RS_NODEJS_ENV 134 | * RS_LOGGER_LEVEL to RS_LOGLEVEL 135 | * MODE to RS_MDOE 136 | * RS_SNAPSHOT_REFRESH_INTERVAL to RS_SNAPSHOT_INTERVAL 137 | * Add options to control Raspberry Pi cameras (RS_MODE=RASPICAM) 138 | * Add options to control USB cameras (RS_MODE=USBCAM) 139 | 140 | ## Changes from 0.1.0-rc7 to 0.1.0-rc.7.1 141 | 142 | * fixed Kitematic auth failure 143 | 144 | ## Changes from 0.1.0-RC6.1 to 0.1.0-rc.7 145 | 146 | * security improvements 147 | * FFmpeg and NGINX optimizations 148 | * fixed update check 149 | * added semantic versioning 150 | * several small bugfixes and improvements 151 | * updated dependencies 152 | * added Aarch64 Docker image and reduced Docker layers 153 | 154 | ## Changes from 0.1.0-RC6 to 0.1.0-RC6.1 155 | 156 | * fixed external streaming with RTSP over TCP input option 157 | 158 | ## Changes from 0.1.0-RC5 to 0.1.0-RC6 159 | 160 | * updated NPM/Bower packages 161 | * updated FFmpeg to 2.8.6 162 | * switched to a NGINX-RTMP fork of [Sergey Dryabzhinsky](https://github.com/sergey-dryabzhinsky/nginx-rtmp-module) 163 | * added ECMA6 development mode (RS_NODE_ENV=dev) and updated NodeJS to 5.7 164 | * refactored frontend structure 165 | * finished ECMA6 frontend remodeling 166 | * started backend refactoring 167 | * optimized fake audio process (resolved NGINX error "hls: force fragment split") 168 | * added FFmpeg patch of [Andrew Shulgin](https://github.com/andrew-shulgin) (Ignore invalid sprop-parameter-sets missing PPS) 169 | * renamed environment variables (old environment variables are still supported but will be deprecated in the future) 170 | * RS_NODE_PORT 171 | * RS_NODE_ENV 172 | * RS_LOGGER_LEVEL 173 | * RS_TIMEZONE 174 | * RS_SNAPSHOT_REFRESH_INTERVAL 175 | * RS_CREATE_HEAPDUMPS 176 | * RS_USERNAME 177 | * RS_PASSWORD 178 | * several small bugfixes and improvements 179 | 180 | #### Team enlargement 181 | 182 | * [Andrew Shulgin](https://github.com/andrew-shulgin) - Many thanks for your support and welcome to our team! 183 | 184 | ## Changes from 0.1.0-RC4.1 to 0.1.0-RC5 185 | 186 | * updated NPM packages, NGINX to 1.9.9 and FFmpeg to 2.8.5 187 | * restructed frontend (WebsocketService, more ECMA6, ServiceFactory, logger as Angular-Service) 188 | * cleanup NPM dep. 189 | * expanded ESLint ruleset and first code optimizations 190 | * added NGINX process monitoring 191 | * cleanup JSONDB 192 | * implemented input-field validation for RTSP/RTMP addresses 193 | 194 | ## Changes from 0.1.0-RC4 to 0.1.0-RC4.1 195 | 196 | * added missing config of v0.1.0-RC4 197 | 198 | ## Changes from 0.1.0-RC3 to 0.1.0-RC4 199 | 200 | * added https sources to Dockerfiles (thx @ [nodiscc](https://github.com/nodiscc)) 201 | * fixed visualisation problem of new RTMP/RTSP endpoint (JSONDB processing) 202 | * optimized input-process (removed generateOutputRTMPPath) 203 | * cleaned up NPM packages 204 | * added ESLint insteed of JSHint+ first ruleset 205 | * first code optimizations (ecma5 -> ecma6) 206 | * optimized RaspiCam-Hack for Raspberry Pi 1 207 | * updated NPM packages and node 208 | * refactored FFmpeg-Fluent integration by own fork (reduces process-output) 209 | * fixed output-process (generated high CPU load on arm) 210 | * fixed preview player (no play-icon, didn't stop when the modalbox is closed) 211 | 212 | ##### Merged pull requests: 213 | 214 | * Fixed ARM typo @ [Vyacheslav Shevchenko](https://github.com/bliz937) 215 | 216 | ## Changes from 0.1.0-RC2 to 0.1.0-RC3 217 | 218 | * fixed links to docs 219 | * updated NPM & Bower dependencies 220 | 221 | ## Changes from 0.1.0-RC1 to 0.1.0-RC2 222 | 223 | * use branch FIX_stderr-variable_of_infinite_growth 224 | * more jshint stylecheck rules and fix problems 225 | * renamed ReStreamer to Restreamer 226 | * refactored docs and wiki 227 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "arrowFunctions": true, 4 | "binaryLiterals": true, 5 | "blockBindings": true, 6 | "classes": true, 7 | "defaultParams": true, 8 | "destructuring": true, 9 | "forOf": true, 10 | "generators": true, 11 | "modules": true, 12 | "objectLiteralComputedProperties": true, 13 | "objectLiteralDuplicateProperties": true, 14 | "objectLiteralShorthandMethods": true, 15 | "objectLiteralShorthandProperties": true, 16 | "octalLiterals": true, 17 | "regexUFlag": true, 18 | "regexYFlag": true, 19 | "spread": true, 20 | "superInFunctions": true, 21 | "templateStrings": true, 22 | "unicodeCodePointEscapes": true, 23 | "globalReturn": true, 24 | "jsx": true 25 | }, 26 | "rules": { 27 | "accessor-pairs": 2, 28 | "block-scoped-var": 2, 29 | "complexity": [ 30 | 0, 31 | 11 32 | ], 33 | "curly": 2, 34 | "default-case": 2, 35 | "dot-location": [ 36 | 2, 37 | "property" 38 | ], 39 | "dot-notation": 2, 40 | "eqeqeq": 2, 41 | "no-alert": 2, 42 | "no-caller": 2, 43 | "no-case-declarations": 2, 44 | "no-div-regex": 2, 45 | "no-else-return": 2, 46 | "no-eq-null": 2, 47 | "no-eval": 2, 48 | "no-extend-native": 2, 49 | "no-extra-bind": 2, 50 | "no-fallthrough": 2, 51 | "no-floating-decimal": 2, 52 | "no-implicit-coercion": 2, 53 | "no-implied-eval": 2, 54 | "no-iterator": 2, 55 | "no-labels": 2, 56 | "no-lone-blocks": 2, 57 | "no-loop-func": 2, 58 | "no-multi-spaces": 2, 59 | "no-multi-str": 2, 60 | "no-native-reassign": 2, 61 | "no-new": 2, 62 | "no-new-func": 2, 63 | "no-new-wrappers": 2, 64 | "no-octal": 2, 65 | "no-octal-escape": 2, 66 | "no-param-reassign": 2, 67 | "no-process-env": 0, 68 | "no-proto": 2, 69 | "no-redeclare": 2, 70 | "no-return-assign": 2, 71 | "no-script-url": 2, 72 | "no-self-compare": 2, 73 | "no-sequences": 2, 74 | "no-throw-literal": 2, 75 | "no-unused-expressions": 2, 76 | "no-use-before-define": 2, 77 | "no-useless-call": 2, 78 | "no-useless-concat": 2, 79 | "no-void": 2, 80 | "no-warning-comments": [ 81 | 0, 82 | { 83 | "terms": [ 84 | "todo", 85 | "fixme" 86 | ], 87 | "location": "start" 88 | } 89 | ], 90 | "no-with": 2, 91 | "radix": 2, 92 | "vars-on-top": 2, 93 | "wrap-iife": 2, 94 | "yoda": 2, 95 | "init-declarations": [ 96 | 2, 97 | "always" 98 | ], 99 | "no-catch-shadow": 2, 100 | "no-delete-var": 2, 101 | "no-label-var": 2, 102 | "no-shadow": 2, 103 | "no-shadow-restricted-names": 2, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-undefined": 2, 107 | "no-unused-vars": 1, 108 | "callback-return": 2, 109 | "global-require": 0, 110 | "handle-callback-err": 2, 111 | "no-mixed-requires": 2, 112 | "no-new-require": 2, 113 | "no-path-concat": 2, 114 | "no-process-exit": 0, 115 | "array-bracket-spacing": [ 116 | 0, 117 | "never" 118 | ], 119 | "block-spacing": [ 120 | 2, 121 | "always" 122 | ], 123 | "brace-style": 2, 124 | "camelcase": 2, 125 | "comma-spacing": [ 126 | 2, 127 | { 128 | "before": false, 129 | "after": true 130 | } 131 | ], 132 | "comma-style": [ 133 | 2, 134 | "last" 135 | ], 136 | "computed-property-spacing": [ 137 | 2, 138 | "never" 139 | ], 140 | "consistent-this": [ 141 | 2, 142 | "self" 143 | ], 144 | "eol-last": 2, 145 | "func-names": 2, 146 | "indent": [ 147 | 2, 148 | 4, 149 | { 150 | "SwitchCase": 1 151 | } 152 | ], 153 | "key-spacing": [ 154 | 2, 155 | { 156 | "beforeColon": false, 157 | "afterColon": true 158 | } 159 | ], 160 | "keyword-spacing": 2, 161 | "linebreak-style": 2, 162 | "lines-around-comment": [ 163 | 0, 164 | { 165 | "beforeBlockComment": false, 166 | "beforeLineComment": false 167 | } 168 | ], 169 | "max-depth": [ 170 | 2, 171 | 5 172 | ], 173 | "max-len": [ 174 | 2, 175 | 160, 176 | 4, 177 | { 178 | "ignoreUrls": true, 179 | "ignorePattern": "(/.*/|`.*`)" 180 | } 181 | ], 182 | "max-nested-callbacks": [ 183 | 2, 184 | 3 185 | ], 186 | "max-params": [ 187 | 2, 188 | 8 189 | ], 190 | "max-statements": [ 191 | 2, 192 | 40 193 | ], 194 | "new-cap": 2, 195 | "new-parens": 2, 196 | "newline-after-var": 0, 197 | "no-array-constructor": 2, 198 | "no-bitwise": 2, 199 | "no-continue": 2, 200 | "no-inline-comments": 0, 201 | "no-lonely-if": 2, 202 | "no-mixed-spaces-and-tabs": 2, 203 | "no-multiple-empty-lines": [ 204 | 0, 205 | { 206 | "max": 2 207 | } 208 | ], 209 | "no-negated-condition": 2, 210 | "no-nested-ternary": 2, 211 | "no-new-object": 2, 212 | "no-plusplus": 0, 213 | "no-restricted-syntax": 2, 214 | "no-whitespace-before-property": 2, 215 | "no-spaced-func": 2, 216 | "no-trailing-spaces": 2, 217 | "no-unneeded-ternary": 2, 218 | "object-curly-spacing": [ 219 | 2, 220 | "never" 221 | ], 222 | "one-var": [ 223 | 2, 224 | "never" 225 | ], 226 | "operator-assignment": [ 227 | 2, 228 | "never" 229 | ], 230 | "operator-linebreak": [ 231 | 2, 232 | "after" 233 | ], 234 | "padded-blocks": [ 235 | 2, 236 | "never" 237 | ], 238 | "quote-props": [ 239 | 2, 240 | "always" 241 | ], 242 | "quotes": [ 243 | 2, 244 | "single" 245 | ], 246 | "require-jsdoc": [ 247 | 2, 248 | { 249 | "require": { 250 | "FunctionDeclaration": true, 251 | "MethodDefinition": false, 252 | "ClassDeclaration": false 253 | } 254 | } 255 | ], 256 | "semi-spacing": 2, 257 | "semi": [ 258 | 2, 259 | "always" 260 | ], 261 | "sort-vars": 2, 262 | "sort-imports": 2, 263 | "space-before-blocks": 2, 264 | "space-before-function-paren": 2, 265 | "space-in-parens": [ 266 | 2, 267 | "never" 268 | ], 269 | "space-infix-ops": 2, 270 | "space-unary-ops": 2, 271 | "spaced-comment": [ 272 | 2, 273 | "always" 274 | ], 275 | "wrap-regex": 2 276 | }, 277 | "env": { 278 | "es6": true, 279 | "node": true, 280 | "browser": true, 281 | "jquery": true 282 | }, 283 | "parserOptions": { 284 | "sourceType": "module", 285 | "ecmaFeatures": { 286 | "jsx": true, 287 | "experimentalObjectRestSpread": true 288 | } 289 | }, 290 | "extends": "eslint:recommended" 291 | } 292 | -------------------------------------------------------------------------------- /conf/jsondb_v1_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": {}, 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "$id": "http://jsonschema.net/", 5 | "type": "object", 6 | "required": [ 7 | "addresses", 8 | "options", 9 | "states", 10 | "userActions" 11 | ], 12 | "properties": { 13 | "addresses": { 14 | "type": "object", 15 | "required": [ 16 | "srcAddress", 17 | "optionalOutputAddress" 18 | ], 19 | "properties": { 20 | "srcAddress": { 21 | "type": "string", 22 | "default": "", 23 | "pattern": "^(.*)$" 24 | }, 25 | "optionalOutputAddress": { 26 | "type": "string", 27 | "default": "", 28 | "pattern": "^(.*)$" 29 | } 30 | } 31 | }, 32 | "options": { 33 | "type": "object", 34 | "required": [ 35 | "rtspTcp" 36 | ], 37 | "properties": { 38 | "rtspTcp": { 39 | "type": "boolean", 40 | "default": false 41 | }, 42 | "video": { 43 | "type": "object", 44 | "default": null, 45 | "required": [ 46 | "codec", 47 | "preset", 48 | "bitrate", 49 | "fps", 50 | "profile", 51 | "tune" 52 | ], 53 | "properties": { 54 | "codec": { 55 | "type": "string", 56 | "default": "copy", 57 | "pattern": "^(.*)$" 58 | }, 59 | "preset": { 60 | "type": "string", 61 | "default": "ultrafast", 62 | "pattern": "^(.*)$" 63 | }, 64 | "bitrate": { 65 | "type": "string", 66 | "default": "4096", 67 | "pattern": "^(.*)$" 68 | }, 69 | "fps": { 70 | "type": "string", 71 | "default": "25", 72 | "pattern": "^(.*)$" 73 | }, 74 | "profile": { 75 | "type": "string", 76 | "default": "none", 77 | "pattern": "^(.*)$" 78 | }, 79 | "tune": { 80 | "type": "string", 81 | "default": "none", 82 | "pattern": "^(.*)$" 83 | } 84 | } 85 | }, 86 | "audio": { 87 | "type": "object", 88 | "required": [ 89 | "codec", 90 | "preset", 91 | "bitrate", 92 | "channels", 93 | "sampling" 94 | ], 95 | "properties": { 96 | "codec": { 97 | "type": "string", 98 | "default": "auto", 99 | "pattern": "^(.*)$" 100 | }, 101 | "preset": { 102 | "type": "string", 103 | "default": "silence", 104 | "pattern": "^(.*)$" 105 | }, 106 | "bitrate": { 107 | "type": "string", 108 | "default": "64", 109 | "pattern": "^(.*)$" 110 | }, 111 | "channels": { 112 | "type": "string", 113 | "default": "mono", 114 | "pattern": "^(.*)$" 115 | }, 116 | "sampling": { 117 | "type": "string", 118 | "default": "44100", 119 | "pattern": "^(.*)$" 120 | } 121 | } 122 | }, 123 | "output": { 124 | "type": "object", 125 | "required": [ 126 | "type", 127 | "rtmp", 128 | "hls" 129 | ], 130 | "properties": { 131 | "type": { 132 | "type": "string", 133 | "default": "rtmp", 134 | "pattern": "^(rtmp|hls)$" 135 | }, 136 | "rtmp": { 137 | "type": "object", 138 | "default": {} 139 | }, 140 | "hls": { 141 | "type": "object", 142 | "required": [ 143 | "method", 144 | "time", 145 | "listSize", 146 | "timeout" 147 | ], 148 | "properties": { 149 | "method": { 150 | "type": "string", 151 | "default": "POST", 152 | "pattern": "^(POST|PUT)$" 153 | }, 154 | "time": { 155 | "type": "string", 156 | "default": "2", 157 | "pattern": "^(.*)$" 158 | }, 159 | "listSize": { 160 | "type": "string", 161 | "default": "10", 162 | "pattern": "^(.*)$" 163 | }, 164 | "timeout": { 165 | "type": "string", 166 | "default": "10", 167 | "pattern": "^(.*)$" 168 | } 169 | } 170 | } 171 | } 172 | }, 173 | "player": { 174 | "type": "object", 175 | "required": [ 176 | "autoplay", 177 | "mute", 178 | "statistics", 179 | "color", 180 | "logo" 181 | ], 182 | "properties": { 183 | "autoplay": { 184 | "type": "boolean", 185 | "default": false 186 | }, 187 | "mute": { 188 | "type": "boolean", 189 | "default": false 190 | }, 191 | "statistics": { 192 | "type": "boolean", 193 | "default": false 194 | }, 195 | "color": { 196 | "type": "string", 197 | "default": "#3daa48", 198 | "pattern": "^#[0-9a-f]{6}$" 199 | }, 200 | "logo": { 201 | "type": "object", 202 | "required": [ 203 | "image", 204 | "position", 205 | "link" 206 | ], 207 | "properties": { 208 | "image": { 209 | "type": "string", 210 | "default": "", 211 | "pattern": "^(.*)$" 212 | }, 213 | "position": { 214 | "type": "string", 215 | "default": "bottom-right", 216 | "pattern": "^(.*)$" 217 | }, 218 | "link": { 219 | "type": "string", 220 | "default": "", 221 | "pattern": "^(.*)$" 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } 228 | }, 229 | "states": { 230 | "type": "object", 231 | "required": [ 232 | "repeatToLocalNginx", 233 | "repeatToOptionalOutput" 234 | ], 235 | "properties": { 236 | "repeatToLocalNginx": { 237 | "type": "object", 238 | "required": [ 239 | "type" 240 | ], 241 | "properties": { 242 | "type": { 243 | "type": "string", 244 | "default": "", 245 | "pattern": "^(.*)$" 246 | } 247 | } 248 | }, 249 | "repeatToOptionalOutput": { 250 | "type": "object", 251 | "required": [ 252 | "type" 253 | ], 254 | "properties": { 255 | "type": { 256 | "type": "string", 257 | "default": "", 258 | "pattern": "^(.*)$" 259 | } 260 | } 261 | } 262 | } 263 | }, 264 | "userActions": { 265 | "type": "object", 266 | "required": [ 267 | "repeatToLocalNginx", 268 | "repeatToOptionalOutput" 269 | ], 270 | "properties": { 271 | "repeatToLocalNginx": { 272 | "type": "string", 273 | "default": "", 274 | "pattern": "^(.*)$" 275 | }, 276 | "repeatToOptionalOutput": { 277 | "type": "string", 278 | "default": "", 279 | "pattern": "^(.*)$" 280 | } 281 | } 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | MODE=${MODE:="default"} 3 | RS_MODE=${RS_MODE:=$MODE} 4 | 5 | # debug reporting 6 | RS_DEBUG=${RS_DEBUG:="false"} 7 | if [ "$RS_DEBUG" = "true" ]; then 8 | export FFREPORT="file=/restreamer/src/webserver/public/debug/%p-%t.log:level=48" 9 | fi 10 | 11 | DEVICE=${DEVICE:="default"} 12 | 13 | if [ "$DEVICE" = "raspi" ]; then 14 | cp ./conf/live-rpi.json ./conf/live.json 15 | fi 16 | 17 | echo "/opt/vc/lib" > /etc/ld.so.conf.d/00-vmcs.conf 18 | ldconfig 19 | 20 | CPU_TYPE=$(uname -m | cut -c 1-3) 21 | if [ "${RS_MODE}" = "RASPICAM" ] && [ "$CPU_TYPE" = "arm" ]; then 22 | npm start & 23 | NGINX_RUNNING=0 24 | until [ "$NGINX_RUNNING" = "1" ]; do 25 | if pgrep "nginx" > /dev/null 26 | then 27 | NGINX_RUNNING=1 28 | else 29 | NGINX_RUNNING=0 30 | sleep 5 31 | fi 32 | done 33 | 34 | ## options 35 | 36 | RS_RASPICAM_INLINE=${RS_RASPICAM_INLINE:="true"} 37 | 38 | RASPIVID_OPTIONS="--timeout 0 --nopreview" 39 | 40 | # flip the image horizontally 41 | if [ "$RS_RASPICAM_HFLIP" = "true" ]; then 42 | RASPIVID_OPTIONS="$RASPIVID_OPTIONS --hflip" 43 | fi 44 | 45 | # flip the image vertically 46 | if [ "$RS_RASPICAM_VFLIP" = "true" ]; then 47 | RASPIVID_OPTIONS="$RASPIVID_OPTIONS --vflip" 48 | fi 49 | 50 | # inline headers (SPS, PPS) -- https://en.wikipedia.org/wiki/X264 51 | if [ "$RS_RASPICAM_INLINE" = "true" ]; then 52 | RASPIVID_OPTIONS="$RASPIVID_OPTIONS --inline" 53 | fi 54 | 55 | # video stabilization 56 | if [ "$RS_RASPICAM_STABILIZATION" = "true" ]; then 57 | RASPIVID_OPTIONS="$RASPIVID_OPTIONS --vstab" 58 | fi 59 | 60 | ## FPS and GOP 61 | 62 | RASPICAM_FPS=25 63 | RASPICAM_GOP=50 64 | 65 | if [ -n "$RS_RASPICAM_FPS" ]; then 66 | RASPICAM_FPS=$RS_RASPICAM_FPS 67 | RASPICAM_GOP=$((RASPICAM_FPS * 2)) 68 | fi 69 | 70 | if [ -n "$RS_RASPICAM_GOP" ]; then 71 | RASPICAM_GOP=$RASPICAM_GOP 72 | fi 73 | 74 | ## bitrate and codec 75 | 76 | RASPICAM_BITRATE=${RS_RASPICAM_BITRATE:=5000000} 77 | # h264 profile: baseline (no B-frames), main, high 78 | RASPICAM_H264PROFILE=${RS_RASPICAM_H264PROFILE:="high"} 79 | # h264 level: 4, 4.1, 4.2 (1080p30, 720p60 and 640 × 480p60/90) 80 | RASPICAM_H264LEVEL=${RS_RASPICAM_H264LEVEL:=4} 81 | RASPICAM_CODEC=${RS_RASPICAM_CODEC:="H264"} 82 | 83 | ## image parameter 84 | 85 | RASPICAM_WIDTH=${RS_RASPICAM_WIDTH:=1920} 86 | RASPICAM_HEIGHT=${RS_RASPICAM_HEIGHT:=1080} 87 | # -100 to 100 88 | RASPICAM_SHARPNESS=${RS_RASPICAM_SHARPNESS:=0} 89 | # -100 to 100 90 | RASPICAM_CONTRAST=${RS_RASPICAM_CONTRAST:=0} 91 | # 0 to 100 92 | RASPICAM_BRIGHTNESS=${RS_RASPICAM_BRIGHTNESS:=50} 93 | # -100 to 100 94 | RASPICAM_SATURATION=${RS_RASPICAM_SATURATION:=0} 95 | 96 | ## image quality and effects 97 | 98 | # capture ISO 99 | RASPICAM_ISO=${RS_RASPICAM_ISO:=0} 100 | # quantization parameter 101 | RASPICAM_QP=${RS_RASPICAM_QP:=0} 102 | # EV compensation, steps of 1/6 stop 103 | RASPICAM_EV=${RS_RASPICAM_EV:=0} 104 | # exposure: off,auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks 105 | RASPICAM_EXPOSURE=${RS_RASPICAM_EXPOSURE:="auto"} 106 | # flicker filter: off,auto,50hz,60hz 107 | RASPICAM_FLICKER=${RS_RASPICAM_FLICKER:="off"} 108 | # white balance: off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon 109 | RASPICAM_AWB=${RS_RASPICAM_AWB:="auto"} 110 | # image effects: none,negative,solarise,sketch,denoise,emboss,oilpaint,hatch,gpen,pastel,watercolour,film,blur,saturation,colourswap,washedout,posterise,colourpoint,colourbalance,cartoon 111 | RASPICAM_IMXFX=${RS_RASPICAM_IMXFX:="none"} 112 | # metering: average,spot,backlit,matrix 113 | RASPICAM_METERING=${RS_RASPICAM_METERING:="average"} 114 | # dynamic range compression: off,low,med,high 115 | RASPICAM_DRC=${RS_RASPICAM_DRC:="off"} 116 | 117 | ## audio 118 | 119 | RASPICAM_AUDIODEVICE=${RS_RASPICAM_AUDIODEVICE:="0"} 120 | RASPICAM_AUDIOBITRATE=${RS_RASPICAM_AUDIOBITRATE:="0"} 121 | RASPICAM_AUDIOCHANNELS=${RS_RASPICAM_AUDIOCHANNELS:="1"} 122 | RASPICAM_AUDIOLAYOUT=${RS_RASPICAM_AUDIOLAYOUT:="mono"} 123 | RASPICAM_AUDIOSAMPLING=${RS_RASPICAM_AUDIOSAMPLING:="44100"} 124 | 125 | RASPICAM_AUDIO="-f lavfi -i anullsrc=r=${RASPICAM_AUDIOSAMPLING}:cl=${RASPICAM_AUDIOLAYOUT}" 126 | 127 | if [ "$RS_RASPICAM_AUDIO" = "true" ]; then 128 | RASPICAM_AUDIO="-thread_queue_size 512 -f alsa -ac ${RASPICAM_AUDIOCHANNELS} -ar ${RASPICAM_AUDIOSAMPLING} -i hw:${RASPICAM_AUDIODEVICE}" 129 | RASPICAM_AUDIOBITRATE=${RS_RASPICAM_AUDIOBITRATE:="64000"} 130 | fi 131 | 132 | RASPICAM_AUDIOBITRATE=$(($RASPICAM_AUDIOBITRATE / 1024)) 133 | 134 | ## RTMP URL 135 | 136 | RTMP_URL="rtmp://127.0.0.1:1935/live/raspicam.stream" 137 | 138 | if [ -n "$RS_TOKEN" ]; then 139 | RTMP_URL="${RTMP_URL}?token=${RS_TOKEN}" 140 | fi 141 | 142 | /opt/vc/bin/raspivid \ 143 | $RASPIVID_OPTIONS \ 144 | --width "$RASPICAM_WIDTH" \ 145 | --height "$RASPICAM_HEIGHT" \ 146 | --framerate "$RASPICAM_FPS" \ 147 | --bitrate "$RASPICAM_BITRATE" \ 148 | --intra "$RASPICAM_GOP" \ 149 | --codec "$RASPICAM_CODEC" \ 150 | --profile "$RASPICAM_H264PROFILE" \ 151 | --level "$RASPICAM_H264LEVEL" \ 152 | --sharpness "$RASPICAM_SHARPNESS" \ 153 | --contrast "$RASPICAM_CONTRAST" \ 154 | --brightness "$RASPICAM_BRIGHTNESS" \ 155 | --saturation "$RASPICAM_SATURATION" \ 156 | --ISO "$RASPICAM_ISO" \ 157 | --qp "$RASPICAM_QP" \ 158 | --exposure "$RASPICAM_EXPOSURE" \ 159 | --flicker "$RASPICAM_FLICKER" \ 160 | --awb "$RASPICAM_AWB" \ 161 | --imxfx "$RASPICAM_IMXFX" \ 162 | --metering "$RASPICAM_METERING" \ 163 | --drc "$RASPICAM_DRC" \ 164 | -o - | ffmpeg -framerate "$RASPICAM_FPS" -i - ${RASPICAM_AUDIO} -map 0:v -map 1:a -codec:v copy -codec:a aac -b:a "${RASPICAM_AUDIOBITRATE}k" -shortest -f flv "${RTMP_URL}" > /dev/null 2>&1 165 | elif [ "${RS_MODE}" = "USBCAM" ]; then 166 | npm start & 167 | NGINX_RUNNING=0 168 | until [ "$NGINX_RUNNING" = "1" ]; do 169 | if pgrep "nginx" > /dev/null 170 | then 171 | NGINX_RUNNING=1 172 | else 173 | NGINX_RUNNING=0 174 | sleep 5 175 | fi 176 | done 177 | 178 | # https://trac.ffmpeg.org/wiki/Capture/Webcam 179 | # https://www.ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2 180 | # https://trac.ffmpeg.org/wiki/Capture/ALSA 181 | # https://ffmpeg.org/ffmpeg-devices.html#alsa 182 | 183 | USBCAM_VIDEODEVICE=${RS_USBCAM_VIDEODEVICE:="/dev/video"} 184 | USBCAM_AUDIODEVICE=${RS_USBCAM_AUDIODEVICE:="0"} 185 | 186 | USBCAM_FPS=25 187 | USBCAM_GOP=50 188 | 189 | if [ -n "$RS_USBCAM_FPS" ]; then 190 | USBCAM_FPS=$RS_USBCAM_FPS 191 | USBCAM_GOP=$((USBCAM_FPS * 2)) 192 | fi 193 | 194 | if [ -n "$RS_USBCAM_GOP" ]; then 195 | USBCAM_GOP=$USBCAM_GOP 196 | fi 197 | 198 | USBCAM_BITRATE=${RS_USBCAM_BITRATE:=5000000} 199 | USBCAM_H264PRESET=${RS_USBCAM_H264PRESET:="ultrafast"} 200 | USBCAM_H264PROFILE=${RS_USBCAM_H264PROFILE:="baseline"} 201 | USBCAM_WIDTH=${RS_USBCAM_WIDTH:=1280} 202 | USBCAM_HEIGHT=${RS_USBCAM_HEIGHT:=720} 203 | 204 | USBCAM_BITRATE=$(($USBCAM_BITRATE / 1024)) 205 | USBCAM_BUFFER=$(($USBCAM_BITRATE * 2)) 206 | 207 | RTMP_URL="rtmp://127.0.0.1:1935/live/usbcam.stream" 208 | 209 | if [ -n "$RS_TOKEN" ]; then 210 | RTMP_URL="${RTMP_URL}?token=${RS_TOKEN}" 211 | fi 212 | 213 | USBCAM_VIDEOENCODER="-codec:v libx264 -preset:v ${USBCAM_H264PRESET} -vf format=yuv420p" 214 | 215 | if [ "$DEVICE" = "raspi" ]; then 216 | USBCAM_VIDEOENCODER="-codec:v h264_omx -profile:v ${USBCAM_H264PROFILE}" 217 | fi 218 | 219 | USBCAM_AUDIOBITRATE=${RS_USBCAM_AUDIOBITRATE:="0"} 220 | USBCAM_AUDIOCHANNELS=${RS_USBCAM_AUDIOCHANNELS:="1"} 221 | USBCAM_AUDIOLAYOUT=${RS_USBCAM_AUDIOLAYOUT:="mono"} 222 | USBCAM_AUDIOSAMPLING=${RS_USBCAM_AUDIOSAMPLING:="44100"} 223 | 224 | USBCAM_AUDIO="-f lavfi -i anullsrc=r=${USBCAM_AUDIOSAMPLING}:cl=${USBCAM_AUDIOLAYOUT}" 225 | 226 | if [ "$RS_USBCAM_AUDIO" = "true" ]; then 227 | USBCAM_AUDIO="-thread_queue_size 512 -f alsa -ac ${USBCAM_AUDIOCHANNELS} -ar ${USBCAM_AUDIOSAMPLING} -i hw:${USBCAM_AUDIODEVICE}" 228 | USBCAM_AUDIOBITRATE=${RS_USBCAM_AUDIOBITRATE:="64000"} 229 | fi 230 | 231 | USBCAM_AUDIOBITRATE=$(($USBCAM_AUDIOBITRATE / 1024)) 232 | 233 | ffmpeg -thread_queue_size 512 -f v4l2 -framerate "$USBCAM_FPS" -video_size "${USBCAM_WIDTH}x${USBCAM_HEIGHT}" -i "${USBCAM_VIDEODEVICE}" ${USBCAM_AUDIO} -map 0:v -map 1:a ${USBCAM_VIDEOENCODER} -r "$USBCAM_FPS" -g "${USBCAM_GOP}" -b:v "${USBCAM_BITRATE}k" -bufsize "${USBCAM_BUFFER}k" -codec:a aac -b:a "${USBCAM_AUDIOBITRATE}k" -shortest -f flv "${RTMP_URL}" > /dev/null 2>&1 234 | else 235 | npm start 236 | fi 237 | -------------------------------------------------------------------------------- /src/webserver/public/scripts/Main/MainController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file holds the AngularJS mainController 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | window.angular.module('Main').controller('mainController', 10 | ['ws', '$scope', '$location', '$rootScope', '$stateParams', 'config', 'loggerService', function mainController (ws, $scope, $location, $rootScope, $stateParams, config, logger) { 11 | let setup = false; 12 | let player = null; 13 | let posterPlugin = null; 14 | 15 | $scope.config = config; 16 | 17 | const updateSnapshot = () => { 18 | if (posterPlugin !== null) { 19 | posterPlugin.options.poster = 'images/live.jpg?t=' + String(new Date().getTime()); 20 | if (!player.isPlaying()) { 21 | posterPlugin.render(); 22 | } 23 | } 24 | }; 25 | 26 | const initClappr = () => { 27 | const plugins = []; 28 | if($scope.reStreamerData.options.player.statistics == true) { 29 | plugins.push(ClapprNerdStats); 30 | plugins.push(ClapprStats); 31 | } 32 | 33 | const config = { 34 | source: 'hls/live.stream.m3u8', 35 | parentId: '#player', 36 | baseUrl: 'libs/scripts/', 37 | poster: 'images/live.jpg?t=' + String(new Date().getTime()), 38 | mediacontrol: { 39 | seekbar: $scope.reStreamerData.options.player.color, 40 | buttons: $scope.reStreamerData.options.player.color 41 | }, 42 | height: '100%', 43 | width: '100%', 44 | disableCanAutoPlay: true, 45 | autoPlay: $scope.reStreamerData.options.player.autoplay, 46 | mute: $scope.reStreamerData.options.player.mute, 47 | plugins: plugins, 48 | clapprStats: { 49 | runEach: 1000, 50 | onReport: (metrics) => {}, 51 | }, 52 | clapprNerdStats: { 53 | shortcut: ['command+shift+s', 'ctrl+shift+s'], 54 | iconPosition: 'top-right' 55 | } 56 | }; 57 | 58 | if($scope.reStreamerData.options.player.logo.image.length != 0) { 59 | config.watermark = $scope.reStreamerData.options.player.logo.image; 60 | config.position = $scope.reStreamerData.options.player.logo.position; 61 | 62 | if($scope.reStreamerData.options.player.logo.link.length != 0) { 63 | config.watermarkLink = $scope.reStreamerData.options.player.logo.link; 64 | } 65 | } 66 | 67 | $('#player').empty(); 68 | 69 | player = new window.Clappr.Player(config); 70 | posterPlugin = player.core.mediaControl.container.getPlugin('poster'); 71 | player.on(window.Clappr.Events.PLAYER_STOP, () => { 72 | posterPlugin.render(); 73 | }); 74 | }; 75 | 76 | $scope.optionalOutputInputInvalid = false; 77 | $scope.nginxRepeatStreamInputInvalid = false; 78 | 79 | $scope.reStreamerData = { 80 | options: { 81 | rtspTcp: false, 82 | video: { 83 | codec: 'copy', 84 | preset: 'ultrafast', 85 | bitrate: 4096, 86 | profile: 'auto', 87 | tune: 'none' 88 | }, 89 | audio: { 90 | codec: 'copy', 91 | preset: 'silence', 92 | bitrate: 64, 93 | channels: 'mono', 94 | sampling: 41000 95 | }, 96 | player: { 97 | autoplay: false, 98 | mute: false, 99 | statistics: false, 100 | color: '#3daa48', 101 | logo: { 102 | image: '', 103 | position: 'bottom-right', 104 | link: '' 105 | } 106 | }, 107 | output: { 108 | type: 'rtmp', 109 | rtmp: {}, 110 | hls: { 111 | method: 'POST' 112 | } 113 | } 114 | }, 115 | states: { 116 | repeatToLocalNginx: { 117 | type: '', 118 | message: '' 119 | }, 120 | repeatToOptionalOutput: { 121 | type: '', 122 | message: '' 123 | } 124 | }, 125 | userActions: { 126 | repeatToLocalNginx: '', 127 | repeatToOptionalOutput: '' 128 | }, 129 | progresses: { 130 | repeatToLocalNginx: '', 131 | repeatToOptionalOutput: '' 132 | }, 133 | addresses: { 134 | optionalOutputAddress: '', 135 | srcAddress: '' 136 | }, 137 | }; 138 | 139 | $rootScope.windowProtocol = window.location.protocol; 140 | $rootScope.windowLocationPort = window.location.port ? `:${window.location.port}` : ''; 141 | $rootScope.windowLocationPath = window.location.pathname; 142 | 143 | $scope.optionalOutput = ''; 144 | 145 | $scope.showStartButton = (streamType) => { 146 | return ($scope.reStreamerData.states[streamType].type == 'disconnected'); 147 | }; 148 | 149 | $scope.showStopButton = (streamType) => { 150 | let state = $scope.reStreamerData.states[streamType].type; 151 | 152 | return (state == 'connected' || state == 'connecting' || state == 'error'); 153 | }; 154 | 155 | $scope.disableInput = (streamType) => { 156 | return ($scope.reStreamerData.states[streamType].type != 'disconnected'); 157 | }; 158 | 159 | $scope.openPlayer = () => { 160 | initClappr(); 161 | 162 | $('#player-modal').modal('show').on('hide.bs.modal', function closeModal (e) { 163 | player.stop(); 164 | $(this).off('hide.bs.modal'); 165 | $(this).modal('hide'); 166 | return e.preventDefault(); 167 | }); 168 | }; 169 | 170 | /** 171 | * Configure Websockets 172 | */ 173 | 174 | ws.emit('checkStates'); // check states of hls and rtmp stream 175 | 176 | // prohibit double binding of events 177 | if (!setup) { 178 | /* 179 | * test websockets connection (should print below message to browser console if it works) 180 | */ 181 | ws.on('updateProgress', (progresses) => { 182 | $scope.reStreamerData.progresses = progresses; 183 | }); 184 | ws.on('publicIp', (publicIp) => { 185 | $rootScope.publicIp = publicIp; 186 | }); 187 | ws.on('updateStreamData', (reStreamerData) => { 188 | $scope.reStreamerData = reStreamerData; 189 | if ($scope.showStopButton('repeatToOptionalOutput')) { 190 | // checkbox 191 | $scope.activateOptionalOutput = true; 192 | } 193 | }); 194 | ws.on('snapshot', updateSnapshot); 195 | } 196 | 197 | $scope.startStream = (streamType) => { 198 | const inputRegex = /^(rtmp(s|t)?|rtsp|https?):\/\//; 199 | const outputRegexRTMP = /^rtmp(s|t)?:\/\//; 200 | const outputRegexHLS = /^https?:\/\/.*\.m3u8/; 201 | 202 | var optionalOutput = ''; 203 | if($scope.activateOptionalOutput === true) { 204 | optionalOutput = $scope.reStreamerData.addresses.optionalOutputAddress; 205 | } 206 | 207 | if(streamType == 'repeatToOptionalOutput') { 208 | $scope.optionalOutputInputInvalid = true; 209 | 210 | if($scope.reStreamerData.options.output.type == 'rtmp') { 211 | $scope.optionalOutputInputInvalid = !outputRegexRTMP.test(optionalOutput); 212 | } 213 | else if($scope.reStreamerData.options.output.type == 'hls') { 214 | $scope.optionalOutputInputInvalid = !outputRegexHLS.test(optionalOutput); 215 | } 216 | 217 | if($scope.optionalOutputInputInvalid) { 218 | return; 219 | } 220 | } 221 | else { 222 | $scope.nginxRepeatStreamInputInvalid = !inputRegex.test($scope.reStreamerData.addresses.srcAddress); 223 | if($scope.nginxRepeatStreamInputInvalid) { 224 | return; 225 | } 226 | } 227 | 228 | ws.emit('startStream', { 229 | src: $scope.reStreamerData.addresses.srcAddress, 230 | options: $scope.reStreamerData.options, 231 | streamType: streamType, 232 | optionalOutput: optionalOutput 233 | }); 234 | }; 235 | 236 | $scope.stopStream = (streamType) => { 237 | ws.emit('stopStream', streamType); 238 | }; 239 | 240 | $scope.playerOptions = () => { 241 | ws.emit('playerOptions', $scope.reStreamerData.options.player); 242 | }; 243 | }] 244 | ); 245 | -------------------------------------------------------------------------------- /src/classes/RestreamerData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file holds the code for the class EnvVar 3 | * @link https://github.com/datarhei/restreamer 4 | * @copyright 2015 datarhei.org 5 | * @license Apache-2.0 6 | */ 7 | 'use strict'; 8 | 9 | const fs = require('fs'); 10 | const Q = require('q'); 11 | const path = require('path'); 12 | const Validator = require('jsonschema').Validator; 13 | 14 | const logger = require('./Logger')('RestreamerData'); 15 | 16 | const dbPath = path.join(global.__base, 'db'); 17 | const dbFile = 'v1.json'; 18 | 19 | const confPath = path.join(global.__base, 'conf'); 20 | const schemaFile = 'jsondb_v1_schema.json'; 21 | 22 | class RestreamerData { 23 | 24 | static checkJSONDb () { 25 | var schemadata = {}; 26 | var dbdata = {}; 27 | var deferred = Q.defer(); 28 | var readSchema = Q.nfcall(fs.readFile, path.join(confPath, schemaFile)); 29 | var readDBFile = Q.nfcall(fs.readFile, path.join(dbPath, dbFile)); 30 | 31 | logger.info('Checking jsondb file...'); 32 | readSchema 33 | .then((s) => { 34 | schemadata = JSON.parse(s.toString('utf8')); 35 | return readDBFile; 36 | }) 37 | .then((d) => { 38 | dbdata = JSON.parse(d.toString('utf8')); 39 | let v = new Validator(); 40 | let instance = dbdata; 41 | let schema = schemadata; 42 | let validateResult = v.validate(instance, schema); 43 | 44 | if (validateResult.errors.length > 0) { 45 | logger.debug(`Validation error of v1.db: ${JSON.stringify(validateResult.errors)}`); 46 | throw new Error(JSON.stringify(validateResult.errors)); 47 | } else { 48 | logger.debug('"v1.db" is valid'); 49 | 50 | // Fill up optional fields if not present 51 | if(!('video' in dbdata.options)) { 52 | dbdata.options.video = { 53 | codec: 'copy', 54 | preset: 'ultrafast', 55 | bitrate: '4096', 56 | fps: '25', 57 | profile: 'auto', 58 | tune: 'none' 59 | }; 60 | } 61 | 62 | if(!('audio' in dbdata.options)) { 63 | dbdata.options.audio = { 64 | codec: 'auto', 65 | preset: 'silence', 66 | bitrate: '64', 67 | channels: 'mono', 68 | sampling: '44100' 69 | }; 70 | 71 | // Update the defaults according to RS_AUDIO 72 | switch(process.env.RS_AUDIO) { 73 | case 'auto': 74 | dbdata.options.audio.codec = 'auto'; 75 | break; 76 | case 'none': 77 | dbdata.options.audio.codec = 'none'; 78 | break; 79 | case 'silence': 80 | dbdata.options.audio.codec = 'aac'; 81 | dbdata.options.audio.preset = 'silence'; 82 | dbdata.options.audio.bitrate = '8'; 83 | dbdata.options.audio.channels = 'mono'; 84 | dbdata.options.audio.sampling = '44100'; 85 | break; 86 | case 'aac': 87 | dbdata.options.audio.codec = 'aac'; 88 | dbdata.options.audio.preset = 'encode'; 89 | dbdata.options.audio.bitrate = '64'; 90 | dbdata.options.audio.channels = 'inherit'; 91 | dbdata.options.audio.sampling = 'inherit'; 92 | break; 93 | case 'mp3': 94 | dbdata.options.audio.codec = 'mp3'; 95 | dbdata.options.audio.preset = 'encode'; 96 | dbdata.options.audio.bitrate = '64'; 97 | dbdata.options.audio.channels = 'inherit'; 98 | dbdata.options.audio.sampling = 'inherit'; 99 | break; 100 | default: 101 | break; 102 | } 103 | } 104 | 105 | if(!('player' in dbdata.options)) { 106 | dbdata.options.player = { 107 | autoplay: false, 108 | mute: false, 109 | statistics: false, 110 | color: '#3daa48', 111 | logo: { 112 | image: '', 113 | position: 'bottom-right', 114 | link: '' 115 | } 116 | }; 117 | } 118 | 119 | if(!('output' in dbdata.options)) { 120 | dbdata.options.output = { 121 | type: 'rtmp', 122 | rtmp: {}, 123 | hls: { 124 | method: 'POST', 125 | time: '2', 126 | listSize: '10', 127 | timeout: '10' 128 | } 129 | }; 130 | } 131 | 132 | if(parseInt(dbdata.options.output.hls.timeout) > 2147) { 133 | dbdata.options.output.hls.timeout = '10'; 134 | } 135 | 136 | if (!fs.existsSync(dbPath)) { 137 | fs.mkdirSync(dbPath); 138 | } 139 | fs.writeFileSync(path.join(dbPath, dbFile), JSON.stringify(dbdata)); 140 | 141 | deferred.resolve(); 142 | } 143 | }) 144 | .catch((error) => { 145 | var defaultStructure = { 146 | addresses: { 147 | srcAddress: '', 148 | optionalOutputAddress: '' 149 | }, 150 | options: { 151 | rtspTcp: true, 152 | video: { 153 | codec: 'copy', 154 | preset: 'ultrafast', 155 | bitrate: '4096', 156 | fps: '25', 157 | profile: 'auto', 158 | tune: 'none' 159 | }, 160 | audio: { 161 | codec: 'auto', 162 | preset: 'silence', 163 | bitrate: '64', 164 | channels: 'mono', 165 | sampling: '44100' 166 | }, 167 | player: { 168 | autoplay: false, 169 | mute: false, 170 | statistics: false, 171 | color: '#3daa48', 172 | logo: { 173 | image: '', 174 | position: 'bottom-right', 175 | link: '' 176 | } 177 | }, 178 | output: { 179 | type: 'rtmp', 180 | rtmp: {}, 181 | hls: { 182 | method: 'POST', 183 | time: '2', 184 | listSize: '10', 185 | timeout: '10' 186 | } 187 | } 188 | }, 189 | states: { 190 | repeatToLocalNginx: { 191 | type: 'stopped' 192 | }, 193 | repeatToOptionalOutput: { 194 | type: 'stopped' 195 | } 196 | }, 197 | userActions: { 198 | repeatToLocalNginx: 'stop', 199 | repeatToOptionalOutput: 'stop' 200 | } 201 | }; 202 | 203 | // Set stream source and start streaming on a fresh installation 204 | if(process.env.RS_INPUTSTREAM != '') { 205 | defaultStructure.addresses.srcAddress = process.env.RS_INPUTSTREAM; 206 | defaultStructure.states.repeatToLocalNginx.type = 'connected'; 207 | defaultStructure.userActions.repeatToLocalNginx = 'start'; 208 | 209 | // Set stream destination and start streaming on a fresh installation 210 | if(process.env.RS_OUTPUTSTREAM != '') { 211 | defaultStructure.addresses.optionalOutputAddress = process.env.RS_OUTPUTSTREAM; 212 | defaultStructure.states.repeatToOptionalOutput.type = 'connected'; 213 | defaultStructure.userActions.repeatToOptionalOutput = 'start'; 214 | } 215 | } 216 | 217 | 218 | logger.debug(`Error reading "v1.db": ${error.toString()}`); 219 | if (!fs.existsSync(dbPath)) { 220 | fs.mkdirSync(dbPath); 221 | } 222 | fs.writeFileSync(path.join(dbPath, dbFile), JSON.stringify(defaultStructure)); 223 | deferred.resolve(); 224 | }); 225 | return deferred.promise; 226 | } 227 | } 228 | 229 | module.exports = RestreamerData; 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /conf/live-rpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live", 3 | "jsondb": "db/v1", 4 | "auth": { 5 | "username": "admin", 6 | "password": "datarhei", 7 | "token": "" 8 | }, 9 | "ffmpeg": { 10 | "options": { 11 | "audio_codec_copy": { 12 | "outputOptions": [ 13 | "-codec:a copy" 14 | ] 15 | }, 16 | "audio_codec_copy_aac": { 17 | "outputOptions": [ 18 | "-codec:a copy", 19 | "-bsf:a aac_adtstoasc" 20 | ] 21 | }, 22 | "audio_codec_none": { 23 | "outputOptions": [ 24 | "-an" 25 | ] 26 | }, 27 | "audio_codec_aac": { 28 | "outputOptions": [ 29 | "-codec:a aac", 30 | "-bsf:a aac_adtstoasc" 31 | ] 32 | }, 33 | "audio_codec_mp3": { 34 | "outputOptions": [ 35 | "-codec:a libmp3lame" 36 | ] 37 | }, 38 | "audio_preset_copy": { 39 | "outputOptions": [ 40 | "-map 0:{audioid}" 41 | ] 42 | }, 43 | "audio_preset_encode": { 44 | "outputOptions": [ 45 | "-b:a {bitrate}k" 46 | ] 47 | }, 48 | "audio_preset_silence": { 49 | "input": "anullsrc=r={sampling}:cl={channels}", 50 | "inputOptions": [ 51 | "-f lavfi", 52 | "-thread_queue_size 512" 53 | ], 54 | "outputOptions": [ 55 | "-map 1:a", 56 | "-b:a {bitrate}k", 57 | "-shortest" 58 | ] 59 | }, 60 | "audio_filter_sampling": { 61 | "outputOptions": [ 62 | "-af aresample=osr={sampling}" 63 | ] 64 | }, 65 | "audio_filter_channels": { 66 | "outputOptions": [ 67 | "-af aresample=ocl={channels}" 68 | ] 69 | }, 70 | "audio_filter_all": { 71 | "outputOptions": [ 72 | "-af aresample=osr={sampling}:ocl={channels}" 73 | ] 74 | }, 75 | "video_codec_copy": { 76 | "inputOptions": [], 77 | "outputOptions": [ 78 | "-map 0:{videoid}", 79 | "-codec:v copy", 80 | "-vsync 0", 81 | "-copyts", 82 | "-start_at_zero" 83 | ] 84 | }, 85 | "video_codec_h264": { 86 | "inputOptions": [ 87 | "-codec:v h264_mmal" 88 | ], 89 | "outputOptions": [ 90 | "-map 0:{videoid}", 91 | "-codec:v h264_omx", 92 | "-b:v {bitrate}k", 93 | "-maxrate {bitrate}k", 94 | "-bufsize {bitrate}k", 95 | "-r {fps}", 96 | "-g {gop}", 97 | "-zerocopy 1", 98 | "-pix_fmt yuv420p", 99 | "-vsync 1" 100 | ] 101 | }, 102 | "video_codec_h264_profile": { 103 | "outputOptions": [ 104 | "-profile:v {profile}" 105 | ] 106 | }, 107 | "video_codec_h264_tune": { 108 | "outputOptions": [] 109 | }, 110 | "video_codec_hevc": { 111 | "inputOptions": [], 112 | "outputOptions": [ 113 | "-map 0:{videoid}", 114 | "-codec:v libx265", 115 | "-preset:v {preset}", 116 | "-b:v {bitrate}k", 117 | "-maxrate {bitrate}k", 118 | "-bufsize {bitrate}k", 119 | "-r {fps}", 120 | "-x265-params keyint={gop}", 121 | "-vsync 1" 122 | ] 123 | }, 124 | "global": { 125 | "inputOptions": [ 126 | "-stats", 127 | "-loglevel quiet", 128 | "-err_detect ignore_err" 129 | ] 130 | }, 131 | "video": { 132 | "outputOptions": [ 133 | "-map_metadata -1", 134 | "-metadata application=datarhei/Restreamer" 135 | ] 136 | }, 137 | "rtmp": { 138 | "outputOptions": [ 139 | "-f flv" 140 | ] 141 | }, 142 | "rtmp-fflags": { 143 | "inputOptions": [ 144 | "-fflags nobuffer+genpts+igndts" 145 | ] 146 | }, 147 | "hls": { 148 | "outputOptions": [ 149 | "-f hls", 150 | "-hls_time {time}", 151 | "-hls_list_size {listSize}", 152 | "-hls_flags delete_segments+append_list", 153 | "-hls_segment_type mpegts", 154 | "-method {method}", 155 | "-timeout {timeout}" 156 | ] 157 | }, 158 | "hls_segment_ts": { 159 | "outputOptions": [ 160 | "-hls_segment_type mpegts", 161 | "-hls_segment_filename /tmp/hls/live.stream_%09d.ts" 162 | ] 163 | }, 164 | "hls_segment_fmp4": { 165 | "outputOptions": [ 166 | "-hls_segment_type fmp4", 167 | "-hls_fmp4_init_filename live.stream_init.mp4", 168 | "-hls_segment_filename /tmp/hls/live.stream_%09d.m4s" 169 | ] 170 | }, 171 | "rtsp": { 172 | "inputOptions": [ 173 | "-stimeout 5000000" 174 | ] 175 | }, 176 | "rtsp-tcp": { 177 | "inputOptions": [ 178 | "-rtsp_transport tcp" 179 | ] 180 | }, 181 | "snapshot": { 182 | "outputOptions": [ 183 | "-vframes 1" 184 | ] 185 | } 186 | }, 187 | "probe":{ 188 | "timeout": "30000" 189 | }, 190 | "monitor": { 191 | "restart_wait": "6000", 192 | "stale_wait": "60000" 193 | } 194 | }, 195 | "nginx": { 196 | "command": "/usr/local/nginx/sbin/nginx", 197 | "args": [ 198 | "-c", 199 | "/restreamer/conf/nginx.conf" 200 | ], 201 | "args_ssl": [ 202 | "-c", 203 | "/restreamer/conf/nginx_ssl.conf" 204 | ], 205 | "streaming": { 206 | "ip": "127.0.0.1", 207 | "rtmp_port": "1935", 208 | "rtmp_hls_path": "/hls/", 209 | "http_port": "8080", 210 | "http_health_path": "/ping" 211 | } 212 | }, 213 | "envVars": [ 214 | { 215 | "name": "RS_NODEJS_PORT", 216 | "alias": [ 217 | "NODEJS_PORT" 218 | ], 219 | "type": "int", 220 | "defaultValue": "3000", 221 | "required": false, 222 | "description": "Webserver port of application." 223 | }, 224 | { 225 | "name": "RS_NODEJS_ENV", 226 | "alias": [ 227 | "NODE_ENV" 228 | ], 229 | "type": "string", 230 | "defaultValue": "prod", 231 | "required": false, 232 | "description": "Node.js Environment ('dev' or 'prod')." 233 | }, 234 | { 235 | "name": "RS_LOGLEVEL", 236 | "alias": [ 237 | "LOGGER_LEVEL" 238 | ], 239 | "type": "int", 240 | "defaultValue": "3", 241 | "required": false, 242 | "description": "Logging level (0=no logging, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG)." 243 | }, 244 | { 245 | "name": "RS_TIMEZONE", 246 | "alias": [ 247 | "TIMEZONE" 248 | ], 249 | "type": "string", 250 | "defaultValue": "Europe/Berlin", 251 | "required": false, 252 | "description": "Set the timezone. Accepts Olson timezone IDs." 253 | }, 254 | { 255 | "name": "RS_SNAPSHOT_INTERVAL", 256 | "alias": [ 257 | "SNAPSHOT_REFRESH_INTERVAL", 258 | "RS_SNAPSHOT_REFRESH_INTERVAL" 259 | ], 260 | "type": "string", 261 | "defaultValue": "1m", 262 | "required": false, 263 | "description": "Interval for new snapshots (in milliseconds, use suffix 's' for seconds, 'm' for minutes). Use a value of 0 to disable snapshots." 264 | }, 265 | { 266 | "name": "RS_USERNAME", 267 | "alias": [ 268 | "RESTREAMER_USERNAME" 269 | ], 270 | "type": "string", 271 | "defaultValue": "admin", 272 | "required": false, 273 | "description": "Username for the backend." 274 | }, 275 | { 276 | "name": "RS_PASSWORD", 277 | "alias": [ 278 | "RESTREAMER_PASSWORD" 279 | ], 280 | "type": "string", 281 | "defaultValue": "datarhei", 282 | "required": false, 283 | "description": "Password for the backend." 284 | }, 285 | { 286 | "name": "RS_TOKEN", 287 | "alias": [], 288 | "type": "string", 289 | "defaultValue": "", 290 | "required": false, 291 | "description": "Token for pushing an RTMP stream." 292 | }, 293 | { 294 | "name": "RS_DEBUG_HEAPDUMPS", 295 | "alias": [ 296 | "CREATE_HEAPDUMPS" 297 | ], 298 | "type": "bool", 299 | "defaultValue": "false", 300 | "required": false, 301 | "description": "Create heapdumps of application." 302 | }, 303 | { 304 | "name": "RS_DEBUG", 305 | "alias": [], 306 | "type": "bool", 307 | "defaultValue": false, 308 | "required": false, 309 | "description": "Enables debug reporting." 310 | }, 311 | { 312 | "name": "RS_AUDIO", 313 | "alias": [], 314 | "type": "string", 315 | "defaultValue": "auto", 316 | "required": false, 317 | "description": "Audio track handling: auto, none (remove audio), silence (force silence), aac (force AAC), mp3 (force MP3)." 318 | }, 319 | { 320 | "name": "RS_HTTPS", 321 | "alias": [], 322 | "type": "bool", 323 | "defaultValue": "false", 324 | "required": false, 325 | "description": "Enables HTTPS support for admin interface and embeddable player." 326 | }, 327 | { 328 | "name": "RS_INPUTSTREAM", 329 | "alias": [], 330 | "type": "string", 331 | "defaultValue": "", 332 | "required": false, 333 | "description": "Automatically start pulling from this stream on a fresh Restreamer installation." 334 | }, 335 | { 336 | "name": "RS_OUTPUTSTREAM", 337 | "alias": [], 338 | "type": "string", 339 | "defaultValue": "", 340 | "required": false, 341 | "description": "Automatically start pushing to this stream on a fresh Restreamer installation." 342 | } 343 | ] 344 | } 345 | --------------------------------------------------------------------------------