ADD 0.1.0-RC6

This commit is contained in:
Jan Stabenow
2016-03-10 19:37:04 +01:00
parent 7234fb06b1
commit b366bad638
72 changed files with 2487 additions and 1642 deletions

View File

@@ -1,3 +1,3 @@
{
"directory": "bin/webserver/public/libs"
"directory": "src/webserver/public/libs"
}

View File

@@ -1,20 +1,20 @@
{
"adjoining-classes": false,
"box-sizing": false,
"box-model": false,
"compatible-vendor-prefixes": false,
"floats": false,
"font-sizes": false,
"gradients": false,
"important": false,
"known-properties": false,
"outline-none": false,
"qualified-headings": false,
"regex-selectors": false,
"shorthand": false,
"text-indent": false,
"unique-headings": false,
"universal-selector": false,
"unqualified-attributes": false,
"zero-units": false
"adjoining-classes": false,
"box-sizing": false,
"box-model": false,
"compatible-vendor-prefixes": false,
"floats": false,
"font-sizes": false,
"gradients": false,
"important": false,
"known-properties": false,
"outline-none": false,
"qualified-headings": false,
"regex-selectors": false,
"shorthand": false,
"text-indent": false,
"unique-headings": false,
"universal-selector": false,
"unqualified-attributes": false,
"zero-units": false
}

View File

@@ -10,5 +10,5 @@ docs
node_modules
bin
db
src/webserver/public/lib/*
src/webserver/public/libs
checkDeployment.sh

View File

@@ -10,6 +10,7 @@ indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = outside
[*.md]
trim_trailing_whitespace = false

View File

@@ -1 +1,4 @@
gruntfile.js
node_modules
src/webserver/public/dist
src/webserver/public/libs
*.min.js

View File

@@ -22,14 +22,20 @@
"unicodeCodePointEscapes": true,
"globalReturn": true,
"jsx": true
},
},
"rules": {
"accessor-pairs": 2,
"block-scoped-var": 2,
"complexity": [0, 11],
"complexity": [
0,
11
],
"curly": 2,
"default-case": 2,
"dot-location": [2, "property"],
"dot-location": [
2,
"property"
],
"dot-notation": 2,
"eqeqeq": 2,
"no-alert": 2,
@@ -37,7 +43,6 @@
"no-case-declarations": 2,
"no-div-regex": 2,
"no-else-return": 2,
"no-empty-label": 2,
"no-eq-null": 2,
"no-eval": 2,
"no-extend-native": 2,
@@ -59,7 +64,7 @@
"no-octal": 2,
"no-octal-escape": 2,
"no-param-reassign": 2,
"no-process-env": 2,
"no-process-env": 0,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": 2,
@@ -72,13 +77,25 @@
"no-useless-call": 2,
"no-useless-concat": 2,
"no-void": 2,
"no-warning-comments": [0, {"terms": ["todo", "fixme"], "location": "start"}],
"no-warning-comments": [
0,
{
"terms": [
"todo",
"fixme"
],
"location": "start"
}
],
"no-with": 2,
"radix": 2,
"vars-on-top": 2,
"wrap-iife": 2,
"yoda": 2,
"init-declarations": [2, "always"],
"init-declarations": [
2,
"always"
],
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
@@ -87,84 +104,188 @@
"no-undef": 2,
"no-undef-init": 2,
"no-undefined": 2,
"no-unused-vars": 2,
"no-use-before-define": 2,
"no-unused-vars": 1,
"callback-return": 2,
"global-require": 2,
"global-require": 0,
"handle-callback-err": 2,
"no-mixed-requires": 2,
"no-new-require": 2,
"no-path-concat": 2,
"no-process-exit": 2,
"array-bracket-spacing": [2, "always"],
"block-spacing": [2, "always"],
"no-process-exit": 0,
"array-bracket-spacing": [
0,
"never"
],
"block-spacing": [
2,
"always"
],
"brace-style": 2,
"camelcase": 2,
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"computed-property-spacing": [2, "never"],
"consistent-this": [2, "that"],
"comma-spacing": [
2,
{
"before": false,
"after": true
}
],
"comma-style": [
2,
"last"
],
"computed-property-spacing": [
2,
"never"
],
"consistent-this": [
2,
"self"
],
"eol-last": 2,
"func-names": 2,
"indent": [2,4],
"key-spacing": [2, {"beforeColon": false, "afterColon": true}],
"indent": [
2,
4,
{
"SwitchCase": 1
}
],
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"keyword-spacing": 2,
"linebreak-style": 2,
"lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true }],
"max-depth": [2, 5],
"max-len": [2, 80, 4, {"ignoreUrls": true}],
"max-nested-callbacks": [2, 3],
"max-params": [2, 4],
"max-statements": [2, 10],
"lines-around-comment": [
0,
{
"beforeBlockComment": false,
"beforeLineComment": false
}
],
"max-depth": [
2,
5
],
"max-len": [
2,
160,
4,
{
"ignoreUrls": true,
"ignorePattern": "(/.*/|`.*`)"
}
],
"max-nested-callbacks": [
2,
3
],
"max-params": [
2,
8
],
"max-statements": [
2,
40
],
"new-cap": 2,
"new-parens": 2,
"newline-after-var": 2,
"newline-after-var": 0,
"no-array-constructor": 2,
"no-bitwise": 2,
"no-continue": 2,
"no-inline-comments": 2,
"no-inline-comments": 0,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [0, {"max": 2}],
"no-multiple-empty-lines": [
0,
{
"max": 2
}
],
"no-negated-condition": 2,
"no-nested-ternary": 2,
"no-new-object": 2,
"no-plusplus": 2,
"no-plusplus": 0,
"no-restricted-syntax": 2,
"no-whitespace-before-property": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-unneeded-ternary": 2,
"object-curly-spacing": [2, "never"],
"one-var": [2, "never"],
"operator-assignment": [2, "never"],
"operator-linebreak": [2, "after"],
"padded-blocks": [2, "never"],
"quote-props": [2, "always"],
"quotes": [2, "single"],
"require-jsdoc": [2, {
"require": {
"FunctionDeclaration": true,
"MethodDefinition": false,
"ClassDeclaration": false
"object-curly-spacing": [
2,
"never"
],
"one-var": [
2,
"never"
],
"operator-assignment": [
2,
"never"
],
"operator-linebreak": [
2,
"after"
],
"padded-blocks": [
2,
"never"
],
"quote-props": [
2,
"always"
],
"quotes": [
2,
"single"
],
"require-jsdoc": [
2,
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": false,
"ClassDeclaration": false
}
}
}],
],
"semi-spacing": 2,
"semi": [2, "always"],
"semi": [
2,
"always"
],
"sort-vars": 2,
"sort-imports": 2,
"space-before-blocks": 2,
"space-before-function-paren": 2,
"space-in-parens": [2, "never"],
"space-in-parens": [
2,
"never"
],
"space-infix-ops": 2,
"space-unary-ops": 2,
"spaced-comment": [2, "always"],
"spaced-comment": [
2,
"always"
],
"wrap-regex": 2
},
"env": {
"es6": true,
"node": true,
"browser": true
"browser": true,
"jquery": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"extends": "eslint:recommended"
}

4
.gitignore vendored
View File

@@ -63,6 +63,10 @@ bin
# Project files #
#################
static/webserver/public/libs/**
src/webserver/public/libs/**
src/webserver/public/dist/
db/**
heapdump
*.map
*.min.js
*.min.css

View File

@@ -1,3 +1,29 @@
## Changes from 0.1.0-RC5 to 0.1.0-RC6
* updated NPM/Bower packages
* updated FFmpeg to 2.8.6
* switched to a NGINX-RTMP fork of [Sergey Dryabzhinsky](https://github.com/sergey-dryabzhinsky/nginx-rtmp-module)
* added ECMA6 development mode (RS_NODE_ENV=dev) and updated NodeJS to 5.7
* refactored frontend structure
* finished ECMA6 frontend remodeling
* started backend refactoring
* optimized fake audio process (resolved NGINX error "hls: force fragment split")
* added FFmpeg patch of [Andrew Shulgin](https://github.com/andrew-shulgin) (Ignore invalid sprop-parameter-sets missing PPS)
* renamed environment variables (old environment variables are still supported but will be deprecated in the future)
* RS_NODE_PORT
* RS_NODE_ENV
* RS_LOGGER_LEVEL
* RS_TIMEZONE
* RS_SNAPSHOT_REFRESH_INTERVAL
* RS_CREATE_HEAPDUMPS
* RS_USERNAME
* RS_PASSWORD
* several small bugfixes and improvements
#### Team enlargement
* [Andrew Shulgin](https://github.com/andrew-shulgin) - Many thanks for your support and welcome to our team!
## Changes from 0.1.0-RC4.1 to 0.1.0-RC5
* updated NPM packages, NGINX to 1.9.9 and FFmpeg to 2.8.5

View File

@@ -1,101 +1,130 @@
FROM node:4.2.6-slim
FROM node:5.7.1-slim
MAINTAINER datarhei <info@datarhei.org>
ENV FFMPEG_VERSION 2.8.5
ENV FFMPEG_VERSION 2.8.6
ENV YASM_VERSION 1.3.0
ENV LAME_VERSION 3_99_5
ENV NGINX_VERSION 1.9.9
ENV NGINX_RTMP_VERSION 1.1.7
ENV NGINX_RTMP_VERSION 1.1.7.10
ENV SRC /usr/local
ENV LD_LIBRARY_PATH ${SRC}/lib
ENV PKG_CONFIG_PATH ${SRC}/lib/pkgconfig
ENV SRC "/usr/local"
ENV LD_LIBRARY_PATH "${SRC}/lib"
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake perl build-essential libpcre3-dev"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev"
RUN rm -rf /var/lib/apt/lists/* && \
apt-get update && \
apt-get install -y --force-yes curl wget git libpcre3 tar ${BUILDDEPS}
apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS}
# yasm
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz && \
tar xzvf yasm-${YASM_VERSION}.tar.gz && \
cd yasm-${YASM_VERSION} && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz" && \
tar xzvf "yasm-${YASM_VERSION}.tar.gz" && \
cd "yasm-${YASM_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# x264
RUN DIR=$(mktemp -d) && cd ${DIR} && \
git clone --depth 1 git://git.videolan.org/x264 && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
git clone --depth 1 "git://git.videolan.org/x264" && \
cd x264 && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" --enable-static && \
make && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-static \
--disable-cli && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# libmp3lame
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz && \
tar xzvf RELEASE__${LAME_VERSION}.tar.gz && \
cd lame-RELEASE__${LAME_VERSION} && \
./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --enable-nasm && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz" && \
tar xzvf "RELEASE__${LAME_VERSION}.tar.gz" && \
cd "lame-RELEASE__${LAME_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-nasm \
--disable-shared && \
make -j"$(nproc)" && \
make install && \
make distclean&& \
rm -rf ${DIR}
make distclean && \
rm -rf "${DIR}"
# ffmpeg
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \
tar xzvf ffmpeg-${FFMPEG_VERSION}.tar.gz && \
cd ffmpeg-${FFMPEG_VERSION} && \
./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" \
--extra-libs=-ldl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-gpl \
--enable-postproc --enable-nonfree --enable-avresample --disable-debug --enable-small --enable-openssl \
--disable-doc --disable-ffserver && \
make && \
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
curl -Lks "https://github.com/FFmpeg/FFmpeg/commit/1c7e2cf9d33968375ee4025d2279c937e147dae2.patch" | patch -p1 && \
cd "ffmpeg-${FFMPEG_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--extra-cflags="-I${SRC}/include" \
--extra-ldflags="-L${SRC}/lib" \
--extra-libs=-ldl \
--enable-nonfree \
--enable-gpl \
--enable-version3 \
--enable-avresample \
--enable-libmp3lame \
--enable-libx264 \
--enable-openssl \
--enable-postproc \
--enable-small \
--disable-debug \
--disable-doc \
--disable-ffserver && \
make -j"$(nproc)" && \
make install && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && \
cp qt-faststart ${SRC}/bin && \
rm -rf ${DIR}
RUN echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf
cp qt-faststart "${SRC}/bin" && \
rm -rf "${DIR}"
RUN echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf"
RUN ffmpeg -buildconf
# nginx-rtmp
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz && \
tar xzvf release-${NGINX_VERSION}.tar.gz && \
curl -LOks https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz && \
tar xzvf v${NGINX_RTMP_VERSION}.tar.gz && \
cd nginx-release-${NGINX_VERSION} && \
auto/configure --with-http_ssl_module --add-module=../nginx-rtmp-module-${NGINX_RTMP_VERSION} && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz" && \
tar xzvf "release-${NGINX_VERSION}.tar.gz" && \
curl -LOks "https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz" && \
tar xzvf "v${NGINX_RTMP_VERSION}.tar.gz" && \
cd "nginx-release-${NGINX_VERSION}" && \
auto/configure \
--with-http_ssl_module \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \
make install && \
rm -rf ${DIR}
rm -rf "${DIR}"
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \
apt-get install -y --force-yes git && \
rm -rf /tmp/*
COPY . /restreamer
WORKDIR /restreamer
RUN npm install -g bower grunt-cli public-ip eslint@v2.0.0-beta.3 && \
RUN npm install -g bower grunt grunt-cli nodemon public-ip eslint && \
npm install && \
grunt build && \
npm prune --production
npm prune --production && \
npm cache clean && \
bower cache clean --allow-root
ENV RESTREAMER_USERNAME admin
ENV RESTREAMER_PASSWORD datarhei
ENV RS_USERNAME admin
ENV RS_PASSWORD datarhei
EXPOSE 8080
VOLUME ["/restreamer/db"]

View File

@@ -2,115 +2,144 @@ FROM resin/rpi-raspbian:jessie
MAINTAINER datarhei <info@datarhei.org>
ENV NODE_VERSION 4.2.6
ENV NPM_VERSION 2.14.12
ENV NODE_VERSION 5.7.1
ENV NPM_VERSION 3.6.0
ENV FFMPEG_VERSION 2.8.5
ENV FFMPEG_VERSION 2.8.6
ENV YASM_VERSION 1.3.0
ENV LAME_VERSION 3_99_5
ENV NGINX_VERSION 1.9.9
ENV NGINX_RTMP_VERSION 1.1.7
ENV NGINX_RTMP_VERSION 1.1.7.10
ENV SRC /usr/local
ENV LD_LIBRARY_PATH ${SRC}/lib
ENV PKG_CONFIG_PATH ${SRC}/lib/pkgconfig
ENV SRC "/usr/local"
ENV LD_LIBRARY_PATH "${SRC}/lib"
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake perl build-essential libpcre3-dev"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev"
RUN rm -rf /var/lib/apt/lists/* && \
apt-get update && \
apt-get install -y curl git libpcre3 tar ${BUILDDEPS}
apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS}
# node
RUN DIR=$(mktemp -d) && cd ${DIR} && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
set -x && \
curl -LOks https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-armv6l.tar.gz && \
tar -xzf "node-v$NODE_VERSION-linux-armv6l.tar.gz" -C /usr/local --strip-components=1 && \
npm install -g npm@"$NPM_VERSION" --unsafe-perm && \
curl -LOks "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-armv6l.tar.gz" && \
tar xzvf "node-v${NODE_VERSION}-linux-armv6l.tar.gz" \
-C "${SRC}" \
--strip-components=1 && \
npm install -g "npm@${NPM_VERSION}" --unsafe-perm && \
npm cache clear && \
npm config set unsafe-perm true -g --unsafe-perm && \
rm -rf ${DIR}
rm -rf "${DIR}"
# yasm
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz && \
tar xzvf yasm-${YASM_VERSION}.tar.gz && \
cd yasm-${YASM_VERSION} && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz" && \
tar xzvf "yasm-${YASM_VERSION}.tar.gz" && \
cd "yasm-${YASM_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# x264
RUN DIR=$(mktemp -d) && cd ${DIR} && \
git clone --depth 1 git://git.videolan.org/x264 && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
git clone --depth 1 "git://git.videolan.org/x264" && \
cd x264 && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" --enable-static && \
make && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-static \
--disable-cli && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# libmp3lame
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz && \
tar xzvf RELEASE__${LAME_VERSION}.tar.gz && \
cd lame-RELEASE__${LAME_VERSION} && \
./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --enable-nasm && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz" && \
tar xzvf "RELEASE__${LAME_VERSION}.tar.gz" && \
cd "lame-RELEASE__${LAME_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-nasm \
--disable-shared && \
make -j"$(nproc)" && \
make install && \
make distclean&& \
rm -rf ${DIR}
make distclean && \
rm -rf "${DIR}"
# ffmpeg
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \
tar xzvf ffmpeg-${FFMPEG_VERSION}.tar.gz && \
cd ffmpeg-${FFMPEG_VERSION} && \
./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" \
--extra-libs=-ldl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-gpl \
--enable-postproc --enable-nonfree --enable-avresample --disable-debug --enable-small --enable-openssl \
--disable-doc --disable-ffserver && \
make && \
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
curl -Lks "https://github.com/FFmpeg/FFmpeg/commit/1c7e2cf9d33968375ee4025d2279c937e147dae2.patch" | patch -p1 && \
cd "ffmpeg-${FFMPEG_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--extra-cflags="-I${SRC}/include" \
--extra-ldflags="-L${SRC}/lib" \
--extra-libs=-ldl \
--enable-nonfree \
--enable-gpl \
--enable-version3 \
--enable-avresample \
--enable-libmp3lame \
--enable-libx264 \
--enable-openssl \
--enable-postproc \
--enable-small \
--disable-debug \
--disable-doc \
--disable-ffserver && \
make -j"$(nproc)" && \
make install && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && \
cp qt-faststart ${SRC}/bin && \
rm -rf ${DIR}
RUN echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf
cp qt-faststart "${SRC}/bin" && \
rm -rf "${DIR}"
RUN echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf"
RUN ffmpeg -buildconf
# nginx-rtmp
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz && \
tar xzvf release-${NGINX_VERSION}.tar.gz && \
curl -LOks https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz && \
tar xzvf v${NGINX_RTMP_VERSION}.tar.gz && \
cd nginx-release-${NGINX_VERSION} && \
auto/configure --with-http_ssl_module --add-module=../nginx-rtmp-module-${NGINX_RTMP_VERSION} && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz" && \
tar xzvf "release-${NGINX_VERSION}.tar.gz" && \
curl -LOks "https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz" && \
tar xzvf "v${NGINX_RTMP_VERSION}.tar.gz" && \
cd "nginx-release-${NGINX_VERSION}" && \
auto/configure \
--with-http_ssl_module \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \
make install && \
rm -rf ${DIR}
rm -rf "${DIR}"
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \
rm -rf /tmp/*
RUN apt-get update && \
apt-get install -y --force-yes git
COPY . /restreamer
WORKDIR /restreamer
RUN npm install -g bower grunt-bower grunt-cli public-ip && \
RUN npm install -g bower grunt grunt-cli nodemon public-ip && \
npm install && \
grunt build && \
npm prune --production
npm prune --production && \
npm cache clean && \
bower cache clean --allow-root
ENV RESTREAMER_USERNAME admin
ENV RESTREAMER_PASSWORD datarhei
ENV RS_USERNAME admin
ENV RS_PASSWORD datarhei
EXPOSE 8080
VOLUME ["/restreamer/db"]

View File

@@ -1,114 +1,145 @@
FROM armbuild/debian:jessie
FROM resin/rpi-raspbian:jessie
MAINTAINER datarhei <info@datarhei.org>
ENV NODE_VERSION 4.2.6
ENV NPM_VERSION 2.14.12
ENV NODE_VERSION 5.7.1
ENV NPM_VERSION 3.6.0
ENV FFMPEG_VERSION 2.8.5
ENV FFMPEG_VERSION 2.8.6
ENV YASM_VERSION 1.3.0
ENV LAME_VERSION 3_99_5
ENV NGINX_VERSION 1.9.9
ENV NGINX_RTMP_VERSION 1.1.7
ENV NGINX_RTMP_VERSION 1.1.7.10
ENV SRC /usr/local
ENV LD_LIBRARY_PATH ${SRC}/lib
ENV PKG_CONFIG_PATH ${SRC}/lib/pkgconfig
ENV SRC "/usr/local"
ENV LD_LIBRARY_PATH "${SRC}/lib"
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake perl build-essential libpcre3-dev"
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev"
RUN rm -rf /var/lib/apt/lists/* && \
apt-get update && \
apt-get install -y curl git libpcre3 tar ${BUILDDEPS}
apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS}
# node
RUN DIR=$(mktemp -d) && cd ${DIR} && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
set -x && \
curl -LOks https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-armv7l.tar.gz && \
tar -xzf "node-v$NODE_VERSION-linux-armv7l.tar.gz" -C /usr/local --strip-components=1 && \
npm install -g npm@"$NPM_VERSION" --unsafe-perm && \
curl -LOks "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-armv7l.tar.gz" && \
tar xzvf "node-v${NODE_VERSION}-linux-armv7l.tar.gz" \
-C "${SRC}" \
--strip-components=1 && \
npm install -g "npm@${NPM_VERSION}" --unsafe-perm && \
npm cache clear && \
npm config set unsafe-perm true -g --unsafe-perm && \
rm -rf ${DIR}
rm -rf "${DIR}"
# yasm
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz && \
tar xzvf yasm-${YASM_VERSION}.tar.gz && \
cd yasm-${YASM_VERSION} && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz" && \
tar xzvf "yasm-${YASM_VERSION}.tar.gz" && \
cd "yasm-${YASM_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# x264
RUN DIR=$(mktemp -d) && cd ${DIR} && \
git clone --depth 1 git://git.videolan.org/x264 && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
git clone --depth 1 "git://git.videolan.org/x264" && \
cd x264 && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" --enable-static && \
make && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-static \
--disable-cli && \
make -j"$(nproc)" && \
make install && \
make distclean && \
rm -rf ${DIR}
rm -rf "${DIR}"
# libmp3lame
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz && \
tar xzvf RELEASE__${LAME_VERSION}.tar.gz && \
cd lame-RELEASE__${LAME_VERSION} && \
./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --enable-nasm && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz" && \
tar xzvf "RELEASE__${LAME_VERSION}.tar.gz" && \
cd "lame-RELEASE__${LAME_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-nasm \
--disable-shared && \
make -j"$(nproc)" && \
make install && \
make distclean&& \
rm -rf ${DIR}
make distclean && \
rm -rf "${DIR}"
# ffmpeg
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \
tar xzvf ffmpeg-${FFMPEG_VERSION}.tar.gz && \
cd ffmpeg-${FFMPEG_VERSION} && \
./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" \
--extra-libs=-ldl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-gpl \
--enable-postproc --enable-nonfree --enable-avresample --disable-debug --enable-small --enable-openssl \
--disable-doc --disable-ffserver && \
make && \
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
curl -Lks "https://github.com/FFmpeg/FFmpeg/commit/1c7e2cf9d33968375ee4025d2279c937e147dae2.patch" | patch -p1 && \
cd "ffmpeg-${FFMPEG_VERSION}" && \
./configure \
--prefix="${SRC}" \
--bindir="${SRC}/bin" \
--extra-cflags="-I${SRC}/include" \
--extra-ldflags="-L${SRC}/lib" \
--extra-libs=-ldl \
--enable-nonfree \
--enable-gpl \
--enable-version3 \
--enable-avresample \
--enable-libmp3lame \
--enable-libx264 \
--enable-openssl \
--enable-postproc \
--enable-small \
--disable-debug \
--disable-doc \
--disable-ffserver && \
make -j"$(nproc)" && \
make install && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && \
cp qt-faststart ${SRC}/bin && \
rm -rf ${DIR}
RUN echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf
cp qt-faststart "${SRC}/bin" && \
rm -rf "${DIR}"
RUN echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf"
RUN ffmpeg -buildconf
# nginx-rtmp
RUN DIR=$(mktemp -d) && cd ${DIR} && \
curl -LOks https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz && \
tar xzvf release-${NGINX_VERSION}.tar.gz && \
curl -LOks https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz && \
tar xzvf v${NGINX_RTMP_VERSION}.tar.gz && \
cd nginx-release-${NGINX_VERSION} && \
auto/configure --with-http_ssl_module --add-module=../nginx-rtmp-module-${NGINX_RTMP_VERSION} && \
make && \
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz" && \
tar xzvf "release-${NGINX_VERSION}.tar.gz" && \
curl -LOks "https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/v${NGINX_RTMP_VERSION}.tar.gz" && \
tar xzvf "v${NGINX_RTMP_VERSION}.tar.gz" && \
cd "nginx-release-${NGINX_VERSION}" && \
auto/configure \
--with-http_ssl_module \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \
make install && \
rm -rf ${DIR}
rm -rf "${DIR}"
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \
apt-get install -y git && \
rm -rf /tmp/*
COPY . /restreamer
WORKDIR /restreamer
RUN npm install -g bower grunt-bower grunt-cli public-ip && \
RUN npm install -g bower grunt grunt-cli nodemon public-ip && \
npm install && \
grunt build && \
npm prune --production
npm prune --production && \
npm cache clean && \
bower cache clean --allow-root
ENV RESTREAMER_USERNAME admin
ENV RESTREAMER_PASSWORD datarhei
ENV RS_USERNAME admin
ENV RS_PASSWORD datarhei
EXPOSE 8080
VOLUME ["/restreamer/db"]

View File

@@ -1,30 +1,46 @@
#Restreamer
Datarhei/Restreamer offers smart free video streaming in real time. Stream H.264 video of IP cameras live to your website. Upload your live video on [YouTube-Live](https://www.youtube.com/), [Ustream](http://www.ustream.tv/), [Twitch](http://www.twitch.tv/), [Livestream.com](http://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.
##Features
- User-Interface including login-security
- JSON / HTTP-API
- <a target= "_blank" href="http://ffmpeg.org/">FFmpeg</a> streaming/encoding the video/camera-stream, creating snapshots or pushing to a external streaming-endpoint
- <a target= "_blank" href="http://nginx.org/">NGINX</a> incl. <a target= "_blank" href="https://github.com/arut/nginx-rtmp-module">RTMP-Module</a> as streaming-backend and hls server
- <a target= "_blank" href="http://nginx.org/">NGINX</a> incl. <a target= "_blank" href="https://github.com/sergey-dryabzhinsky/nginx-rtmp-module">RTMP-Module</a> as streaming-backend and hls server
- <a target= "_blank" href="https://github.com/clappr/clappr">Clappr-Player</a> to embed your stream on your website
- <a target= "_blank" href="https://www.docker.com/">Docker</a> and <a target= "_blank" href="https://kitematic.com/">Kitematic (Docker-Toolbox)</a> optimizations and very easy installation
##Roadmap
- RC6 (tba)
## Upcomming releases
- RC7 (tba)
## Roadmap
- optimizing FFmpeg handling
- backend refactoring
- full REST API
- security improvements
##Documentation
Documentation is available on [Datarhei/Restreamer GitHub pages](https://datarhei.github.io/restreamer/).
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.
More additional informations about streaming, cameras and so on you can find in our [Wiki](https://datarhei.github.com/restreamer/wiki).
##Help / Bugs
If you have problems or found a bug feel free to create a new issue upon the <a target= "_blank" href="https://github.com/datarhei/restreamer/issues">Github issue management</a>.
Want to talk to us? Write an email to <a href="mailto:open@datarhei.org?subject=Datarhei/Restreamer">open@datarhei.org</a>, go to [Support](../support.html) or choose a nickname speak to us in IRC: <a href="irc://irc.freenode.net#piwik">irc.freenode.net/#datarhei</a> (<a target= "_blank" href="https://webchat.freenode.net/?channels=datarhei">webchat</a>). You could ask a question in our (<a target= "_blank" href="https://groups.google.com/forum/#!forum/datarhei">Forum</a>) on Google Groups, too.
##Authors
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).
Special thanks for supporting this project continuously to [Andrew Shulgin](https://github.com/andrew-shulgin).
##Copyright
Code released under the [Apache license](LICENSE). Images are copyrighted by datarhei.org

View File

@@ -1,20 +1,20 @@
{
"name": "Restreamer",
"version": "0.1.0-RC5",
"license": "Apache-2.0",
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.0",
"html5shiv": "3.7.3",
"respond": "1.4.2",
"angular-bootstrap": "~0.14.3",
"angular-animate": "1.4.9",
"ui-router": "~0.2.15",
"angular-translate": "~2.9.0",
"angular-translate-loader-static-files": "~2.9.0"
},
"resolutions": {
"angular": "1.4.8",
"angular-bootstrap": "~0.14.3"
}
"name": "Restreamer",
"version": "0.1.0-RC6",
"license": "Apache-2.0",
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.1",
"html5shiv": "3.7.3",
"respond": "1.4.2",
"angular-bootstrap": "~0.14.3",
"angular-animate": "1.5.0",
"ui-router": "0.2.18",
"angular-translate": "2.9.2",
"angular-translate-loader-static-files": "2.9.2"
},
"resolutions": {
"angular": "1.4.8",
"angular-bootstrap": "~0.14.3"
}
}

View File

@@ -1,70 +1,81 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://jsonschema.net",
"type": "object",
"properties": {
"addresses": {
"id": "http://jsonschema.net/addresses",
"type": "object",
"properties": {
"srcAddress": {
"id": "http://jsonschema.net/addresses/srcAddress",
"type": "string"
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://jsonschema.net",
"type": "object",
"properties": {
"addresses": {
"id": "http://jsonschema.net/addresses",
"type": "object",
"properties": {
"srcAddress": {
"id": "http://jsonschema.net/addresses/srcAddress",
"type": "string"
},
"optionalOutputAddress": {
"id": "http://jsonschema.net/addresses/optionalOutputAddress",
"type": "string"
}
},
"required": [
"srcAddress",
"optionalOutputAddress"
]
},
"optionalOutputAddress": {
"id": "http://jsonschema.net/addresses/optionalOutputAddress",
"type": "string"
}
},
"required": [
"srcAddress",
"optionalOutputAddress"
]
},
"states": {
"id": "http://jsonschema.net/states",
"type": "object",
"properties": {
"repeatToLocalNginx": {
"id": "http://jsonschema.net/states/repeatToLocalNginx",
"type": "object",
"properties": {
"type": {
"id": "http://jsonschema.net/states/repeatToLocalNginx/type",
"type": "string"
"options": {
"id": "http://jsonschema.net/options",
"type": "object",
"properties": {
"rtspTcp": {
"id": "http://jsonschema.net/options/rtspTcp",
"type": "boolean"
}
}
}
},
"repeatToOptionalOutput": {
"id": "http://jsonschema.net/states/repeatToOptionalOutput",
"type": "object",
"properties": {
"type": {
"id": "http://jsonschema.net/states/repeatToOptionalOutput/type",
"type": "string"
"states": {
"id": "http://jsonschema.net/states",
"type": "object",
"properties": {
"repeatToLocalNginx": {
"id": "http://jsonschema.net/states/repeatToLocalNginx",
"type": "object",
"properties": {
"type": {
"id": "http://jsonschema.net/states/repeatToLocalNginx/type",
"type": "string"
}
}
},
"repeatToOptionalOutput": {
"id": "http://jsonschema.net/states/repeatToOptionalOutput",
"type": "object",
"properties": {
"type": {
"id": "http://jsonschema.net/states/repeatToOptionalOutput/type",
"type": "string"
}
}
}
}
}
}
}
},
"userActions": {
"id": "http://jsonschema.net/userActions",
"type": "object",
"properties": {
"repeatToLocalNginx": {
"id": "http://jsonschema.net/userActions/repeatToLocalNginx",
"type": "string"
},
"repeatToOptionalOutput": {
"id": "http://jsonschema.net/userActions/repeatToOptionalOutput",
"type": "string"
"userActions": {
"id": "http://jsonschema.net/userActions",
"type": "object",
"properties": {
"repeatToLocalNginx": {
"id": "http://jsonschema.net/userActions/repeatToLocalNginx",
"type": "string"
},
"repeatToOptionalOutput": {
"id": "http://jsonschema.net/userActions/repeatToOptionalOutput",
"type": "string"
}
}
}
}
}
},
"required": [
"addresses",
"states",
"userActions"
]
},
"required": [
"addresses",
"options",
"states",
"userActions"
]
}

View File

@@ -7,23 +7,21 @@
},
"ffmpeg": {
"options": {
"native_h264":[
"-c copy",
"native_h264": [
"-codec copy",
"-map_metadata -1",
"-metadata application=datarhei/Restreamer",
"-metadata server=NGINX-RTMP",
"-f flv"
],
"native_h264_soundless_aac":[
"-ar 44100",
"-ac 2",
"-acodec pcm_s16le",
"-f s16le",
"-ac 2",
"-i /dev/zero",
"-c:v copy",
"native_h264_soundless_aac": [
"-f lavfi",
"-i aevalsrc=0",
"-vcodec copy",
"-acodec aac",
"-ab 128k",
"-map 0:0",
"-map 1:0",
"-shortest",
"-map_metadata -1",
"-metadata application=datarhei/Restreamer",
"-metadata server=NGINX-RTMP",
@@ -43,5 +41,71 @@
"rtmp_port": "1935",
"rtmp_hls_path": "/hls/"
}
}
},
"envVars": [
{
"name": "RS_NODE_PORT",
"alias": "NODEJS_PORT",
"type": "int",
"defaultValue": "3000",
"required": false,
"description": "Webserver port of application"
},
{
"name": "RS_NODE_ENV",
"alias": "NODE_ENV",
"type": "string",
"defaultValue": "prod",
"required": false,
"description": "Nodejs Environment"
},
{
"name": "RS_LOGGER_LEVEL",
"alias": "LOGGER_LEVEL",
"type": "int",
"defaultValue": "3",
"required": true,
"description": "Logger level to defined, what should be logged"
},
{
"name": "RS_TIMEZONE",
"alias": "TIMEZONE",
"type": "string",
"defaultValue": "Europe/Berlin",
"required": true,
"description": "Set the timezone"
},
{
"name": "RS_SNAPSHOT_REFRESH_INTERVAL",
"alias": "SNAPSHOT_REFRESH_INTERVAL",
"type": "string",
"defaultValue": "1m",
"required": false,
"description": "Interval to create a new Snapshot (in minutes)"
},
{
"name": "RS_CREATE_HEAPDUMPS",
"alias": "CREATE_HEAPDUMPS",
"type": "bool",
"defaultValue": "false",
"required": false,
"description": "Create Heapdumps of application"
},
{
"name": "RS_USERNAME",
"alias": "RESTREAMER_USERNAME",
"type": "string",
"defaultValue": "admin",
"required": false,
"description": "Backend user name"
},
{
"name": "RS_PASSWORD",
"alias": "RESTREAMER_PASSWORD",
"type": "string",
"defaultValue": "datarhei",
"required": false,
"description": "Backend login password"
}
]
}

View File

@@ -10,18 +10,14 @@ rtmp {
application live {
live on;
meta copy;
allow publish 127.0.0.1;
deny publish all;
}
application hls {
live on;
hls on;
hls_type live;
hls_playlist_length 60s;
hls_fragment 2s;
hls_path /tmp/hls;
meta copy;
allow publish 127.0.0.1;
deny publish all;
}
}
}
@@ -30,6 +26,12 @@ http {
tcp_nopush on;
server {
listen 8080;
location ~ ^/(libs|locales|images|dist|help|css|scripts|player.html|crossdomain.xml) {
root /restreamer/src/webserver/public;
allow all;
include /usr/local/nginx/conf/mime.types;
add_header Access-Control-Allow-Origin *;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
@@ -37,20 +39,6 @@ http {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location /player.html {
root /restreamer/bin/webserver/public;
allow all;
}
location /libs {
root /restreamer/bin/webserver/public;
allow all;
include /usr/local/nginx/conf/mime.types;
add_header Access-Control-Allow-Origin *;
}
location /images {
root /restreamer/bin/webserver/public;
allow all;
}
location /hls {
types {
application/vnd.apple.mpegurl m3u8;

View File

@@ -1,59 +1,58 @@
'use strict';
module.exports = function(grunt) {
// files for this project
var files = {
compiledFrontendJS: ['bin/webserver/public/scripts/**/*.js', 'bin/executors/**/gui/js/*.js'],
es6Src: ['**/*.js'],
stylesheets: ['static/webserver/public/css/*.css']
};
// path to store the transpiled es6 files
const transpiledPath = "src/webserver/transpiled/";
const files = {
//workaround to keep correct order
transpiledFrontendJs: [
`${transpiledPath}/webserver/public/scripts/App.js`,
`${transpiledPath}/webserver/public/scripts/App.Config.js`,
`${transpiledPath}/webserver/public/scripts/Main/MainModule.js`,
`${transpiledPath}/webserver/public/scripts/Main/MainController.js`,
`${transpiledPath}/webserver/public/scripts/Login/LoginModule.js`,
`${transpiledPath}/webserver/public/scripts/Login/LoginController.js`,
`${transpiledPath}/webserver/public/scripts/Header/HeaderModule.js`,
`${transpiledPath}/webserver/public/scripts/Header/HeaderController.js`,
`${transpiledPath}/webserver/public/scripts/Header/HeaderDirective.js`,
`${transpiledPath}/webserver/public/scripts/Footer/FooterModule.js`,
`${transpiledPath}/webserver/public/scripts/Footer/FooterController.js`,
`${transpiledPath}/webserver/public/scripts/Footer/FooterDirective.js`,
`${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingInterfaceModule.js`,
`${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingStatusController.js`,
`${transpiledPath}/webserver/public/scripts/StreamingInterface/StreamingStatusDirective.js`,
`${transpiledPath}/webserver/public/scripts/Shared/LoggerService.js`,
`${transpiledPath}/webserver/public/scripts/Shared/WebsocketsService.js`
],
es6Src: [
'webserver/public/scripts/**/*.js'
],
stylesheets: ['src/webserver/public/css/*.css']
};
module.exports = function (grunt) {
// Project Configuration
grunt.initConfig({
/*
Watcher config with livereload
*/
watch: {
scripts: {
files: ['src/**/*.js'],
tasks: ['compile-code'],
options: {
interrupt: true,
livereload: {
host: 'localhost',
port: 5000
}
}
},
statics: {
files: ['static/**/*.html'],
tasks: ['copy-statics'],
options: {
interrupt: true,
livereload: {
host: 'localhost',
port: 5000
}
}
}
},
/*
Config for shell commands
*/
Config for shell commands
*/
shell: {
start: {
command: 'npm start'
},
removeOldBinFolder: {
command: 'rm -Rf bin/'
removeTempTranspilingFolder: {
command: `rm -Rf ${transpiledPath}`
},
createBinFolder: {
command: 'mkdir bin/'
},
copyStatics: {
command: 'cp -R static/* bin'
createTempTranspilingFolder: {
command: `mkdir ${transpiledPath}`
},
bower: {
command: 'bower install --allow-root'
@@ -63,13 +62,13 @@ module.exports = function(grunt) {
},
//temp workaround - https://github.com/clappr/clappr/issues/709
clappr: {
command: 'curl -LOks https://github.com/clappr/clappr/archive/master.tar.gz && tar xzvf master.tar.gz && rm master.tar.gz && mv clappr-master bin/webserver/public/libs/clappr'
command: 'rm -rf src/webserver/public/libs/clappr && git clone --depth 1 git://github.com/clappr/clappr src/webserver/public/libs/clappr'
}
},
/*
Config for Babel compiling
*/
Config for Babel compiling
*/
babel: {
options: {
sourceMap: true,
@@ -80,16 +79,16 @@ module.exports = function(grunt) {
{
expand: true,
cwd: 'src/',
src:'<%= es6Src %>',
dest: 'bin/'
src: '<%= es6Src %>',
dest: transpiledPath
}
]
}
},
/*
Config for eslinter
*/
Config for eslinter
*/
eslint: {
all: ['src/**/*.js'],
options: {
@@ -98,19 +97,19 @@ module.exports = function(grunt) {
},
/*
config for css linter
config for css linter
*/
csslint: {
options: {
csslintrc: '.csslintrc'
},
all: {
src: ['static/webserver/public/css/*.css']
},
all: {
src: ['src/webserver/public/css/*.css']
}
},
/*
uglify and minify frontend javascript
uglify and minify frontend javascript
*/
uglify: {
production: {
@@ -118,41 +117,41 @@ module.exports = function(grunt) {
mangle: true
},
files: {
'bin/webserver/public/dist/application.min.js': 'bin/webserver/public/dist/application.js'
'src/webserver/public/dist/application.min.js': 'src/webserver/public/dist/application.js'
}
}
},
/*
minify css files
minify css files
*/
cssmin: {
combine: {
files: {
'bin/webserver/public/css/restreamer.min.css': '<%= stylesheets %>'
'src/webserver/public/css/restreamer.min.css': '<%= stylesheets %>'
}
}
},
/*
produces one file from all fontend javascript bewaring DI naming of angular
produces one file from all fontend javascript bewaring DI naming of angular
*/
ngAnnotate: {
production: {
files: {
'bin/webserver/public/dist/application.js': '<%= compiledFrontendJS %>'
'src/webserver/public/dist/application.js': '<%= transpiledFrontendJs %>'
}
}
}
});
}
});
/*
Load NPM tasks
*/
require('load-grunt-tasks')(grunt);
grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function() {
grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function () {
grunt.config.set('es6Src', files.es6Src);
grunt.config.set('compiledFrontendJS', files.compiledFrontendJS);
grunt.config.set('transpiledFrontendJs', files.transpiledFrontendJs);
grunt.config.set('stylesheets', files.stylesheets);
});
grunt.loadNpmTasks('grunt-shell');
@@ -163,27 +162,26 @@ module.exports = function(grunt) {
*/
// lint
grunt.registerTask('lint', ['csslint', 'shell:eslint']);
// clear old bin folder and create new one
grunt.registerTask('clearOldBuild', ['shell:removeOldBinFolder', 'shell:createBinFolder']);
// clear old transpile folder and create new one
grunt.registerTask('clearOldBuild', ['shell:removeTempTranspilingFolder', 'shell:createTempTranspilingFolder']);
// install frontendlibraries (atm through bower)
grunt.registerTask('installFrontendLibraries', ['shell:bower', 'shell:clappr']);
// minify the frontend files
grunt.registerTask('minifyFrontendFiles', ['cssmin', 'ngAnnotate', 'uglify']);
/*
Build Tasks
Build Tasks
*/
grunt.registerTask('build', ['loadConfig','clearOldBuild', 'shell:copyStatics', 'babel', 'minifyFrontendFiles', 'installFrontendLibraries']);
grunt.registerTask('compile-code', ['loadConfig','babel', 'shell:copyStatics', 'minifyFrontendFiles']);
grunt.registerTask('copy-statics', ['loadConfig', 'shell:copyStatics']);
grunt.registerTask('build', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles', 'installFrontendLibraries', 'shell:removeTempTranspilingFolder']);
/*
Run Tasks
Just Compile
*/
grunt.registerTask('compile', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles']);
/*
Run Tasks
*/
// run current build in /bin
grunt.registerTask('run', ['shell:start']);
// rebuild and run
grunt.registerTask('run-clean', ['build', 'run']);
// update code and run
grunt.registerTask('run-update-code', ['loadConfig','babel','shell:copyStatics', 'minifyFrontendFiles', 'run'])
};

View File

@@ -1,40 +1,43 @@
{
"name": "Restreamer",
"version": "0.1.0-RC5",
"description": "Allows you to do h.264 real-time video streaming on your website without a streaming provider",
"author": "datarhei.org",
"license": "Apache-2.0",
"scripts": {
"start": "node ./bin/start"
},
"dependencies": {
"body-parser": "1.14.2",
"compression": "~1.6.0",
"connect-flash": "^0.1.1",
"cookie-parser": "1.4.1",
"express": "4.13.4",
"express-session": "^1.12.1",
"fluent-ffmpeg": "git://github.com/datarhei/node-fluent-ffmpeg",
"jsonschema": "^1.0.2",
"moment-timezone": "^0.5.0",
"node-json-db": "^0.5.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"ps-find": "^1.1.0",
"q": "1.4.1",
"socket.io": "1.4.5"
},
"devDependencies": {
"babel-preset-es2015": "^6.1.2",
"eslint": "^2.0.0-beta.3",
"grunt": "^0.4.5",
"grunt-babel": "^6.0.0",
"grunt-contrib-csslint": "^0.5.0",
"grunt-contrib-cssmin": "~0.14.0",
"grunt-contrib-uglify": "~0.11.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-ng-annotate": "~1.0.1",
"grunt-shell": "^1.1.2",
"load-grunt-tasks": "~3.4.0"
}
"name": "Restreamer",
"version": "0.1.0-RC6",
"description": "Allows you to do h.264 real-time video streaming on your website without a streaming provider",
"author": "datarhei.org",
"repository": {
"type": "git",
"url": "git://github.com/datarhei/restreamer.git"
},
"license": "Apache-2.0",
"scripts": {
"start": "node ./src/start"
},
"dependencies": {
"body-parser": "1.15.0",
"compression": "~1.6.1",
"cookie-parser": "1.4.1",
"express": "4.13.4",
"express-session": "^1.13.0",
"fluent-ffmpeg": "git://github.com/datarhei/node-fluent-ffmpeg",
"jsonschema": "^1.1.0",
"moment-timezone": "^0.5.0",
"node-json-db": "git://github.com/andrew-shulgin/node-json-db",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"ps-find": "^1.1.0",
"q": "1.4.1",
"socket.io": "1.4.5"
},
"devDependencies": {
"eslint": "2.3.0",
"babel-preset-es2015": "^6.5.0",
"grunt": "0.4.5",
"grunt-babel": "^6.0.0",
"grunt-contrib-csslint": "^1.0.0",
"grunt-contrib-cssmin": "~1.0.0",
"grunt-contrib-uglify": "~1.0.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-ng-annotate": "~1.0.1",
"grunt-shell": "1.2.1",
"load-grunt-tasks": "~3.4.0"
}
}

4
run.sh
View File

@@ -15,7 +15,7 @@ then
sleep 5
fi
done
/opt/vc/bin/raspivid -t 0 -w 1280 -h 720 -fps 25 -b 500000 -o - | ffmpeg -i - -c copy -f flv rtmp://127.0.0.1:1935/live/raspicam.stream
/opt/vc/bin/raspivid -t 0 -w 1280 -h 720 -fps 25 -b 500000 -o - | ffmpeg -i - -f lavfi -i aevalsrc=0 -vcodec copy -acodec aac -strict experimental -map 0:0 -map 1:0 -shortest -flags +global_header -f flv rtmp://127.0.0.1:1935/live/raspicam.stream > /dev/null 2>&1
elif [ "${MODE}" == "USBCAM" ];
then
apt-get update && apt-get install -y v4l-utils libv4l-0
@@ -30,7 +30,7 @@ then
sleep 5
fi
done
ffmpeg -f v4l2 -r 25 -s 1280x720 -i /dev/video0 -f flv rtmp://127.0.0.1:1935/live/usb.stream
ffmpeg -f v4l2 -r 25 -s 1280x720 -i /dev/video0 -f lavfi -i aevalsrc=0 -vcodec copy -acodec aac -strict experimental -map 0:0 -map 1:0 -shortest -flags +global_header -f flv rtmp://127.0.0.1:1935/live/usb.stream > /dev/null 2>&1
else
npm start
fi

View File

@@ -4,24 +4,51 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const logger = require('./Logger')('EnvVar');
/**
* Class for environment variables with default values
*/
class EnvVar {
static init (config) {
var killProcess = false;
/**
* constructs an envvar
* @param {string} name
* @param {string} required
* @param {string} defaultValue
* @param {string} description
*/
constructor (name, required, defaultValue, description) {
this.name = name;
this.required = required;
this.defaultValue = defaultValue;
this.description = description;
for (let envVar of config.envVars) {
if (typeof process.env[envVar.alias] !== 'undefined') {
process.env[envVar.name] = process.env[envVar.alias];
delete process.env[envVar.alias];
}
if (typeof process.env[envVar.name] !== 'undefined') {
logger.info(`ENV "${envVar.name} = ${process.env[envVar.name]}"`, envVar.description);
} else if (envVar.required === true) {
logger.error(`No value set for env "${envVar.name}", but it is required`);
killProcess = true;
} else {
process.env[envVar.name] = envVar.defaultValue;
logger.info(`ENV "${envVar.name} = ${process.env[envVar.name]}", set to default value`, envVar.description);
}
if (typeof process.env[envVar.name] !== 'undefined') {
switch (envVar.type) {
case 'int':
process.env[envVar.name] = parseInt(process.env[envVar.name], 10);
break;
case 'bool':
process.env[envVar.name] = process.env[envVar.name] === 'true';
break;
default: // keep strings
break;
}
}
}
if (killProcess === true) {
setTimeout(()=> {
process.exit();
}, 500);
}
}
}

View File

@@ -4,18 +4,19 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const moment = require('moment-timezone');
const LEVEL_MUTE = 0;
const LEVEL_ERROR = 1;
const LEVEL_WARN = 2;
const LEVEL_INFO = 3;
const LEVEL_DEBUG = 4;
// set default timezone to use the timezone before the default values are
process.env.TIMEZONE = process.env.TIMEZONE ? process.env.TIMEZONE : 'Europe/Berlin';
if (typeof process.env.LOGGER_LEVEL === 'undefined') {
process.env.LOGGER_LEVEL = '3';
}
// @todo: it is really ugly and wrong to log with hardcoded timezone before environment is read
process.env.RS_TIMEZONE = process.env.RS_TIMEZONE || 'Europe/Berlin';
process.env.RS_LOGGER_LEVEL = process.env.RS_LOGGER_LEVEL || 3;
/**
* Class for logger
@@ -27,7 +28,7 @@ class Logger {
* @returns {boolean}
*/
static isMuted () {
return !!process.env.LOGGER_MUTED;
return process.env.RS_LOGGER_LEVEL === LEVEL_MUTE;
}
/**
@@ -35,6 +36,7 @@ class Logger {
* @param {string} context context of the log message (classname.methodname)
*/
constructor (context) {
process.env.RS_LOGGER_LEVEL = process.env.RS_LOGGER_LEVEL || LEVEL_INFO;
this.context = context;
}
@@ -45,76 +47,100 @@ class Logger {
* @param {string} type
*/
stdout (message, context, type) {
var time = moment().tz(process.env.RS_TIMEZONE).format('DD-MM-YYYY HH:mm:ss.SSS');
var loggerContext = `${String(context)}`;
if (Logger.isMuted()) {
return;
}
if (context === false) {
context = '';
if (context) {
process.stdout.write(`[${time}] [${type}] ${message} [${loggerContext}]\n`);
} else {
context = '(' + context + ')';
process.stdout.write(`[${time}] [${type}] ${message}\n`);
}
var str = '[' + (moment().tz(process.env.TIMEZONE).format('DD-MM-YYYY HH:mm:ss.SSS')) + '] [' + type + '] ' + message + ' ' + context;
console.log(str);
}
/**
* print an info message if LOG_LEVEL >= LEVEL_INFO
* @param {string} message
* @param {string} context
* @param {string=} context
* @param {boolean=} alertGui
*/
info (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') {
context = this.context;
loggerContext = this.context;
}
if (typeof alertGui === 'undefined') {
alertGui = false;
loggerAlertGui = false;
}
if (process.env.LOGGER_LEVEL >= LEVEL_INFO) {
return this.stdout(message, context, 'INFO');
if (process.env.RS_LOGGER_LEVEL >= LEVEL_INFO) {
return this.stdout(message, loggerContext, 'INFO');
}
if (alertGui) {
// todo: if alertGui is activated on frontend and websocketcontroller, insert emit here
// todo: if alertGui is activated on frontend and websockets controller, insert emit here
if (loggerAlertGui) {
return;
}
}
/**
* print a warning message if LOG_LEVEL >= LEVEL_WARN
* @param {string} message
* @param {string} context
* @param {string=} context
* @param {boolean=} alertGui
*/
warn (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') {
context = this.context;
loggerContext = this.context;
}
if (typeof alertGui === 'undefined') {
alertGui = false;
loggerAlertGui = false;
}
if (process.env.LOGGER_LEVEL >= LEVEL_WARN) {
return this.stdout(message, context, 'WARN');
if (process.env.RS_LOGGER_LEVEL >= LEVEL_WARN) {
return this.stdout(message, loggerContext, 'WARN');
}
if (alertGui) {
// todo: if alertGui is activated on frontend and websocketcontroller, insert emit here
// todo: if alertGui is activated on frontend and websockets controller, insert emit here
if (loggerAlertGui) {
return;
}
}
/**
* print a debug message if LOG_LEVEL >= LEVEL_DEBUG
* @param {string} message
* @param {string} context
* @param {string=} context
* @param {boolean=} alertGui
*/
debug (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') {
context = this.context;
loggerContext = this.context;
}
if (typeof alertGui === 'undefined') {
alertGui = false;
loggerAlertGui = false;
}
if (process.env.LOGGER_LEVEL >= LEVEL_DEBUG) {
return this.stdout(message, context, 'DEBUG');
if (process.env.RS_LOGGER_LEVEL >= LEVEL_DEBUG) {
return this.stdout(message, loggerContext, 'DEBUG');
}
if (alertGui) {
// todo: if alertGui is activated on frontend and websocketcontroller, insert emit here
// todo: if alertGui is activated on frontend and websockets controller, insert emit here
if (loggerAlertGui) {
return;
}
}
@@ -122,30 +148,38 @@ class Logger {
* print a debug message if LOG_LEVEL >= LEVEL_ERROR
* sends a string to
* @param {string} message
* @param {string} context
* @param {string=} context
* @param {boolean=} alertGui
*/
error (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') {
context = this.context;
loggerContext = this.context;
}
if (typeof alertGui === 'undefined') {
alertGui = false;
loggerAlertGui = false;
}
if (process.env.LOGGER_LEVEL >= LEVEL_ERROR) {
return this.stdout(message, context, 'ERROR');
if (process.env.RS_LOGGER_LEVEL >= LEVEL_ERROR) {
return this.stdout(message, loggerContext, 'ERROR');
}
if (alertGui) {
// todo: if alertGui is activated on frontend and websocketcontroller, insert emit here
// todo: if alertGui is activated on frontend and websockets controller, insert emit here
if (loggerAlertGui) {
return;
}
}
}
// define loglevels in logger class
// define log levels in logger class
Logger.LEVEL_ERROR = LEVEL_ERROR;
Logger.LEVEL_WARN = LEVEL_WARN;
Logger.LEVEL_INFO = LEVEL_INFO;
Logger.LEVEL_DEBUG = LEVEL_DEBUG;
module.exports = (context)=>{
module.exports = (context) => {
return new Logger(context);
};

View File

@@ -4,6 +4,7 @@
* @copyright 2016 datarhei.org
* @license Apache-2.0
*/
'use strict';
const Q = require('q');
const psFind = require('ps-find');
@@ -11,7 +12,7 @@ const spawn = require('child_process').spawn;
const logger = require('./Logger')('Nginxrtmp');
/**
* class to watch and controll the nginx rtmpserver process
* class to watch and control the NGINX RTMP server process
*/
class Nginxrtmp {
@@ -30,24 +31,19 @@ class Nginxrtmp {
* @returns {string}
*/
init () {
let promise = null;
promise = this.getState()
.then((state)=>{
switch(state) {
case 'not_running':
this.start();
break;
case 'running':
this.logger.info('NGINX allready started...');
break;
default:
throw new Error('state could not be detected');
}
});
return promise;
return this.getState()
.then((state) => {
switch (state) {
case 'not_running':
this.start();
break;
case 'running':
this.logger.info('NGINX already started');
break;
default:
throw new Error('NGINX state could not be detected');
}
});
}
/**
@@ -66,16 +62,16 @@ class Nginxrtmp {
.then((state)=> {
switch (state) {
case 'not_running':
this.logger.info('NGINX could not be started');
break;
case 'not_running':
this.logger.info('NGINX could not be started');
break;
case 'running':
this.logger.info('NGINX successfully started');
break;
case 'running':
this.logger.info('NGINX successfully started');
break;
default:
throw new Error('NGINX state could not be detected');
default:
throw new Error('NGINX state could not be detected');
}
});
}
@@ -86,15 +82,15 @@ class Nginxrtmp {
*/
bindNginxProcessEvents (process) {
process.stdout.on('data', (data) => {
this.logger.info(`The NGINX rtmp process created an output: ${data}`);
this.logger.info(`The NGINX RTMP process created an output: ${data}`);
});
process.stderr.on('data', (data) => {
this.logger.error(`The NGINX rtmp process created an error output: ${data}`);
this.logger.error(`The NGINX RTMP process created an error output: ${data}`);
});
process.stderr.on('close', (code) => {
this.logger.error(`The NGINX rtmp process closed with code: ${code}`);
this.logger.error(`The NGINX RTMP process exited with code: ${code}`);
this.start();
});
}
@@ -110,26 +106,23 @@ class Nginxrtmp {
let deferred = Q.defer();
// delay the state detection if waiting for process is needed
Q
.delay(delay)
.then(()=> {
return Q.nfcall(psFind.find, nginxProcessString);
})
.then(()=>{
state = 'running';
})
// ps-find throws exception in case of 'not found' so we have to handle that
.catch(()=>{
state = 'not_running';
})
.finally(()=>{
deferred.resolve(state);
});
Q.delay(delay)
.then(()=> {
return Q.nfcall(psFind.find, nginxProcessString);
})
.then(()=> {
state = 'running';
})
.catch(()=> { // ps-find throws exception in case of 'not found' so we have to handle that
state = 'not_running';
})
.finally(()=> {
deferred.resolve(state);
});
return deferred.promise;
}
}
module.exports = (config)=>{
module.exports = (config)=> {
return new Nginxrtmp(config);
};

View File

@@ -4,12 +4,16 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const config = require('../../conf/live.json');
const path = require('path');
const config = require(path.join(global.__base, 'conf', 'live.json'));
const logger = require('./Logger')('Restreamer');
const WebsocketsController = require('./WebsocketController');
const WebsocketsController = require('./WebsocketsController');
const FfmpegCommand = require('fluent-ffmpeg');
const Q = require('q');
const app = require.main.require('./webserver/app').app;
const JsonDB = require('node-json-db');
/**
* class Restreamer creates and manages streams through ffmpeg
@@ -31,9 +35,7 @@ class Restreamer {
* @returns {string}
*/
static generateSnapshotPath () {
const path = require('path');
return path.join(__dirname, '..', 'webserver', 'public', 'images', 'live.jpg');
return path.join(global.__public, 'images', 'live.jpg');
}
/**
@@ -41,17 +43,18 @@ class Restreamer {
* @param {boolean} firstSnapshot
*/
static fetchSnapshot (firstSnapshot) {
var command = null;
if (Restreamer.data.states.repeatToLocalNginx.type === 'connected' || firstSnapshot) {
var command = new FfmpegCommand(Restreamer.generateOutputHLSPath());
command = new FfmpegCommand(Restreamer.generateOutputHLSPath());
command.output(Restreamer.generateSnapshotPath());
command.outputOption(config.ffmpeg.options.snapshot);
command.on('error', (error)=> {
logger.error('Error on fetching snapshot: ' + error.toString());
});
command.on('end', () =>{
command.on('end', () => {
logger.info('updated snapshot');
Q.delay(process.env.SNAPSHOT_REFRESH_INTERVAL).then(function () {
Q.delay(this.calculateSnapshotRefreshInterval()).then(() => {
Restreamer.fetchSnapshot(false);
});
});
@@ -59,6 +62,21 @@ class Restreamer {
}
}
static calculateSnapshotRefreshInterval () {
let fallbackRefreshInterval = 60000;
let snapshotRefreshInterval = process.env.RS_SNAPSHOT_REFRESH_INTERVAL.match(/([0-9]+)([a-z]{1,2})?/);
if (typeof snapshotRefreshInterval[2] === 'undefined' && snapshotRefreshInterval[1] > 30000) {
return snapshotRefreshInterval[1];
} else if (snapshotRefreshInterval[2] === 'm') {
return snapshotRefreshInterval[1] * 1000 * 60;
} else if (snapshotRefreshInterval[2] === 's' && snapshotRefreshInterval[1] > 30) {
return snapshotRefreshInterval[1] * 1000;
}
return fallbackRefreshInterval;
}
/**
* stop stream
* @param {string} processName
@@ -71,47 +89,54 @@ class Restreamer {
if (processHasBeenSpawned) {
Restreamer.data.processes[processName].kill();
Restreamer.data.processes[processName] = {
state: 'not_connected'
'state': 'not_connected'
};
}
}
/**
* restore the ffmpeg processes from jsondb (called on app start to restore ffmpeg processes
* after the applicatoin has been killed or stuff
* after the application has been killed or stuff
*/
static restoreFFMpegProcesses () {
var JsonDB = require('node-json-db');
var db = new JsonDB(config.jsondb, true, false);
var repeatToLocalNginxIsConnected = false;
var repeatToLocalNginxIsConnecting = false;
var repeatToOptionalOutputIsConnected = false;
var repeatToOptionalOutputIsConnecting = false;
try {
Restreamer.data.addresses = db.getData('/addresses');
Restreamer.data.states = db.getData('/states');
Restreamer.data.userActions = db.getData('/userActions');
Restreamer.data.addresses = db.getData('/addresses');
Restreamer.data.states = db.getData('/states');
Restreamer.data.options = db.getData('/options');
Restreamer.data.userActions = db.getData('/userActions');
// check if the srcAddress has been repeated to Local Nginx
var repeatToLocalNginxIsConnected = Restreamer.data.states.repeatToLocalNginx.type === 'connected';
var repeatToLocalNginxIsConnecting = Restreamer.data.states.repeatToLocalNginx.type === 'connecting';
var repeatToOptionalOutputIsConnected = Restreamer.data.states.repeatToOptionalOutput.type === 'connected';
var repeatToOptionalOutputIsConnecting = Restreamer.data.states.repeatToOptionalOutput.type === 'connecting';
repeatToLocalNginxIsConnected = Restreamer.data.states.repeatToLocalNginx.type === 'connected';
repeatToLocalNginxIsConnecting = Restreamer.data.states.repeatToLocalNginx.type === 'connecting';
repeatToOptionalOutputIsConnected = Restreamer.data.states.repeatToOptionalOutput.type === 'connected';
repeatToOptionalOutputIsConnecting = Restreamer.data.states.repeatToOptionalOutput.type === 'connecting';
if (Restreamer.data.addresses.srcAddress && (!!repeatToLocalNginxIsConnected || !!repeatToLocalNginxIsConnecting)) {
Restreamer.startStream(Restreamer.data.addresses.srcAddress, 'repeatToLocalNginx');
}
if (Restreamer.data.addresses.optionalOutputAddress && (!!repeatToOptionalOutputIsConnected || !!repeatToOptionalOutputIsConnecting)) {
Restreamer.startStream(Restreamer.data.addresses.srcAddress, 'repeatToOptionalOutput', Restreamer.data.addresses.optionalOutputAddress);
}
// check if the srcAddress has been repeated to Local Nginx
if (Restreamer.data.addresses.srcAddress &&
(repeatToLocalNginxIsConnected || repeatToLocalNginxIsConnecting)) {
Restreamer.startStream(
Restreamer.data.addresses.srcAddress,
'repeatToLocalNginx'
);
}
catch(error) {
logger.error('error restoring ffmpeg process: ' + error);
if (Restreamer.data.addresses.optionalOutputAddress &&
(repeatToOptionalOutputIsConnected || repeatToOptionalOutputIsConnecting)) {
Restreamer.startStream(
Restreamer.data.addresses.srcAddress,
'repeatToOptionalOutput',
Restreamer.data.addresses.optionalOutputAddress
);
}
}
/**
* write JSON file for persistency
* write JSON file for persistence
*/
static writeToDB () {
var JsonDB = require('node-json-db');
var db = new JsonDB(config.jsondb, true, false);
db.push('/', Restreamer.dataForJsonDb());
@@ -135,39 +160,44 @@ class Restreamer {
* add output to ffmpeg command
* @param {FfmpegCommand} ffmpegCommand
* @param {string} outputAddress
* @return {promise}
* @return {Promise}
*/
static addOutput (ffmpegCommand, outputAddress) {
ffmpegCommand.output(outputAddress);
return Restreamer.appendOutputOptionFromConfig(ffmpegCommand);
}
static applyOptions (ffmpegCommand) {
if (Restreamer.data.options.rtspTcp && Restreamer.data.addresses.srcAddress.indexOf('rtsp') === 0) {
ffmpegCommand.inputOptions('-rtsp_transport tcp');
}
}
/**
* append the ffmpeg options of the config file to an output
* @param {FfmpegCommand} ffmpegCommand
* @return {promise}
* @return {Promise}
*/
static appendOutputOptionFromConfig (ffmpegCommand) {
var deferred = Q.defer();
var ffmpegOptions = [];
ffmpegCommand.ffprobe(function (err, data) {
ffmpegCommand.ffprobe((err, data) => {
if (err) {
return deferred.reject(err);
} else {
var ffmpegOptions;
if (data.streams.length > 1) {
ffmpegOptions = config.ffmpeg.options.native_h264;
logger.debug('Selected ffmpeg.option: native_h264');
} else {
ffmpegOptions = config.ffmpeg.options.native_h264_soundless_aac;
logger.debug('Selected ffmpeg.option: native_h264_soundless_aac');
}
for (let option of ffmpegOptions) {
ffmpegCommand.outputOption(option);
}
return deferred.resolve();
}
if (data.streams.length > 1) {
ffmpegOptions = config.ffmpeg.options.native_h264;
logger.debug('Selected ffmpeg.option: native_h264');
} else {
ffmpegOptions = config.ffmpeg.options.native_h264_soundless_aac;
logger.debug('Selected ffmpeg.option: native_h264_soundless_aac');
}
for (let option of ffmpegOptions) {
ffmpegCommand.outputOption(option);
}
return deferred.resolve();
});
return deferred.promise;
}
@@ -176,14 +206,14 @@ class Restreamer {
* update the state of the stream
* @param {string} processName
* @param {string} state
* @param {string} message
* @param {string=} message
* @return {string} name of the new state
*/
static updateState (processName, state, message) {
logger.debug(`Update state of "${processName}" from state "${Restreamer.data.states[processName].type}" to state "${state}"`);
Restreamer.data.states[processName] = {
type: state,
message: message
'type': state,
'message': message
};
Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui();
@@ -191,38 +221,48 @@ class Restreamer {
}
/**
* update the last submitted useraction (like click on stop stream, click on start stream)
* update the last submitted user action (like click on stop stream, click on start stream)
* @param {string} processName
* @param {string} action useraction
* @param {string} action user action
* @return {string} name of the new user action
*/
static updateUserAction (processName, action) {
logger.debug(`Set useraction of "${processName}" from "${Restreamer.data.userActions[processName]}" to "${action}"`);
logger.debug(`Set user action of "${processName}" from "${Restreamer.data.userActions[processName]}" to "${action}"`);
Restreamer.data.userActions[processName] = action;
Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui();
return action;
}
/**
* update options
* @param {Object} options
*/
static updateOptions (options) {
Restreamer.data.options = options;
Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui();
}
/**
*
* @param {string} src src-address of the ffmpeg stream
* @param {string} streamType repeatToOptionalOutput or repeatToLocalNginx
* @param {string} optionalOutput address of the optional output
* @param {string} retryCounter current value of the retry counter (startStream retries automatically if anything fails)
* @param {string=} optionalOutput address of the optional output
* @param {string=} retryCounter current value of the retry counter (startStream retries automatically if anything fails)
*/
static startStream (src, streamType, optionalOutput, retryCounter) {
/** @var {FfmpegCommand} instance of FFmpeg command of module fluent-ffmpeg */
var command = null;
/** @var {Promise} Promise to make sure, the add output process has been finished */
var addOutputPromise = null;
logger.info(`Start stream "${streamType}"`);
// update the retry counter for the streamType
Restreamer.data.retryCounter[streamType].current = typeof retryCounter === 'undefined' ? 0 : retryCounter;
/** @var {FfmpegCommand} instance of FFmpeg command of module fluent-ffmpeg*/
var command;
/** @var {Promise} Promise to make sure, the add output process has been finished */
var addOutputPromise;
/** @var {Boolean} */
const repeatToLocalNginx = streamType === 'repeatToLocalNginx';
@@ -243,50 +283,70 @@ class Restreamer {
// repeat to local nginx server
if (repeatToLocalNginx) {
command = new FfmpegCommand(src, {
outputLineLimit: 1
'outputLineLimit': 1
});
// add outputs to the ffmpeg stream
addOutputPromise = Restreamer.addOutput(command, Restreamer.generateOutputHLSPath())
.catch(function (error) {
addOutputPromise = Restreamer.addOutput(command, Restreamer.generateOutputHLSPath()).catch((error) => {
logger.error(`Error adding one or more outputs: ${error.toString}`);
});
// repeat to optional output
// repeat to optional output
} else if (repeatToOptionalOutput) {
command = new FfmpegCommand(Restreamer.generateOutputHLSPath(), {
outputLineLimit: 1
'outputLineLimit': 1
});
Restreamer.data.addresses.optionalOutputAddress = optionalOutput;
addOutputPromise = Restreamer.addOutput(command, optionalOutput);
}
// after adding outputs, define events on the new FFmpeg stream
addOutputPromise.then(function () {
addOutputPromise.then(() => {
var progressMethod = (progress) => {
if (Restreamer.data.states[streamType].type === 'connecting') {
Restreamer.data.retryCounter[streamType].current = 1;
Restreamer.updateState(streamType, 'connected');
}
Restreamer.data.progresses[streamType] = progress;
Restreamer.updateProgressOnGui();
command.removeAllListeners('progress');
};
Restreamer.applyOptions(command);
command
/*
* STREAMING STARTED
*/
.on('start', function (commandLine) {
// stream started
.on('start', (commandLine) => {
if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping on "start" event of FFmpeg command since "stopped" has been clicked');
return;
} else {
logger.debug(`FFmpeg spawned: ${commandLine}`);
Restreamer.data.processes[streamType] = command;
}
logger.debug(`FFmpeg spawned: ${commandLine}`);
Restreamer.data.processes[streamType] = command;
// fetch snapshot only, if repeated to local nginx
if (repeatToLocalNginx) {
Restreamer.fetchSnapshot(true);
}
// fetch snapshot only, if repeated to local nginx
if (repeatToLocalNginx) {
Restreamer.fetchSnapshot(true);
}
})
/*
* ERROR HANDLER
*/
.on('error', (error)=>{
// stream ended
.on('end', ()=> {
Restreamer.updateState(streamType, 'disconnected');
Restreamer.data.retryCounter[streamType].current++;
if (Restreamer.data.retryCounter[streamType].current <= config.ffmpeg.monitor.retries) {
if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping retry since "stopped" has been clicked');
return;
}
logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(() => {
logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`);
Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current);
});
}
})
// stream error handler
.on('error', (error) => {
if (error.toString().indexOf('SIGKILL') > -1) {
Restreamer.updateState(streamType, 'disconnected');
logger.info(`FFmpeg streaming stopped for "${streamType}"`);
@@ -296,79 +356,47 @@ class Restreamer {
if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping retry since "stopped" has been clicked');
return;
} else {
Restreamer.updateState(streamType, 'error', error.toString());
logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait} ms`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(function () {
logger.info(`Retry FFmpeg connection to "${src}" retrycounter: ${Restreamer.data.retryCounter[streamType].current}`);
Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current);
});
}
}
}
})
/*
STREAMING ENDED
*/
.on('end', ()=>{
Restreamer.updateState(streamType, 'disconnected');
Restreamer.data.retryCounter[streamType].current++;
if (Restreamer.data.retryCounter[streamType].current <= config.ffmpeg.monitor.retries) {
if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping retry since "stopped" has been clicked');
return;
} else {
Restreamer.updateState(streamType, 'error', error.toString());
logger.error(`Error on stream ${streamType}: ${error.toString()}`);
logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(function () {
logger.info(`Retry FFmpeg connection to "${src}" retrycounter: ${Restreamer.data.retryCounter[streamType].current}`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(() => {
logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`);
Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current);
});
} else {
Restreamer.updateState(streamType, 'error', error.toString());
}
}
});
/*
* PROGRESS
*/
var progressMethod = function (progress) {
if (Restreamer.data.states[streamType].type === 'connecting') {
Restreamer.updateState(streamType, 'connected');
}
Restreamer.data.progresses[streamType] = progress;
Restreamer.updateProgressOnGui();
command.removeAllListeners('progress');
};
command.on('progress', progressMethod);
setInterval(()=>{
setInterval(()=> {
if (command.listeners('progress').length === 0) {
command.on('progress', progressMethod);
}
}, 1000);
command.exec();
}).catch(function (error) {
}).catch((error) => {
logger.error(`Error starting FFmpeg command: ${error.toString()}`);
});
}
/**
* binded on app-start to bind websocketevents of Restreamer
* bind websocket events on application start
*/
static bindWebsocketEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', function (socket) {
WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => {
socket.on('startStream', (options)=> {
Restreamer.updateUserAction(options.streamType, 'start');
Restreamer.updateOptions(options.options);
Restreamer.startStream(options.src, options.streamType, options.optionalOutput);
});
socket.on('stopStream', (streamType)=>{
socket.on('stopStream', (streamType)=> {
Restreamer.updateUserAction(streamType, 'stop');
Restreamer.stopStream(streamType);
});
socket.on('checkForAppUpdates', ()=>{
const app = require('../webserver/app');
socket.on('checkForAppUpdates', ()=> {
socket.emit('checkForAppUpdatesResult', app.get('updateAvailable'));
});
socket.on('checkStates', Restreamer.updateStreamDataOnGui);
@@ -381,12 +409,13 @@ class Restreamer {
* @returns {object}
*/
static extractDataOfStreams () {
var sData = {};
sData.userActions = Restreamer.data.userActions;
sData.addresses = Restreamer.data.addresses;
sData.states = Restreamer.data.states;
sData.retryCounter = Restreamer.data.retryCounter;
return sData;
return {
'addresses': Restreamer.data.addresses,
'options': Restreamer.data.options,
'userActions': Restreamer.data.userActions,
'states': Restreamer.data.states,
'retryCounter': Restreamer.data.retryCounter
};
}
/**
@@ -394,61 +423,69 @@ class Restreamer {
* @return {object}
*/
static dataForJsonDb () {
var dbData = {};
dbData.addresses = Restreamer.data.addresses;
dbData.userActions = Restreamer.data.userActions;
dbData.states = Restreamer.data.states;
return dbData;
return {
'addresses': Restreamer.data.addresses,
'options': Restreamer.data.options,
'userActions': Restreamer.data.userActions,
'states': Restreamer.data.states
};
}
}
/*
define data structure of Restreamer Data
define data structure of Restreamer Data
*/
Restreamer.data = {
retryCounter: {
repeatToLocalNginx: {
current: 0,
max: config.ffmpeg.monitor.retries
'retryCounter': {
'repeatToLocalNginx': {
'current': 0,
'max': config.ffmpeg.monitor.retries
},
repeatToOptionalOutput: {
current: 0,
max: config.ffmpeg.monitor.retries
'repeatToOptionalOutput': {
'current': 0,
'max': config.ffmpeg.monitor.retries
}
},
states: {
repeatToLocalNginx: {
type: 'disconnected',
message: ''
'options': {
'rtspTcp': false
},
'states': {
'repeatToLocalNginx': {
'type': 'disconnected',
'message': ''
},
repeatToOptionalOutput: {
type: 'disconnected',
message: ''
'repeatToOptionalOutput': {
'type': 'disconnected',
'message': ''
}
},
userActions: {
repeatToLocalNginx: 'start',
repeatToOptionalOutput: 'start'
'userActions': {
'repeatToLocalNginx': 'start',
'repeatToOptionalOutput': 'start'
},
processes: {
'processes': {
// overwritten with ffmpeg process if stream has been started
repeatToLocalNginx: {
state: 'not_connected'
'repeatToLocalNginx': {
'state': 'not_connected'
},
// overwritten with ffmpeg process if stream has been started
repeatToOptionalOutput: {
state: 'not_connected'
'repeatToOptionalOutput': {
'state': 'not_connected'
}
},
progresses: {
'progresses': {
// overwritten with ffmpeg process if stream has been started
repeatToLocalNginx: {},
'repeatToLocalNginx': {},
// overwritten with ffmpeg process if stream has been started
repeatToOptionalOutput: {}
'repeatToOptionalOutput': {}
},
addresses: {
srcAddress: '',
optionalOutputAddress: ''
'addresses': {
'srcAddress': '',
'optionalOutputAddress': ''
}
};

View File

@@ -0,0 +1,85 @@
/**
* @file holds the code for the class EnvVar
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const fs = require('fs');
const Q = require('q');
const path = require('path');
const Validator = require('jsonschema').Validator;
const logger = require('./Logger')('RestreamerData');
const dbPath = path.join(global.__base, 'db');
const dbFile = 'v1.json';
const confPath = path.join(global.__base, 'conf');
const schemaFile = 'jsondb_v1_schema.json';
class RestreamerData {
static checkJSONDb () {
var schemadata = {};
var dbdata = {};
var deferred = Q.defer();
var readSchema = Q.nfcall(fs.readFile, path.join(confPath, schemaFile));
var readDBFile = Q.nfcall(fs.readFile, path.join(dbPath, dbFile));
logger.info('Checking jsondb file...');
readSchema
.then((s) => {
schemadata = JSON.parse(s.toString('utf8'));
return readDBFile;
})
.then((d) => {
dbdata = JSON.parse(d.toString('utf8'));
let v = new Validator();
let instance = dbdata;
let schema = schemadata;
let validateResult = v.validate(instance, schema);
if (validateResult.errors.length > 0) {
logger.debug(`Validation error of v1.db: ${JSON.stringify(validateResult.errors)}`);
throw new Error(JSON.stringify(validateResult.errors));
} else {
logger.debug('"v1.db" is valid');
deferred.resolve();
}
})
.catch((error) => {
var defaultStructure = {
'addresses': {
'srcAddress': '',
'optionalOutputAddress': ''
},
'options': {
'rtspTcp': true
},
'states': {
'repeatToLocalNginx': {
'type': 'stopped'
},
'repeatToOptionalOutput': {
'type': 'stopped'
}
},
'userActions': {
'repeatToLocalNginx': 'stop',
'repeatToOptionalOutput': 'stop'
}
};
logger.debug(`Error reading "v1.db": ${error.toString()}`);
if (!fs.existsSync(dbPath)) {
fs.mkdirSync(dbPath);
}
fs.writeFileSync(path.join(dbPath, dbFile), JSON.stringify(defaultStructure));
deferred.resolve();
});
return deferred.promise;
}
}
module.exports = RestreamerData;

View File

@@ -4,13 +4,16 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const logger = require('../classes/Logger')('WebsocketsController');
const logger = require.main.require('./classes/Logger')('WebsocketsController');
const app = require.main.require('./webserver/app').app;
const packageJson = require(require('path').join(global.__base, 'package.json'));
/**
* static class websocket controller, that helps communicating through websockets to different namespaces and ensures
* that websocket events are binded, if the websocket server has been initialized (through promise made on app start)
* @todo since currently the restream is a singlepage application, there is no need to use different namespaces
* that websocket events are bound, if the websocket server has been initialized (through promise made on app start)
* @todo since currently the Restreamer is a single page application, there is no need to use different namespaces
*/
class WebsocketsController {
@@ -21,10 +24,8 @@ class WebsocketsController {
* @param {object} data data to emit to the client event listener
*/
static emitToNamespace (namespace, event, data) {
var app = require('../webserver/app');
app.get('websocketsReady').promise.then(function (io) {
logger.debug('websocket got event ' + event + ' to namespace ' + namespace + '', 'Websockets');
app.get('websocketsReady').promise.then((io) => {
logger.debug(`websocket got event ${event} to namespace ${namespace}`, 'Websockets');
io.of(namespace).emit(event, data);
});
}
@@ -35,29 +36,23 @@ class WebsocketsController {
* @param {function} callback
*/
static addOnConnectionEventToNamespace (namespace, callback) {
var app = require('../webserver/app');
app.get('websocketsReady').promise.then(function (io) {
app.get('websocketsReady').promise.then((io) => {
var nsp = io.of(namespace);
nsp.on('connection', function (socket) {
nsp.on('connection', (socket) => {
callback(socket);
});
});
}
/**
* bind default events of all classes that are using websocketevents
* bind default events of all classes that are using websockets events
*/
static bindDefaultEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', function (socket) {
socket.on('getVersion', (options)=> {
var packageJson = require('../../package.json');
WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => {
socket.on('getVersion', () => {
socket.emit('version', packageJson.version);
});
var app = require('../webserver/app');
socket.emit('publicIp', app.get('publicIp'));
});
require('./Restreamer').bindWebsocketEvents();

View File

@@ -1,23 +1,28 @@
/**
* @file this file is loaded on application start and inits the application
* @file this file is loaded on application start and initializes the application
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
const logger = require('./classes/Logger')('start');
const EnvVar = require('./classes/EnvVar');
const packageJson = require('../package.json');
const app = require('./webserver/app');
const config = require('../conf/live.json');
const nginxrtmp = require('./classes/Nginxrtmp')(config);
const Q = require('q');
const Validator = require('jsonschema').Validator;
const fs = require('fs');
'use strict';
const path = require('path');
/*
init simple_streamer with environments
*/
global.__src = __dirname;
global.__base = path.join(__dirname, '..');
global.__public = path.join(__dirname, 'webserver', 'public');
const logger = require('./classes/Logger')('start');
const EnvVar = require('./classes/EnvVar');
const packageJson = require(path.join('..', 'package.json'));
const config = require(path.join(global.__base, 'conf', 'live.json'));
const nginxrtmp = require('./classes/Nginxrtmp')(config);
const Q = require('q');
const Restreamer = require('./classes/Restreamer');
const RestreamerData = require('./classes/RestreamerData');
const WebsocketsController = require('./classes/WebsocketsController');
const restreamerApp = require('./webserver/app');
// show start message
logger.info(' _ _ _ _ ', false);
logger.info(' __| | __ _| |_ __ _ _ __| |___ ___(_)', false);
logger.info(' / _ |/ _ | __/ _ | __| _ |/ _ | |', false);
@@ -27,167 +32,35 @@ logger.info('', false);
logger.info('Restreamer v' + packageJson.version, false);
logger.info('', false);
logger.info('ENVIRONMENTS', false);
logger.info('More informations in our Docs', false);
logger.info('More information in our Docs', false);
logger.info('', false);
// define environment variables
var env_vars = [];
// setup environment vars
EnvVar.init(config);
env_vars.push(new EnvVar('NODEJS_PORT', false, 3000, 'Webserver port of application'));
env_vars.push(new EnvVar('LOGGER_LEVEL', true, '3', 'Logger level to defined, what should be logged'));
env_vars.push(new EnvVar('TIMEZONE', true, 'Europe/Berlin', 'Set the timezone'));
env_vars.push(new EnvVar('SNAPSHOT_REFRESH_INTERVAL', false, 60000, 'Interval to create a new Snapshot'));
env_vars.push(new EnvVar('CREATE_HEAPDUMPS', false, 'false', 'Create Heapdumps of application'));
// check for app updates
restreamerApp.checkForRestreamerUpdates();
// Check for updates each 12 hours
setInterval(restreamerApp.checkForRestreamerUpdates, 12 * 3600 * 1000);
// manage all environments
var killProcess = false;
for (let e of env_vars) {
if (typeof process.env[e.name] !== 'undefined') {
logger.info(`ENV "${e.name}=${process.env[e.name]}"`, e.description);
} else if (e.required === true) {
logger.error(`No value set for env "${e.name}", but it is required`);
killProcess = true;
} else {
process.env[e.name] = e.defaultValue;
logger.info(`ENV "${e.name}=${process.env[e.name]}", set to default-value!`, e.description);
}
}
// kill process after a short delay to log the error message to console
if (killProcess === true) {
setTimeout(()=> {
process.exit();
}, 500);
}
logger.info('', false);
/**
* check if the data from jsondb is valid against the Restreamer jsondb schema
* @returns {promise}
*/
const checkJsonDb = function () {
logger.info('Checking jsondb file...');
var schemadata;
var dbdata;
var deferred = Q.defer();
var readSchema = Q.nfcall(fs.readFile, path.join(__dirname, '../', 'conf', 'jsondb_v1_schema.json'));
var readDBFile = Q.nfcall(fs.readFile, path.join(__dirname, '../', 'db', 'v1.json'));
readSchema
.then((s)=> {
schemadata = JSON.parse(s.toString('utf8'));
return readDBFile;
})
.then((d)=> {
dbdata = JSON.parse(d.toString('utf8'));
let v = new Validator();
let instance = dbdata;
let schema = schemadata;
let validateResult = v.validate(instance, schema);
if (validateResult.errors.length > 0) {
logger.debug(`Validation error of v1.db: ${JSON.stringify(validateResult.errors)}`);
throw new Error(JSON.stringify(validateResult.errors));
} else {
logger.debug('"v1.db" is valid');
deferred.resolve();
}
}).catch(function (error) {
logger.debug(`Error reading "v1.db": ${error.toString()}`);
var defaultStructure = {
addresses: {
srcAddress: '',
optionalOutputAddress: ''
},
states: {
repeatToLocalNginx: {
type: 'stopped'
},
repeatToOptionalOutput: {
type: 'stopped'
}
},
userActions: {
repeatToLocalNginx: 'stop',
repeatToOptionalOutput: 'stop'
}
};
fs.writeFileSync(path.join(__dirname, '../', 'db', 'v1.json'), JSON.stringify(defaultStructure));
deferred.resolve();
});
return deferred.promise;
};
/**
* start the express webserver
* @returns {promise}
*/
const startWebserver = function startWebserver () {
var deferred = Q.defer();
logger.info('Starting webserver...');
app.set('port', process.env.NODEJS_PORT);
var server = app.listen(app.get('port'), function () {
require('./classes/WebsocketController').bindDefaultEvents();
app.set('io', require('socket.io')(server));
app.set('server', server.address());
// promise to determine if the webserver has been started to avoid ws binding before
app.get('websocketsReady').resolve(app.get('io'));
logger.info(`Webserver running on port ${process.env.NODEJS_PORT}`, 'start.webserver');
deferred.resolve(server.address().port);
});
return deferred.promise;
};
/**
* get the public ip of the server, on that the Restreamer is running
*/
const getPublicIp = function () {
logger.info('Getting public ip...', 'start.publicip');
var exec = require('child_process').exec;
exec('public-ip', (err, stdout, stderr)=> {
if (err) {
logger.error(err);
}
app.set('publicIp', stdout.split('\n')[0]);
});
};
/**
* Restores FFmpeg processes, that have been spawned i.E. on the last app-start (stored on jsondb)
* @returns {promise}
*/
const restoreFFMPEGProcesses = function () {
var deferred = Q.defer();
const Restreamer = require('./classes/Restreamer');
logger.info('Restoring FFmpeg processes...', 'start.restore');
Restreamer.restoreFFMpegProcesses();
deferred.resolve();
return deferred.promise;
};
/**
* let's do it
*/
// add default websocket events, @todo this will be removed, when the new websocket workflow is implemented
WebsocketsController.bindDefaultEvents();
// start the app
nginxrtmp.init()
.then(checkJsonDb)
.then(startWebserver)
.then(checkJsonDb)
.then(restoreFFMPEGProcesses)
.then(getPublicIp)
.catch(function (error) {
logger.error(`Error starting webserver and nginx for application: ${error}`);
setTimeout(()=> {
process.exit();
}, 500);
});
.then(()=> {
return RestreamerData.checkJSONDb();
})
.then(()=> {
return restreamerApp.startWebserver();
})
.then(() => {
return Q.fcall(Restreamer.restoreFFMpegProcesses);
})
.then(()=> {
restreamerApp.getPublicIp();
})
.catch((error)=> {
let errorMessage = `Error starting webserver and nginx for application: ${error}`;
throw new Error(errorMessage);
});

View File

@@ -3,13 +3,11 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
/*
* libraries
*/
// auth stuff
const passport = require('passport');
const flash = require('connect-flash');
const passportConfig = require('./config/passport');
// express
const express = require('express');
@@ -23,101 +21,238 @@ const https = require('https');
const path = require('path');
const Q = require('q');
const crypto = require('crypto');
const exec = require('child_process').exec;
/*
* modules
// modules
const packageJson = require(path.join(global.__base, 'package.json'));
const logger = require.main.require('./classes/Logger')('RestreamerExpressApp');
const indexRouter = require('./controllers/index');
const apiV1 = require('./controllers/api/v1');
// middleware
const expressLogger = require('./middleware/expressLogger');
/**
* Class for the ReStreamer webserver, powered by express.js
*/
const packageJson = require('../../package.json');
const logger = require('../classes/Logger')('Webserver');
class RestreamerExpressApp {
// middlewares
const expressLogger = require('./middlewares/expressLogger');
/**
* constructs a new express app with prod or dev config
*/
constructor () {
this.app = express();
this.secretKey = crypto.randomBytes(16).toString('hex');
// create express app
const app = express();
// generate random key
const secretKey = crypto.randomBytes(16).toString('hex');
app.use(session({
secret: secretKey,
resave: false,
// session secret
saveUninitialized: true}));
// create promise for 'websockets ready'
app.set('websocketsReady', Q.defer());
// add passport auth
app.use(passport.initialize());
// use connect-flash for flash messages stored in session
app.use(flash());
// persistent login sessions
app.use(passport.session());
require('./config/passport')(passport);
// configure express app
app.use(bodyParser.json());
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
app.use(compression());
app.set('json spaces', 4);
require('./controllers/index')(app, passport);
app.use('/', expressLogger);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found ' + req.url);
err.status = 404;
next(err);
});
// no stacktraces leaked to user
app.use(function (err, req, res, next) {
res.status(err.status || 500);
res.send({
message: err.message,
error: {}
});
});
var checkForAppUpdates = function () {
const url = {'host': 'datarhei.org', 'path': '/apps.json'};
logger.debug('Checking app for updates...');
https.get(url, function (response) {
if (response.statusCode === 200) {
response.on('data', function (body) {
var updateCheck = JSON.parse(body);
var updateAvailable = false;
if (updateCheck.restreamer.version === packageJson.version) {
updateAvailable = false;
logger.debug(`Checking app for updates successful. Update is not available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
} else {
updateAvailable = updateCheck.restreamer.version;
logger.debug(`Checking app for updates successful. Update is available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
}
logger.info('Checking app for updates successfull');
app.set('updateAvailable', updateAvailable);
});
if (process.env.RS_NODE_ENV === 'dev') {
this.initDev();
} else {
logger.info('Update check failed', false);
this.initProd();
}
}).on('error', function () {
logger.info('Update check failed', false);
});
};
}
// start interval to check for updates
checkForAppUpdates();
setInterval(checkForAppUpdates, 24 * 60 * 60 * 1000);
/**
* use sessions for the express app
*/
useSessions () {
this.app.use(session({
'secret': this.secretKey,
'resave': false,
'saveUninitialized': true // session secret
}));
}
module.exports = app;
/**
* use passport auth
*/
useAuth () {
// add passport auth
this.app.use(passport.initialize());
// persistent login sessions
this.app.use(passport.session());
// add config to passport
passportConfig(passport);
}
/**
* add automatic parsers for the body
*/
addParsers () {
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({'extended': true}));
this.app.use(cookieParser());
}
/**
* add content compression on responses
*/
addCompression () {
this.app.use(compression());
}
/**
* add express logger
*/
addExpressLogger () {
this.app.use('/', expressLogger);
}
/**
* beautify json response
*/
beautifyJSONResponse () {
this.app.set('json spaces', 4);
}
/**
* create a promise to check when websockets are ready for bindings
*/
createPromiseForWebsockets () {
this.app.set('websocketsReady', Q.defer());
}
/**
* add the restreamer routes
*/
addRoutes () {
indexRouter(this.app, passport);
this.app.use('/v1', apiV1);
}
/**
* add 404 error handling on pages, that have not been found
*/
add404ErrorHandling () {
this.app.use((req, res, next) => {
var err = new Error('Not Found ' + req.url);
err.status = 404;
next(err);
});
}
/**
* add ability for internal server errors
*/
add500ErrorHandling () {
this.app.use((err, req, res, next) => {
logger.error(err);
res.status(err.status || 500);
res.send({
'message': err.message,
'error': {}
});
});
}
/**
* check for app updates
*/
checkForRestreamerUpdates () {
const url = {'host': 'datarhei.org', 'path': '/apps.json'};
logger.debug('Checking app for updates...');
https.get(url, (response)=> {
if (response.statusCode === 200) {
response.on('data', (body)=> {
var updateCheck = JSON.parse(body);
var updateAvailable = false;
if (updateCheck.restreamer.version === packageJson.version) {
updateAvailable = false;
logger.debug(`Checking app for updates successful. Update is not available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
} else {
updateAvailable = updateCheck.restreamer.version;
logger.debug(`Checking app for updates successful. Update is available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
}
logger.info('Checking app for updates successful');
this.app.set('updateAvailable', updateAvailable);
});
} else {
logger.info('Update check failed', false);
}
}).on('error', () => {
logger.info('Update check failed', false);
});
}
/**
* get public ip of the app
*/
getPublicIp () {
logger.info('Getting public ip...', 'start.publicip');
exec('public-ip', (err, stdout, stderr) => {
if (err) {
logger.error(err);
}
this.app.set('publicIp', stdout.split('\n')[0]);
});
}
/**
* start the webserver and open the websocket
* @returns {*|promise}
*/
startWebserver () {
var deferred = Q.defer();
var server = null;
logger.info('Starting webserver...');
this.app.set('port', process.env.RS_NODE_PORT);
server = this.app.listen(this.app.get('port'), ()=> {
this.app.set('io', require('socket.io')(server));
this.app.set('server', server.address());
// promise to determine if the webserver has been started to avoid ws binding before
this.app.get('websocketsReady').resolve(this.app.get('io'));
logger.info(`Webserver running on port ${process.env.RS_NODE_PORT}`);
deferred.resolve(server.address().port);
});
return deferred.promise;
}
/**
* stuff that have always to be added to the webapp
*/
initAlways () {
this.useSessions();
this.useAuth();
this.addParsers();
this.addCompression();
this.addExpressLogger();
this.beautifyJSONResponse();
this.createPromiseForWebsockets();
this.addRoutes();
}
/**
* prod config for the express app
*/
initProd () {
logger.debug('init webserver with PROD environment');
this.initAlways();
this.app.get('/', (req, res)=> {
res.sendFile(path.join(global.__public, 'index.html'));
});
this.add404ErrorHandling();
this.add500ErrorHandling();
}
/**
* dev config for the express app
*/
initDev () {
logger.debug('init webserver with DEV environment');
this.initAlways();
this.app.get('/', (req, res)=> {
res.sendFile(path.join(global.__public, 'index-dev.html'));
});
this.add404ErrorHandling();
this.add500ErrorHandling();
}
}
const restreamerApp = new RestreamerExpressApp();
module.exports = restreamerApp;

View File

@@ -4,55 +4,36 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
var LocalStrategy = require('passport-local').Strategy;
var auth = require('../../../conf/live.json').auth;
if (process.env.RESTREAMER_USERNAME) {
var username = process.env.RESTREAMER_USERNAME;
} else {
var username = auth.username;
}
if (process.env.RESTREAMER_USERNAME) {
var password = process.env.RESTREAMER_PASSWORD;
} else {
var password = auth.password;
}
var auth = require(require('path').join(global.__base, 'conf', 'live.json')).auth;
module.exports = (passport) => {
// used to serialize the user for the session
passport.serializeUser(function (user, done) {
passport.serializeUser(function serializeUser (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
passport.deserializeUser(function deserializeUser (user, done) {
done(null, user);
});
passport.use('local-login', new LocalStrategy({
usernameField: 'user',
passwordField: 'pass',
passport.use('local-login', new LocalStrategy(
{
'usernameField': 'user',
'passwordField': 'pass',
'passReqToCallback': true // allows us to pass back the entire request to the callback
},
// callback with user and pass from our form
function checkLogin (req, user, pass, done) {
var username = process.env.RS_USERNAME || auth.username;
var password = process.env.RS_PASSWORD || auth.password;
// allows us to pass back the entire request to the callback
passReqToCallback: true
},
// callback with user and pass from our form
function (req, user, pass, done) {
// login success
if (user === username && pass === password) {
/*
* WEBSOCKET SECURITY HERE
*/
done(null, auth);
} else {
done(null, false, req.flash('wrong password or wrong user'));
}
}));
// login success
if (user === username && pass === password) {
// WEBSOCKET SECURITY HERE
done(null, auth);
} else {
done(null, false);
}
}));
};

View File

@@ -0,0 +1,43 @@
/**
* @file controller for routing from /v1
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
const express = require('express');
const router = new express.Router();
// TODO: solve the circular dependency problem and place Restreamer require here
router.get('/states', (req, res) => {
const states = require.main.require('./classes/Restreamer').data.states;
res.json({
'repeat_to_local_nginx': states.repeatToLocalNginx,
'repeat_to_optional_output': states.repeatToOptionalOutput
});
});
router.get('/progresses', (req, res) => {
const progresses = require.main.require('./classes/Restreamer').data.progresses;
res.json({
'repeat_to_local_nginx': {
'frames': progresses.repeatToLocalNginx.frames,
'current_fps': progresses.repeatToLocalNginx.currentFps,
'current_kbps': progresses.repeatToLocalNginx.currentKbps,
'target_size': progresses.repeatToLocalNginx.targetSize,
'timemark': progresses.repeatToLocalNginx.timemark
},
'repeat_to_optional_output': {
'frames': progresses.repeatToOptionalOutput.frames,
'current_fps': progresses.repeatToOptionalOutput.currentFps,
'current_kbps': progresses.repeatToOptionalOutput.currentKbps,
'target_size': progresses.repeatToOptionalOutput.targetSize,
'timemark': progresses.repeatToOptionalOutput.timemark
}
});
});
module.exports = router;

View File

@@ -4,73 +4,34 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
/* eslint no-unused-vars: 0 */
'use strict';
const express = require('express');
const path = require('path');
const Restreamer = require('../../classes/Restreamer');
module.exports = (app, passport) => {
// static paths
app.use('/css', express.static(path.join(__dirname, '../', 'public', 'css')));
app.use('/libs', express.static(path.join(__dirname, '../', 'public', 'libs')));
app.use('/dist', express.static(path.join(__dirname, '../', 'public', 'dist')));
app.use('/help', express.static(path.join(__dirname, '../', 'public', 'help')));
app.use('/images', express.static(path.join(__dirname, '../', 'public', 'images')));
app.use('/locales', express.static(path.join(__dirname, '../', 'public', 'locales')));
app.use('/crossdomain.xml', express.static(path.join(__dirname, '../', 'public', 'crossdomain.xml')));
app.use('/player.html', express.static(path.join(__dirname, '../', 'public', 'player.html')));
app.get('/main.html', (req, res)=>{
if (req.isAuthenticated()) {
res.sendFile(path.join(__dirname, '../', 'public', 'main.html'));
} else {
res.sendFile(path.join(__dirname, '../', 'public', 'login.html'));
}
app.get('/favicon.ico', (req, res) => {
res.sendFile(path.join(global.__public, 'images', 'favicon.ico'));
});
app.get('/', (req, res)=>{
res.sendFile(path.join(__dirname, '../', 'public', 'index.html'));
app.get('/main.html', (req, res) => {
if (req.isAuthenticated()) {
res.sendFile(path.join(global.__public, 'main.html'));
} else {
res.sendFile(path.join(global.__public, 'login.html'));
}
});
/* Handle Login POST */
app.post('/login',
passport.authenticate('local-login', {
successRedirect: '/',
failureRedirect: '/',
failureFlash : true
'successRedirect': '/',
'failureRedirect': '/#/login_invalid'
})
);
app.get('/logout', (req, res)=>{
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
// small get.api
app.get('/v1/states', (req, res)=>{
var states = Restreamer.data.states;
res.json({
repeat_to_local_nginx: states.repeatToLocalNginx,
repeat_to_optional_output: states.repeatToOptionalOutput
});
});
app.get('/v1/progresses', (req, res)=>{
var progresses = Restreamer.data.progresses;
res.json({
repeat_to_local_nginx: {
frames: progresses.repeatToLocalNginx.frames,
current_fps: progresses.repeatToLocalNginx.currentFps,
current_kbps: progresses.repeatToLocalNginx.currentKbps,
target_size: progresses.repeatToLocalNginx.targetSize,
timemark: progresses.repeatToLocalNginx.timemark
},
repeat_to_optional_output: {
frames: progresses.repeatToOptionalOutput.frames,
current_fps: progresses.repeatToOptionalOutput.currentFps,
current_kbps: progresses.repeatToOptionalOutput.currentKbps,
target_size: progresses.repeatToOptionalOutput.targetSize,
timemark: progresses.repeatToOptionalOutput.timemark
}
});
});
};

View File

@@ -4,24 +4,25 @@
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
/* eslint vars-on-top: 0 */
const Logger = require('../../classes/Logger');
const logger = new Logger('webserver');
const logger = require.main.require('./classes/Logger')('webserver');
module.exports = (req, res, next)=>{
module.exports = (req, res, next)=> {
req._startTime = new Date();
var log = () =>{
var log = () => {
var code = res.statusCode;
var len = parseInt(res.getHeader('Content-Length'), 10);
var duration = ('new Date' - 'req._startTime');
var url = (req.originalUrl || req.url);
var method = req.method;
if (isNaN(len)) {
len = '';
} else {
len = ' - ' + len;
}
var duration = ('new Date' - 'req._startTime');
var url = (req.originalUrl || req.url);
var method = req.method;
logger.debug(method + ' \'' + url + '\' ' + code + ' ' + duration + ' ' + req.ip + ' ' + len, 'Webserver');
};

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" />
<allow-access-from domain="*"/>
</cross-domain-policy>

View File

@@ -1,7 +1,7 @@
html, body {
height:100%;
margin:0;
padding:0
height: 100%;
margin: 0;
padding: 0
}
body {
@@ -9,32 +9,29 @@ body {
}
.container-fluid {
height:100%;
display:table;
height: 100%;
display: table;
width: 100%;
padding: 0;
background: url(../images/bg.png) no-repeat center center fixed;
background: #3d3d39 url(../images/bg.png) no-repeat fixed center center;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
background-color: #3d3d39;
}
.container-body {
float: none;
margin: 0 auto;
max-width: 500px;
border-color: #2c2c28;
border-width: 1px;
border-style: solid;
border: 1px solid #2c2c28;
border-radius: 10px;
padding: 50px;
}
.row-fluid {
height: 100%;
display:table-cell;
display: table-cell;
vertical-align: middle;
}
@@ -53,7 +50,7 @@ h1, h2, h3, h4, h5, h6 {
}
h1 {
text-shadow: 0px 1px #000;
text-shadow: 0 1px #000;
font-weight: 100;
margin-bottom: 30px;
font-size: 46px;
@@ -62,9 +59,10 @@ h1 {
a {
color: #fff;
text-decoration: none;
cursor: pointer;
}
a:visted {
a:visited {
color: #fff;
text-decoration: none;
}
@@ -74,7 +72,6 @@ a:hover, a:active, a:focus {
text-decoration: none;
}
.locales {
color: #60605e;
text-decoration: none;
@@ -120,9 +117,9 @@ a:hover, a:active, a:focus {
color: #373734;
}
.form-control[disabled]{
.form-control[disabled] {
color: #60605e;
font-weight:bold;
font-weight: bold;
background-color: #373734;
border: 2px solid #434341;
}
@@ -157,14 +154,12 @@ a:hover, a:active, a:focus {
background-color: #ac5647;
color: #fff;
}
.container .jumbotron, .container-fluid .jumbotron, .jumbotron {
padding-top: 18px;
padding-bottom: 18px;
margin-top: -4px;
margin-bottom: 18px;
text-align: center;
padding-right: 20px;
padding-left: 20px;
padding: 18px 20px;
}
.player-link {
@@ -187,12 +182,13 @@ label {
.modal-content {
background-color: #373734;
}
.modal-header {
border-bottom: 0px solid #e5e5e5;
border-bottom: 0 solid #e5e5e5;
}
.modal-footer {
border-top: 0px solid #e5e5e5;
border-top: 0 solid #e5e5e5;
text-align: left;
}
@@ -209,7 +205,7 @@ label {
pre {
display: block;
padding: 9.5px;
padding: 10px;
margin: 0 0 10px;
font-size: 12px;
line-height: 1.42857143;
@@ -224,3 +220,23 @@ pre {
.jwplayer:focus, .jwplayer:active {
outline: 0;
}
.footer .links a {
margin-left: 10px;
}
.underline {
text-decoration: underline;
}
.icon16 {
font-size: 16px;
}
.icon14 {
font-size: 14px;
}
.green {
color: #3daa48;
}

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Restreamer">
<meta name="author" content="datarhei">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<title>Restreamer</title>
<link href="/libs/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<link href="/css/restreamer.css" rel="stylesheet">
<script src="/libs/jquery/dist/jquery.js"></script>
<script src="/libs/bootstrap/dist/js/bootstrap.js"></script>
<script src="/libs/clappr/dist/clappr.js"></script>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script src="/socket.io/socket.io.js"></script>
<script src="/libs/angular/angular.js"></script>
<script src="/libs/ui-router/release/angular-ui-router.js"></script>
<script src="/libs/angular-animate/angular-animate.js"></script>
<script src="/libs/angular-bootstrap/ui-bootstrap.js"></script>
<script src="/libs/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="/libs/angular-translate/angular-translate.js"></script>
<script src="/libs/angular-translate-loader-static-files/angular-translate-loader-static-files.js"></script>
<!-- ANGULAR APP -->
<script src="/scripts/App.js"></script>
<script src="/scripts/App.Config.js"></script>
<!-- HEADER MODULE -->
<script src="/scripts/Header/HeaderModule.js"></script>
<script src="/scripts/Header/HeaderDirective.js"></script>
<script src="/scripts/Header/HeaderController.js"></script>
<!-- MAIN MODULE -->
<script src="/scripts/Main/MainModule.js"></script>
<script src="/scripts/Main/MainController.js"></script>
<!-- LOGIN MODULE -->
<script src="/scripts/Login/LoginModule.js"></script>
<script src="/scripts/Login/LoginController.js"></script>
<!-- FOOTER MODULE -->
<script src="/scripts/Footer/FooterModule.js"></script>
<script src="/scripts/Footer/FooterDirective.js"></script>
<script src="/scripts/Footer/FooterController.js"></script>
<!-- STREAMING INTERFACE MODULE -->
<script src="/scripts/StreamingInterface/StreamingInterfaceModule.js"></script>
<script src="/scripts/StreamingInterface/StreamingStatusController.js"></script>
<script src="/scripts/StreamingInterface/StreamingStatusDirective.js"></script>
<!-- SHARED -->
<script src="/scripts/Shared/LoggerService.js"></script>
<script src="/scripts/Shared/WebsocketsService.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div class="container-body">
<div header></div>
<div id="content" ui-view></div>
<hr/>
<div footer></div>
</div>
</div>
</div>
<!-- Large modal -->
<div id="player-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Player</h4>
</div>
<div class="modal-body">
<div class="embed-responsive embed-responsive-16by9">
<div id="player" class="embed-responsive-item"></div>
</div>
<h4>{{'player_modal_help_title' | translate}}
<a href="{{config.urls.embedPlayerHelp}}" target="_blank" class="green underline">
<span class="glyphicon glyphicon-question-sign icon16" aria-hidden="true"></span>
</a>
</h4>
<pre>&lt;iframe src="http://{{publicIp}}:{{windowLocationPort}}/player.html" name="restreamer-player" width="800" height="450" scrolling="no" frameborder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen="true"&gt;&lt;/iframe&gt;</pre>
<pre>&lt;img src="http://{{publicIp}}:{{windowLocationPort}}/images/live.jpg" width="800" height="450"&gt;</pre>
<p>{{'player_modal_help_content' | translate}}
<a href="{{config.urls.portForwardingHelp}}" target="_blank" class="underline">
<span class="glyphicon glyphicon-question-sign icon14" aria-hidden="true"></span>
</a>
</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Restreamer">
<meta name="author" content="datarhei">
<link rel="icon" href="images/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<title>Restreamer</title>
<link href="/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
@@ -29,30 +29,17 @@
<script src="/libs/angular-bootstrap/ui-bootstrap-tpls.min.js"></script>
<script src="/libs/angular-translate/angular-translate.min.js"></script>
<script src="/libs/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js"></script>
<script src="/dist/application.js"></script>
<script src="/dist/application.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div class="container-body">
<div ng-controller="languageCtrl">
<p class="pull-right">
<a href="#" ng-click="switchLanguage('en_US')" class="locales" ng-class="{'active': langIs('en_US')}">EN</a>
/
<a href="#" ng-click="switchLanguage('de_DE')" class="locales" ng-class="{'active': langIs('de_DE')}">DE</a>
</p>
</div>
<h1>Restreamer</h1>
<div id="content" ui-view ></div>
<hr />
<div id="footer">
<span class="version">
v{{version}}
</span>
<a href="https://datarhei.github.io/restreamer/docs/references-updates.html" target="_blank" class="btn btn-xs btn-success ng-binding ng-scope" ng-if="checkForAppUpdatesResult != false">{{'update_btn' | translate}}</a>
<p class="pull-right"><a href="https://github.com/datarhei/restreamer/issues/new" target="_blank">{{'issue_tracker' | translate}}</a> <a href="https://github.com/datarhei/restreamer" target="_blank" style="padding-left:10px">{{'project_page' | translate}}</a></p>
</div>
<div header></div>
<div id="content" ui-view></div>
<hr/>
<div footer></div>
</div>
</div>
</div>
@@ -62,17 +49,27 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Player</h4>
</div>
<div class="modal-body">
<div class="embed-responsive embed-responsive-16by9">
<div id="player" class="embed-responsive-item"></div>
</div>
<h4>{{'player_modal_help_titel' | translate}} <a href="https://datarhei.github.io/restreamer/docs/guides-embed-upon-your-website.html" target="_blank" style="color:#3daa48;text-decoration:underline"><span class="glyphicon glyphicon-question-sign" style="font-size: 16px;" aria-hidden="true"></span></a></h4>
<h4>{{'player_modal_help_title' | translate}}
<a href="{{config.urls.embedPlayerHelp}}" target="_blank" class="green underline">
<span class="glyphicon glyphicon-question-sign icon16" aria-hidden="true"></span>
</a>
</h4>
<pre>&lt;iframe src="http://{{publicIp}}:{{windowLocationPort}}/player.html" name="restreamer-player" width="800" height="450" scrolling="no" frameborder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen="true"&gt;&lt;/iframe&gt;</pre>
<pre>&lt;img src="http://{{publicIp}}:{{windowLocationPort}}/images/live.jpg" width="800" height="450"&gt;</pre>
<p>{{'player_modal_help_content' | translate}} <a href="https://datarhei.github.io/restreamer/wiki/portforwarding.html" target="_blank" style="text-decoration:underline"><span class="glyphicon glyphicon-question-sign" style="font-size: 14px;" aria-hidden="true"></span></a></p>
<p>{{'player_modal_help_content' | translate}}
<a href="{{config.urls.portForwardingHelp}}" target="_blank" class="underline">
<span class="glyphicon glyphicon-question-sign icon14" aria-hidden="true"></span>
</a>
</p>
</div>
</div>
</div>

View File

@@ -1,15 +1,18 @@
{
"help_titel": "Hilfe",
"help_title": "Hilfe",
"issue_tracker": "Fehler melden",
"project_page": "Hilfe",
"update_btn": "Update verfügbar",
"login_username": "Benutzername",
"login_password": "Passwort",
"login_invalid": "Benutzername oder Passwort ist ungültig",
"login_btn": "Anmelden",
"logout": "Ausloggen",
"button_start": "Start",
"button_stop": "Stop",
"input_titel": "RTMP/RTSP Video Quelle",
"input_title": "RTMP/RTSP Video Quelle",
"input_example": "z.B. rtsp://192.168.57.100/media.amp",
"rtsp_tcp": "RTSP über TCP",
"process_input_invalid": "Die Stream-Adresse ist nicht valide. Bitte überprüfe die Eingabe",
"process_success": "Das Streaming wurde erfolgreich aufgebaut.",
"process_failed": "Auf den Stream konnte nicht zugegriffen werden.",
@@ -17,7 +20,7 @@
"process_init": "Der Streaming-Prozess wird erstellt. Bitte warten...",
"output_optional": "Externer RTMP-Streaming-Server",
"output_optional_example": "z.b. rtmp://live.youtube.com/channelId",
"player_link_titel": "Player öffnen",
"player_modal_help_titel": "Der iFrame-Code und ein Preview-Image zur Einbettung in der Webseite:",
"player_link_title": "Player öffnen",
"player_modal_help_title": "Der iFrame-Code und ein Preview-Image zur Einbettung in der Webseite:",
"player_modal_help_content": "Zusätzlich muss der Port aus den Beispielen auf die IP-Adresse des Rechners weitergeleitet werden"
}

View File

@@ -1,23 +1,26 @@
{
"help_titel": "Help",
"help_title": "Help",
"issue_tracker": "Issue alert",
"project_page": "Help",
"update_btn": "Update available",
"login_username": "Username",
"login_password": "Password",
"login_invalid": "Username or Password is invalid",
"login_btn": "Login",
"logout": "Logout",
"button_start": "Start",
"button_stop": "Stop",
"input_titel": "RTMP/RTSP Video Source",
"input_title": "RTMP/RTSP Video Source",
"input_example": "e.g. rtsp://192.168.57.100/media.amp",
"rtsp_tcp": "RTSP over TCP",
"process_input_invalid": "Invalid stream address, please check your input",
"process_success": "Streaming is successfully initiated.",
"process_failed": "Your stream wasnt accessable.",
"process_failed": "Your stream wasn't accessible.",
"process_failed_retry": "Retry count",
"process_init": "Streaming process is initiating. Please wait...",
"output_optional": "External RTMP-Streaming-Server",
"output_optional_example": "e.g. rtmp://live.youtube.com/channelId",
"player_link_titel": "Open player",
"player_modal_help_titel": "iFrame code and preview of image to embed in website:",
"player_link_title": "Open player",
"player_modal_help_title": "iFrame code and preview of image to embed in website:",
"player_modal_help_content": "In addition the port of the examples have to be forwarded from the router to IP address of the computer"
}

View File

@@ -0,0 +1,15 @@
<form action='/login' method='post'>
<div class="form-group ng-scope">
<input id="input_username" class="form-control input ng-pristine ng-untouched ng-valid" type='text' name='user' placeholder='{{"login_username" | translate}}'>
</div>
<div class="form-group ng-scope">
<input id="input_password" class="form-control input ng-pristine ng-untouched ng-valid" type="password" name='pass' placeholder='{{"login_password" | translate}}'>
</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-if="login_invalid">{{"login_invalid" | translate}}</div>
<div class="text-right">
<button class="btn btn-success ng-binding ng-scope" type="submit">{{'login_btn' | translate}}</button>
</div>
</form>

View File

@@ -0,0 +1,85 @@
<!--
Repeat to local nginx
-->
<meta ng-init="$root.loggedIn = true">
<div class="form-group">
<label>
{{'input_title' | translate}}
<a href="https://datarhei.github.io/restreamer/docs/references-rtmp-rtsp-video-source.html" target="_blank">
<span class="glyphicon glyphicon-question-sign icon16" aria-hidden="true"></span>
</a>
</label>
<input type="text" class="form-control input" id="input_uri"
placeholder="{{'input_example' | translate}}"
ng-model="reStreamerData.addresses.srcAddress"
ng-disabled="showStopButton('repeatToLocalNginx')">
</div>
<streaming-status name="repeatToLocalNginx" data="reStreamerData"></streaming-status>
<div class="form-inline form-group">
<div ng-if="reStreamerData.addresses.srcAddress.indexOf('rtsp') === 0 && showStartButton('repeatToLocalNginx')"
class="checkbox pull-left">
<label>
<input type="checkbox" ng-model="reStreamerData.options.rtspTcp"> {{'rtsp_tcp' | translate}}
<!-- TODO: add help for RTSP over TCP
<a href="{{config.urls.rtspTcpHelp}}" target="_blank">
<span class="glyphicon glyphicon-question-sign icon14" aria-hidden="true"></span>
</a>
-->
</label>
</div>
<div class="player-link pull-left" ng-if="reStreamerData.states.repeatToLocalNginx.type == 'connected'">
<a class="underline" ng-click="openPlayer()">
{{'player_link_title' | translate}}
</a>
</div>
<button type="button" class="btn btn-success pull-right"
ng-if="showStartButton('repeatToLocalNginx')"
ng-click="startStream('repeatToLocalNginx')">
{{'button_start' | translate}}
</button>
<button type="button" class="btn btn-danger pull-right"
ng-if="showStopButton('repeatToLocalNginx')"
ng-click="stopStream('repeatToLocalNginx')">
{{'button_stop' | translate}}
</button>
<div class="clearfix"></div>
</div>
<!-- todo: this hr to css border-bottom or something-->
<hr/>
<!--
Repeat to optional Output
-->
<h4>Optional:</h4>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="activateOptionalOutput"> {{'output_optional' | translate}}
<a href="{{config.urls.rtmpServerHelp}}" target="_blank">
<span class="glyphicon glyphicon-question-sign icon14" aria-hidden="true"></span>
</a>
</label>
</div>
<div>
<div class="form-group" ng-show="activateOptionalOutput === true">
<input type="text" class="form-control input" id="output_optional_uri"
placeholder="{{'output_optional_example' | translate}}"
ng-model="reStreamerData.addresses.optionalOutputAddress"
ng-disabled="showStopButton('repeatToOptionalOutput')">
</div>
</div>
<streaming-status name="repeatToOptionalOutput" data="reStreamerData"></streaming-status>
<div class="text-right">
<div class="btn btn-danger" ng-click="stopStream('repeatToOptionalOutput')"
ng-if="showStopButton('repeatToOptionalOutput')">{{'button_stop' | translate}}
</div>
<div ng-show="activateOptionalOutput === true">
<div class="btn btn-success" ng-click="startStream('repeatToOptionalOutput')"
ng-if="showStartButton('repeatToOptionalOutput')">{{'button_start' | translate}}
</div>
</div>
</div>

View File

@@ -12,7 +12,7 @@
<script src="/libs/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="/libs/clappr/dist/clappr.min.js"></script>
</head>
<body style="margin:0px;background-color:#000;">
<body style="margin: 0; background-color: #000;">
<div class="container-fluid">
<div class="row">
<div>
@@ -20,14 +20,15 @@
<div id="player" class="embed-responsive-item"></div>
</div>
<script>
var player = new Clappr.Player({
source: ("https:" === window.location.protocol ? "https:" : "http:") + "//" + window.location.hostname + ":" + window.location.port + "/hls/live.stream.m3u8",
parentId: "#player",
baseUrl: '/libs/clappr/dist/',
poster: "images/live.jpg",
mediacontrol: {seekbar: "#3daa48", buttons: "#3daa48"},
height: "100%",
width: "100%"
var player = new window.Clappr.Player({
'source': (window.location.protocol === 'https:' ? 'https:' : 'http:') +
'//' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8',
'parentId': '#player',
'baseUrl': '/libs/clappr/dist/',
'poster': 'images/live.jpg',
'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'},
'height': '100%',
'width': '100%'
});
</script>
</div>

View File

@@ -0,0 +1,25 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
// Global config for frontend
const config = {
'urls': {
// project
'issueTracker': 'https://github.com/datarhei/restreamer/issues/new',
'projectPage': 'https://github.com/datarhei/restreamer',
'updatePage': 'https://datarhei.github.io/restreamer/docs/references-updates.html',
// help
'embedPlayerHelp': 'https://datarhei.github.io/restreamer/docs/guides-embed-upon-your-website.html',
'portForwardingHelp': 'https://datarhei.github.io/restreamer/wiki/portforwarding.html',
'rtmpServerHelp': 'https://datarhei.github.io/restreamer/docs/references-external-rtmp-streaming-server.html'
}
};
window.angular.module('app').constant('config', config);

View File

@@ -0,0 +1,38 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
var app = window.angular.module('app', [
'ui.router',
'ui.bootstrap',
'pascalprecht.translate',
'Footer',
'Header',
'Main',
'StreamingInterface',
'Login']);
app.config(($stateProvider, $urlRouterProvider) => {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('main', {
'templateUrl': 'main.html',
'url': '/:error',
'controller': 'mainController'
})
.state('helpSource', {
'templateUrl': 'help/source.html',
'url': '/help/source'
})
.state('helpOptionalOutput', {
'templateUrl': 'help/optionalOutput.html',
'url': '/help/optionalOutput'
});
});

View File

@@ -0,0 +1,14 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Footer').controller('footerController', ['ws', '$scope', 'config', (ws, $scope, config) => {
ws.emit('getVersion');
ws.on('version', (version) => {
$scope.version = version;
$scope.config = config;
});
}]);

View File

@@ -0,0 +1,15 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Footer').directive('footer', () => {
return {
'restrict': 'A',
'replace': true,
'templateUrl': '/scripts/Footer/_footer.html',
'controller': 'footerController'
};
});

View File

@@ -0,0 +1,8 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Footer', []);

View File

@@ -0,0 +1,14 @@
<div class="footer">
<span class="version">
v{{version}}
</span>
<a class="btn btn-xs btn-success ng-binding ng-scope"
ng-if="checkForAppUpdatesResult != false" href="{{config.urls.updatePage}}" target="_blank">
{{'update_btn' | translate}}
</a>
<p class="pull-right links">
<a href="{{config.urls.issueTracker}}" target="_blank">{{'issue_tracker' | translate}}</a>
<a href="{{config.urls.projectPage}}" target="_blank">{{'project_page' | translate}}</a>
<a ng-if="loggedIn" href="/logout">{{'logout' | translate}}</a>
</p>
</div>

View File

@@ -0,0 +1,23 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Header').controller('headerController', ['$scope', '$translate', 'loggerService', ($scope, $translate, loggerService) => {
$scope.currentLocale = $translate.preferredLanguage();
$scope.switchLanguage = (locale) => {
$scope.currentLocale = locale;
$translate.use(locale).then(
() => {
loggerService.info('Switched language to ' + locale);
},
(error) => {
loggerService.error('INFO', 'Switching language to ' + locale + ' failed: ' + error);
});
};
$scope.langIs = function langIs (locale) {
return locale === $scope.currentLocale;
};
}]);

View File

@@ -0,0 +1,15 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Header').directive('header', () => {
return {
'restrict': 'A',
'replace': true,
'templateUrl': '/scripts/Header/_header.html',
'controller': 'headerController'
};
});

View File

@@ -0,0 +1,17 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Header', []);
window.angular.module('Header').config(['$translateProvider', ($translateProvider) => {
$translateProvider.useStaticFilesLoader({
'prefix': 'locales/lang-',
'suffix': '.json'
});
$translateProvider.useSanitizeValueStrategy('escape');
$translateProvider.preferredLanguage('en_US');
}]);

View File

@@ -0,0 +1,9 @@
<div>
<p class="pull-right">
<a ng-click="switchLanguage('en_US')" class="locales" ng-class="{'active': langIs('en_US')}">EN</a>
/
<a ng-click="switchLanguage('de_DE')" class="locales" ng-class="{'active': langIs('de_DE')}">DE</a>
</p>
<h1>Restreamer</h1>
</div>

View File

@@ -0,0 +1,8 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Login').controller('loginController', [], () => {});

View File

@@ -0,0 +1,9 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
// create new angular module
window.angular.module('Login', []);

View File

@@ -0,0 +1,157 @@
/**
* @file holds the Angularjs mainController
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Main').controller('mainController',
['ws', '$scope', '$location', '$rootScope', '$stateParams', 'config',
function mainController (ws, $scope, $location, $rootScope, $stateParams, config) {
let setup = false;
let player = null;
if ($stateParams.error === 'login_invalid') {
$scope.login_invalid = 'login_invalid';
}
$scope.config = config;
const initClappr = () => {
player = new window.Clappr.Player({
'source': (window.location.protocol === 'https:' ? 'https:' : 'http:') +
'//' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8',
'parentId': '#player',
'baseUrl': '/libs/clappr/dist/',
'poster': 'images/live.jpg',
'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'},
'height': '100%',
'width': '100%'
});
};
$rootScope.loggedIn = false;
$scope.optionalOutputInputInvalid = false;
$scope.nginxRepeatStreamInputInvalid = false;
$scope.reStreamerData = {
'retryCounter': {
'repeatToLocalNginx': 0,
'repeatToOptionalOutput': 0
},
'options': {
'rtspTcp': false
},
'states': {
'repeatToLocalNginx': {
'type': ''
},
'repeatToOptionalOutput': {
'type': ''
}
},
'userActions': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'progresses': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'addresses': {
'optionalOutputAddress': '',
'srcAddress': ''
}
};
$rootScope.windowLocationPort = $location.port();
$scope.optionalOutput = '';
$scope.showStopButton = (streamType) => {
return $scope.reStreamerData.userActions[streamType] === 'start';
};
$scope.showStartButton = (streamType) => {
return $scope.reStreamerData.userActions[streamType] === 'stop';
};
$scope.openPlayer = () => {
if (player === null) {
initClappr();
}
$('#player-modal').modal('show').on('hide.bs.modal', function closeModal (e) {
player.stop();
$(this).off('hide.bs.modal');
$(this).modal('hide');
return e.preventDefault();
});
};
/*
* Configure Websockets
*/
// check states of hls and rtmp stream
ws.emit('checkStates');
// check for app updates
ws.emit('checkForAppUpdates');
// prohibit double binding of events
if (!setup) {
/*
* test websockets connection (should print below message to browser console if it works)
*/
ws.on('updateProgress', (progresses) => {
$scope.reStreamerData.progresses = progresses;
});
ws.on('publicIp', (publicIp) => {
$rootScope.publicIp = publicIp;
});
ws.on('updateStreamData', (reStreamerData) => {
$scope.reStreamerData = reStreamerData;
if ($scope.showStopButton('repeatToOptionalOutput')) {
// checkbox
$scope.activateOptionalOutput = true;
}
});
ws.on('checkForAppUpdatesResult', (result) => {
$rootScope.checkForAppUpdatesResult = result;
});
}
$scope.startStream = (streamType) => {
const rtmpRegex = /^(?:rtmp:\/\/|rtsp:\/\/)(?:(?:[^:])+:(?:[^@])+@)?(?:(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}))(:?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?(?:\/.*)?/;
var optionalOutput = '';
if ($scope.activateOptionalOutput === true) {
optionalOutput = $scope.reStreamerData.addresses.optionalOutputAddress;
}
if (streamType === 'repeatToOptionalOutput') {
$scope.optionalOutputInputInvalid = !rtmpRegex.test(optionalOutput);
if ($scope.optionalOutputInputInvalid) {
return;
}
} else {
$scope.nginxRepeatStreamInputInvalid = !rtmpRegex.test($scope.reStreamerData.addresses.srcAddress);
if ($scope.nginxRepeatStreamInputInvalid) {
return;
}
}
ws.emit('startStream', {
'src': $scope.reStreamerData.addresses.srcAddress,
'options': $scope.reStreamerData.options,
'streamType': streamType,
'optionalOutput': optionalOutput
});
};
$scope.stopStream = (streamType) => {
ws.emit('stopStream', streamType);
};
}]);

View File

@@ -0,0 +1,8 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('Main', []);

View File

@@ -4,9 +4,10 @@
* @license Apache-2.0
*/
/*
styles of the logging outputs
*/
/* eslint no-console: 0*/
'use strict';
// styles of the logging output
const INFO = 'color: #0000FF; font-weight: bold';
const DEBUG = 'color: #AABBCC; font-weight: bold';
const ERROR = 'color: #FF0011d; font-weight: bold';
@@ -14,74 +15,67 @@ const WEBSOCKETS_IN = 'color: #00BFFF; font-weight: bold';
const WEBSOCKETS_OUT = 'color: #00BF00; font-weight: bold';
const WEBSOCKETS_NAMESPACE = 'color: #00BF00; font-weight: bold';
class loggerService {
/*
* no further dependencies needed
*/
constructor () {}
/*
const LoggerService = function loggerService () {
/**
* log an info message
* @param {string} message
*/
info (message) {
this.info = (message) => {
this.log(INFO, message, 'INFO');
}
};
/*
* log an info message
/**
* log an debug message
* @param {string} message
*/
debug (message) {
this.debug = (message) => {
this.log(DEBUG, message, 'DEBUG');
}
};
/*
* log an info message
/**
* log an error message
* @param {string} message
*/
error (message) {
this.error = (message) => {
this.log(ERROR, message, 'ERROR');
}
};
/*
* log an info message
/**
* log an websocket in message
* @param {string} message
*/
websockets_in (message) {
this.websocketsIn = (message) => {
this.log(WEBSOCKETS_IN, message, 'WS_IN');
}
};
/*
* log an info message
/**
* log an websocket out message
* @param {string} message
*/
websockets_out (message) {
this.websocketsOut = (message) => {
this.log(WEBSOCKETS_OUT, message, 'WS_OUT');
}
};
/*
* log an info message
/**
* log an websocket namespace message
* @param {string} message
*/
websockets_namespace (message) {
this.websocketsNamespace = (message) => {
this.log(WEBSOCKETS_NAMESPACE, message, 'WS_CONNECT');
}
};
/*
/**
* log a message with style
* @param {string} style
* @param {string} message
* @param {string} type
*/
log (style, message, type) {
console.log('%c ' + '[' + type + ']' + message, style);
}
}
this.log = (style, message, type) => {
console.log('%c [' + type + ']' + message, style);
};
};
/*
* configure loggerService as angulerjs Service
*/
window.app.factory('loggerService', function () {
return new loggerService();
// configure loggerService as AngularJS Service
window.angular.module('app').factory('loggerService', () => {
return new LoggerService();
});

View File

@@ -0,0 +1,61 @@
/**
* @file Service to handle websocket connections and events
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
/* eslint no-undef: 0*/
'use strict';
const WebsocketsService = function websockeService ($rootScope, loggerService) {
this.$rootScope = $rootScope;
this.loggerService = loggerService;
this.socket = io.connect();
this.loggerService.websocketsNamespace('websockets connected');
/**
* emit an event to socket
* @param event
* @param data
* @returns {WebsocketsService}
*/
this.emit = (event, data) => {
this.loggerService.websocketsOut(`emit event "${event}"`);
this.socket.emit(event, data);
return this;
};
/**
* react on an event to socket with callback
* @param event
* @param {function} callback
* @returns {WebsocketsService}
*/
this.on = (event, callback) => {
var self = this;
this.loggerService.websocketsIn(`got event "${event}"`);
this.socket.on(event, function woEvent () {
var args = arguments;
self.$rootScope.$apply(function weApply () {
callback.apply(null, args);
});
});
return this;
};
/**
* disable an event on socket
* @param event
* @param callback
*/
this.off = (event, callback) => {
this.socket.removeListener(event, callback);
};
};
// connect service to angular.js
window.angular.module('app').factory('ws', ['$rootScope', 'loggerService', ($rootScope, loggerService) => {
return new WebsocketsService($rootScope, loggerService);
}]);

View File

@@ -0,0 +1,8 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('StreamingInterface', []);

View File

@@ -0,0 +1,62 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
/**
* Streaming Status Controller
*
* controlls the display of the streaming status (fps, status)
*/
window.angular.module('StreamingInterface').controller('streamingStatusController',
['$scope', ($scope) => {
/**
* extract statusName
* @returns {string}
*/
const statusName = () => {
return $scope.data.states[$scope.name].type;
};
/**
* check if the status is connecting
* @returns {boolean}
*/
$scope.connecting = () => {
return statusName() === 'connecting';
};
/**
* check if the status is connected
* @returns {boolean}
*/
$scope.connected = () => {
return statusName() === 'connected';
};
/**
* check if the status is error
* @returns {boolean}
*/
$scope.error = () => {
$scope.retries = $scope.data.retryCounter[$scope.name].current;
$scope.maxRetries = $scope.data.retryCounter[$scope.name].max;
return statusName() === 'error';
};
/**
* @returns {number} current FPS
*/
$scope.fps = () => {
return $scope.data.progresses ? $scope.data.progresses[$scope.name].currentFps : 0;
};
/**
* @returns {number} current bit rate
*/
$scope.kbps = () => {
return $scope.data.progresses ? $scope.data.progresses[$scope.name].currentKbps : 0;
};
}]);

View File

@@ -0,0 +1,19 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
'use strict';
window.angular.module('StreamingInterface').directive('streamingStatus', () => {
return {
'scope': {
'data': '=',
'name': '@name'
},
'restrict': 'E',
'replace': true,
'templateUrl': '/scripts/StreamingInterface/_streamingStatus.html',
'controller': 'streamingStatusController'
};
});

View File

@@ -0,0 +1,30 @@
<div class="streamingStatus">
<!-- Stream is connecting -->
<div class="jumbotron progress-bar-info progress-bar-striped" ng-if="connecting()">
{{'process_init' | translate}}
</div>
<!-- Stream is connected -->
<div class="jumbotron progress-bar-success progress-bar-striped" ng-if="connected()">
{{'process_success' | translate}}
<div ng-if="fps()">
<span class="ffmpeg-progress fps">{{fps()}} FPS</span>
/
<span class="ffmpeg-progress kbps">{{kbps()}} Kb/s</span>
</div>
</div>
<!-- Stream has an error -->
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-if="error()">
<span>{{'process_failed' | translate}}</span>
<span ng-if="maxRetries >= retries">{{'process_failed_retry' | translate}}</span>
<span ng-if="maxRetries >= retries">{{retries}} / {{maxRetries}}</span>
</div>
<!-- regex error on input field -->
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-show="nginxRepeatStreamInputInvalid">
{{'process_input_invalid' | translate}}
</div>
</div>

View File

@@ -1,50 +0,0 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
var app = angular.module('app', [ 'ui.router', 'ui.bootstrap', 'pascalprecht.translate' ]);
app.config(function ($translateProvider) {
// lang
$translateProvider.useStaticFilesLoader({
prefix: 'locales/lang-',
suffix: '.json'
});
$translateProvider.useSanitizeValueStrategy('escape');
$translateProvider.preferredLanguage('en_US');
});
app.config(function ($stateProvider, $urlRouterProvider) {
// For any unmatched url, redirect to /
$urlRouterProvider.otherwise('/');
$stateProvider
.state('main', {
templateUrl: 'main.html',
url: '/',
controller: 'mainCtrl'}
)
.state('helpSource', {
templateUrl: 'help/source.html',
url: '/help/source'}
)
.state('helpOptionalOutput', {
templateUrl: 'help/optionalOutput.html',
url: '/help/optionalOutput'}
);
});
app.filter('inArray', function ($filter) {
return function (list, arrayFilter, element) {
if (arrayFilter) {
return $filter('filter')(list, function (listItem) {
return arrayFilter.indexOf(listItem[element]) !== -1;
});
}
};
});

View File

@@ -1,20 +0,0 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
window.app.controller('languageCtrl', [ '$scope', '$translate', function ($scope, $translate) {
$scope.currentLocale = $translate.preferredLanguage();
$scope.switchLanguage = function (locale) {
$scope.currentLocale = locale;
$translate.use(locale).then(function () {
window.Logger.log('INFO', 'Switched language to ' + locale);
}, function (error) {
window.Logger.error('INFO', 'Switching language to ' + locale + ' failed: ' + error);
});
};
$scope.langIs = function (locale) {
return locale === $scope.currentLocale;
};
} ]);

View File

@@ -1,174 +0,0 @@
/**
* @file holds the Angularjs mainController
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
window.app.controller('mainCtrl', [ 'ws', '$scope', '$location', '$rootScope', '$translate', function (ws, $scope, $location, $rootScope, $translate) {
// binding just once
var setup = false;
var player = null;
const initClappr = function () {
player = new Clappr.Player({
source: 'http://' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8',
parentId: '#player',
baseUrl: '/libs/clappr/dist/',
poster: 'images/live.jpg',
mediacontrol: {seekbar: '#3daa48', buttons: '#3daa48'},
height: '100%',
width: '100%'
});
};
$translate.use('en_US');
$scope.optionalOutputInputInvalid = false;
$scope.nginxRepeatStreamInputInvalid = false;
$scope.reStreamerData = {
retryCounter: {
repeatToLocalNginx: 0,
repeatToOptionalOutput: 0
},
states: {
repeatToLocalNginx: {
type: ''
},
repeatToOptionalOutput: {
type: ''
}
},
userActions: {
repeatToLocalNginx: '',
repeatToOptionalOutput: ''
},
addresses: {
optionalOutputAddress: '',
srcAddress: ''
}
};
$rootScope.windowLocationPort = $location.port();
$scope.optionalOutput = '';
$scope.showStopButton = function (streamType) {
return $scope.reStreamerData.userActions[streamType] === 'start';
};
$scope.showStartButton = function (streamType) {
return $scope.reStreamerData.userActions[streamType] === 'stop';
};
$scope.nginxRepeatStreamConnecting = function () {
return $scope.reStreamerData.states.repeatToLocalNginx.type === 'connecting';
};
$scope.nginxRepeatStreamConnected = function () {
return $scope.reStreamerData.states.repeatToLocalNginx.type === 'connected';
};
$scope.nginxRepeatStreamError = function () {
return $scope.reStreamerData.states.repeatToLocalNginx.type === 'error';
};
$scope.optionalOutputConnecting = function () {
return $scope.reStreamerData.states.repeatToOptionalOutput.type === 'connecting';
};
$scope.optionalOutputConnected = function () {
return $scope.reStreamerData.states.repeatToOptionalOutput.type === 'connected';
};
$scope.optionalOutputError = function () {
return $scope.reStreamerData.states.repeatToOptionalOutput.type === 'error';
};
$scope.openPlayer = function () {
if (player === null) {
initClappr();
}
$('#player-modal').modal('show');
$('#player-modal').on('hide.bs.modal', function (e) {
player.stop();
$('#player-modal').off('hide.bs.modal');
$('#player-modal').modal('hide');
return e.preventDefault();
});
};
/*
* Configure Websockets
*/
ws.emit('getVersion');
// check states of hls and rtmp stream
ws.emit('checkStates');
// check for app updates
ws.emit('checkForAppUpdates');
// prohibit double binding of events
if (!setup) {
/**
* test websockets connection (should print below message to browser console if it works)
*/
ws.on('version', function (version) {
$rootScope.version = version;
window.Logger.log('INFO', 'Datarhei ' + version + ' websockets connected');
});
ws.on('updateProgress', function (progresses) {
$scope.reStreamerData.progresses = progresses;
});
ws.on('publicIp', function (publicIp) {
$rootScope.publicIp = publicIp;
});
ws.on('updateStreamData', function (reStreamerData) {
$scope.reStreamerData = reStreamerData;
if ($scope.showStopButton('repeatToOptionalOutput')) {
// checkbox
$scope.activateOptionalOutput = true;
}
});
ws.on('checkForAppUpdatesResult', function (result) {
$rootScope.checkForAppUpdatesResult = result;
});
}
$scope.startStream = function (streamType) {
const rtmp_regex = /^(?:rtmp:\/\/|rtsp:\/\/)(?:(?:[^:])+:(?:[^@])+@)?(?:(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}))(:?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?(?:\/.*)?/;
var optionalOutput = false;
if ($scope.activateOptionalOutput === true) {
optionalOutput = $scope.reStreamerData.addresses.optionalOutputAddress;
}
if (streamType === 'repeatToOptionalOutput'){
$scope.optionalOutputInputInvalid = !rtmp_regex.test(optionalOutput);
if ($scope.optionalOutputInputInvalid ) {
return;
}
}else {
$scope.nginxRepeatStreamInputInvalid = !rtmp_regex.test($scope.reStreamerData.addresses.srcAddress);
if ($scope.nginxRepeatStreamInputInvalid ) {
return;
}
}
ws.emit('startStream', {
src: $scope.reStreamerData.addresses.srcAddress,
streamType: streamType,
optionalOutput: optionalOutput
});
};
$scope.stopStream = function (streamType) {
ws.emit('stopStream', streamType);
};
} ]);

View File

@@ -1,71 +0,0 @@
/**
* @file Service to handle websocket connections and events
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
/* jslint browser: true */
class WebsocketsService {
/*
* construct the Websockets Service
* @param $rootScope
* @param loggerService
*/
constructor ($rootScope, loggerService) {
this.$rootScope = $rootScope;
this.loggerService = loggerService;
this.socket = io.connect();
this.loggerService.websockets_namespace('websockets connected');
}
/*
* emit an event to socket
* @param event
* @param data
* @returns {WebsocketsService}
*/
emit (event, data) {
this.loggerService.websockets_out(`emit event "${event}"`);
this.socket.emit(event, data);
return this;
}
/*
* react on an event to socket with callback
* @param event
* @param {function} callback
* @returns {WebsocketsService}
*/
on (event, callback) {
this.loggerService.websockets_in(`got event "${event}"`);
var self = this;
this.socket.on(event, function () {
var args = arguments;
self.$rootScope.$apply(function () {
callback.apply(null, args);
});
});
return this;
}
/*
* disable an event on socket
* @param event
* @param callback
*/
off (event, callback) {
this.socket.removeListener(event, callback);
}
}
/*
* connect service to angular.js
*/
window.app.factory('ws', [ '$rootScope', 'loggerService', function ($rootScope, loggerService) {
return new WebsocketsService($rootScope, loggerService);
} ]);

View File

@@ -1,19 +0,0 @@
/**
* @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org
* @license Apache-2.0
*/
window.Logger = {
level: {
INFO: 'color: #0000FF; font-weight: bold',
DEBUG: 'color: #AABBCC; font-weight: bold',
ERROR: 'color: #FF0011d; font-weight: bold',
WEBSOCKETS_IN: 'color: #00BFFF; font-weight: bold',
WEBSOCKETS_OUT: 'color: #00BF00; font-weight: bold',
WEBSOCKETS_NAMESPACE: 'color: #00BF00; font-weight: bold'
},
log: function (level, message) {
console.log('%c ' +'[' + level + '] ' + message, this.level[level]);
}
};

View File

@@ -1,11 +0,0 @@
<form action="/login" method="POST">
<div class="form-group ng-scope">
<input type="text" class="form-control input ng-pristine ng-untouched ng-valid" name="user" id="input_username" placeholder="{{'login_username' | translate}}">
</div>
<div class="form-group ng-scope">
<input type="password" class="form-control input ng-pristine ng-untouched ng-valid" name="pass" id="input_password" placeholder="{{'login_password' | translate}}">
</div>
<div class="text-right">
<button type="submit" class="btn btn-success ng-binding ng-scope">{{'login_btn' | translate}}</button>
</div>
</form>

View File

@@ -1,61 +0,0 @@
<!--
Repeat to local nginx
-->
<div class="form-group">
<label>{{'input_titel' | translate}} <a href="https://datarhei.github.io/restreamer/docs/references-rtmp-rtsp-video-source.html", target="_blank"><span class="glyphicon glyphicon-question-sign" style="font-size: 16px;" aria-hidden="true"></span></a></label>
<input type="text" class="form-control input" id="input_uri" placeholder="{{'input_example' | translate}}" ng-model="reStreamerData.addresses.srcAddress" ng-disabled="nginxRepeatStreamConnecting() || nginxRepeatStreamConnected()">
</div>
<div class="jumbotron progress-bar-info progress-bar-striped" ng-if="nginxRepeatStreamConnecting()">{{'process_init' | translate}}</div>
<div class="jumbotron progress-bar-success progress-bar-striped" ng-if="nginxRepeatStreamConnected()">
{{'process_success' | translate}}
<div ng-if="reStreamerData.progresses.repeatToLocalNginx.currentFps > 0">
<span class="ffmpeg-progress fps">{{reStreamerData.progresses.repeatToLocalNginx.currentFps}}fps </span>
/
<span class="ffmpeg-progress kbps">{{reStreamerData.progresses.repeatToLocalNginx.currentKbps}}Kb/s</span>
</div>
</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-show="nginxRepeatStreamInputInvalid">{{'process_input_invalid' | translate}}</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-if="nginxRepeatStreamError()">{{'process_failed' | translate}} {{'process_failed_retry' | translate}} {{reStreamerData.progresses.repeatToLocalNginx.retryCount}}/{{reStreamerData.progresses.repeatToLocalNginx.retryMax}}</div>
<div class="form group" >
<p class="player-link" ng-if="nginxRepeatStreamConnected()">
<a href="#" style="text-decoration:underline" ng-click="openPlayer()">
{{'player_link_titel' | translate}}
</a>
</p>
<div class="text-right">
<button type="button" class="btn btn-success" ng-if="showStartButton('repeatToLocalNginx')" ng-click="startStream('repeatToLocalNginx')">{{'button_start' | translate}}</button>
<button type="button" class="btn btn-danger" ng-if="showStopButton('repeatToLocalNginx')" ng-click="stopStream('repeatToLocalNginx')">{{'button_stop' | translate}}</button>
</div>
</div>
<hr/>
<!--
Repeat to optional Output
-->
<h4>Optional:</h4>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="activateOptionalOutput"> {{'output_optional' | translate}} <a href="https://datarhei.github.io/restreamer/docs/references-external-rtmp-streaming-server.html", target="_blank"><span class="glyphicon glyphicon-question-sign" style="font-size: 14px;" aria-hidden="true"></span></a>
</label>
</div>
<div>
<div class="form-group" ng-show="activateOptionalOutput === true">
<input type="text" class="form-control input" id="output_optional_uri" placeholder="{{'output_optional_example' | translate}}" ng-model="reStreamerData.addresses.optionalOutputAddress" ng-disabled="optionalOutputConnected()">
</div>
</div>
<div class="jumbotron progress-bar-info progress-bar-striped" ng-if="optionalOutputConnecting()">{{'process_init' | translate}}</div>
<div class="jumbotron progress-bar-success progress-bar-striped" ng-if="optionalOutputConnected()">
{{'process_success' | translate}}
<div ng-if="reStreamerData.progresses.repeatToOptionalOutput.currentFps > 0">
<span class="ffmpeg-progress fps">{{reStreamerData.progresses.repeatToOptionalOutput.currentFps}}fps </span>
/
<span class="ffmpeg-progress kbps">{{reStreamerData.progresses.repeatToOptionalOutput.currentKbps}}Kb/s</span>
</div>
</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-show="optionalOutputInputInvalid">{{'process_input_invalid' | translate}}</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-if="optionalOutputError()">{{'process_failed' | translate}} {{'process_failed_retry' | translate}} {{reStreamerData.progresses.repeatToOptionalOutput.retryCount}}/{{reStreamerData.progresses.repeatToOptionalOutput.retryMax}}</div>
<div class="text-right">
<div class="btn btn-danger" ng-click="stopStream('repeatToOptionalOutput')" ng-if="showStopButton('repeatToOptionalOutput')">{{'button_stop' | translate}}</div>
<div ng-show="activateOptionalOutput === true">
<div class="btn btn-success" ng-click="startStream('repeatToOptionalOutput')" ng-if="showStartButton('repeatToOptionalOutput')">{{'button_start' | translate}}</div>
</div>
</div>