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

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

View File

@@ -10,6 +10,7 @@ indent_size = 4
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
spaces_around_brackets = outside
[*.md] [*.md]
trim_trailing_whitespace = false 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

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

4
.gitignore vendored
View File

@@ -63,6 +63,10 @@ bin
# Project files # # Project files #
################# #################
static/webserver/public/libs/** static/webserver/public/libs/**
src/webserver/public/libs/**
src/webserver/public/dist/
db/** db/**
heapdump heapdump
*.map *.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 ## 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 * 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> MAINTAINER datarhei <info@datarhei.org>
ENV FFMPEG_VERSION 2.8.5 ENV FFMPEG_VERSION 2.8.6
ENV YASM_VERSION 1.3.0 ENV YASM_VERSION 1.3.0
ENV LAME_VERSION 3_99_5 ENV LAME_VERSION 3_99_5
ENV NGINX_VERSION 1.9.9 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 SRC "/usr/local"
ENV LD_LIBRARY_PATH ${SRC}/lib ENV LD_LIBRARY_PATH "${SRC}/lib"
ENV PKG_CONFIG_PATH ${SRC}/lib/pkgconfig 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/* && \ RUN rm -rf /var/lib/apt/lists/* && \
apt-get update && \ 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 # yasm
RUN DIR=$(mktemp -d) && cd ${DIR} && \ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz && \ curl -LOks "https://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz" && \
tar xzvf yasm-${YASM_VERSION}.tar.gz && \ tar xzvf "yasm-${YASM_VERSION}.tar.gz" && \
cd yasm-${YASM_VERSION} && \ cd "yasm-${YASM_VERSION}" && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" && \ ./configure \
make && \ --prefix="${SRC}" \
--bindir="${SRC}/bin" && \
make -j"$(nproc)" && \
make install && \ make install && \
make distclean && \ make distclean && \
rm -rf ${DIR} rm -rf "${DIR}"
# x264 # x264
RUN DIR=$(mktemp -d) && cd ${DIR} && \ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
git clone --depth 1 git://git.videolan.org/x264 && \ git clone --depth 1 "git://git.videolan.org/x264" && \
cd x264 && \ cd x264 && \
./configure --prefix="$SRC" --bindir="${SRC}/bin" --enable-static && \ ./configure \
make && \ --prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-static \
--disable-cli && \
make -j"$(nproc)" && \
make install && \ make install && \
make distclean && \ make distclean && \
rm -rf ${DIR} rm -rf "${DIR}"
# libmp3lame # libmp3lame
RUN DIR=$(mktemp -d) && cd ${DIR} && \ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz && \ curl -LOks "https://github.com/rbrito/lame/archive/RELEASE__${LAME_VERSION}.tar.gz" && \
tar xzvf RELEASE__${LAME_VERSION}.tar.gz && \ tar xzvf "RELEASE__${LAME_VERSION}.tar.gz" && \
cd lame-RELEASE__${LAME_VERSION} && \ cd "lame-RELEASE__${LAME_VERSION}" && \
./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --enable-nasm && \ ./configure \
make && \ --prefix="${SRC}" \
--bindir="${SRC}/bin" \
--enable-nasm \
--disable-shared && \
make -j"$(nproc)" && \
make install && \ make install && \
make distclean&& \ make distclean && \
rm -rf ${DIR} rm -rf "${DIR}"
# ffmpeg # ffmpeg
RUN DIR=$(mktemp -d) && cd ${DIR} && \ # patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
curl -LOks https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
tar xzvf ffmpeg-${FFMPEG_VERSION}.tar.gz && \ curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
cd ffmpeg-${FFMPEG_VERSION} && \ tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" \ curl -Lks "https://github.com/FFmpeg/FFmpeg/commit/1c7e2cf9d33968375ee4025d2279c937e147dae2.patch" | patch -p1 && \
--extra-libs=-ldl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-gpl \ cd "ffmpeg-${FFMPEG_VERSION}" && \
--enable-postproc --enable-nonfree --enable-avresample --disable-debug --enable-small --enable-openssl \ ./configure \
--disable-doc --disable-ffserver && \ --prefix="${SRC}" \
make && \ --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 install && \
make distclean && \ make distclean && \
hash -r && \ hash -r && \
cd tools && \ cd tools && \
make qt-faststart && \ make qt-faststart && \
cp qt-faststart ${SRC}/bin && \ cp qt-faststart "${SRC}/bin" && \
rm -rf ${DIR} rm -rf "${DIR}"
RUN echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf RUN echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf"
RUN ffmpeg -buildconf RUN ffmpeg -buildconf
# nginx-rtmp # nginx-rtmp
RUN DIR=$(mktemp -d) && cd ${DIR} && \ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz && \ curl -LOks "https://github.com/nginx/nginx/archive/release-${NGINX_VERSION}.tar.gz" && \
tar xzvf 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 && \ 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 && \ tar xzvf "v${NGINX_RTMP_VERSION}.tar.gz" && \
cd nginx-release-${NGINX_VERSION} && \ cd "nginx-release-${NGINX_VERSION}" && \
auto/configure --with-http_ssl_module --add-module=../nginx-rtmp-module-${NGINX_RTMP_VERSION} && \ auto/configure \
make && \ --with-http_ssl_module \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \
make install && \ make install && \
rm -rf ${DIR} rm -rf "${DIR}"
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \ RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \
apt-get install -y --force-yes git && \
rm -rf /tmp/* rm -rf /tmp/*
COPY . /restreamer COPY . /restreamer
WORKDIR /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 && \ npm install && \
grunt build && \ grunt build && \
npm prune --production npm prune --production && \
npm cache clean && \
bower cache clean --allow-root
ENV RESTREAMER_USERNAME admin ENV RS_USERNAME admin
ENV RESTREAMER_PASSWORD datarhei ENV RS_PASSWORD datarhei
EXPOSE 8080 EXPOSE 8080
VOLUME ["/restreamer/db"] VOLUME ["/restreamer/db"]

View File

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

View File

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

View File

@@ -1,30 +1,46 @@
#Restreamer #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. 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 ##Features
- User-Interface including login-security - User-Interface including login-security
- JSON / HTTP-API - 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://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://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 - <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 ## Upcomming releases
- RC6 (tba)
- RC7 (tba)
## Roadmap
- optimizing FFmpeg handling
- backend refactoring
- full REST API
- security improvements
##Documentation ##Documentation
Documentation is available on [Datarhei/Restreamer GitHub pages](https://datarhei.github.io/restreamer/). 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. 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). More additional informations about streaming, cameras and so on you can find in our [Wiki](https://datarhei.github.com/restreamer/wiki).
##Help / Bugs ##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>. 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. 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 ##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). 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 ##Copyright
Code released under the [Apache license](LICENSE). Images are copyrighted by datarhei.org Code released under the [Apache license](LICENSE). Images are copyrighted by datarhei.org

View File

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

View File

@@ -21,6 +21,16 @@
"optionalOutputAddress" "optionalOutputAddress"
] ]
}, },
"options": {
"id": "http://jsonschema.net/options",
"type": "object",
"properties": {
"rtspTcp": {
"id": "http://jsonschema.net/options/rtspTcp",
"type": "boolean"
}
}
},
"states": { "states": {
"id": "http://jsonschema.net/states", "id": "http://jsonschema.net/states",
"type": "object", "type": "object",
@@ -64,6 +74,7 @@
}, },
"required": [ "required": [
"addresses", "addresses",
"options",
"states", "states",
"userActions" "userActions"
] ]

View File

@@ -7,23 +7,21 @@
}, },
"ffmpeg": { "ffmpeg": {
"options": { "options": {
"native_h264":[ "native_h264": [
"-c copy", "-codec copy",
"-map_metadata -1", "-map_metadata -1",
"-metadata application=datarhei/Restreamer", "-metadata application=datarhei/Restreamer",
"-metadata server=NGINX-RTMP", "-metadata server=NGINX-RTMP",
"-f flv" "-f flv"
], ],
"native_h264_soundless_aac":[ "native_h264_soundless_aac": [
"-ar 44100", "-f lavfi",
"-ac 2", "-i aevalsrc=0",
"-acodec pcm_s16le", "-vcodec copy",
"-f s16le",
"-ac 2",
"-i /dev/zero",
"-c:v copy",
"-acodec aac", "-acodec aac",
"-ab 128k", "-map 0:0",
"-map 1:0",
"-shortest",
"-map_metadata -1", "-map_metadata -1",
"-metadata application=datarhei/Restreamer", "-metadata application=datarhei/Restreamer",
"-metadata server=NGINX-RTMP", "-metadata server=NGINX-RTMP",
@@ -43,5 +41,71 @@
"rtmp_port": "1935", "rtmp_port": "1935",
"rtmp_hls_path": "/hls/" "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 { application live {
live on; live on;
meta copy; meta copy;
allow publish 127.0.0.1;
deny publish all;
} }
application hls { application hls {
live on; live on;
hls on; hls on;
hls_type live;
hls_playlist_length 60s; hls_playlist_length 60s;
hls_fragment 2s; hls_fragment 2s;
hls_path /tmp/hls; hls_path /tmp/hls;
meta copy;
allow publish 127.0.0.1;
deny publish all;
} }
} }
} }
@@ -30,6 +26,12 @@ http {
tcp_nopush on; tcp_nopush on;
server { server {
listen 8080; 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 / { location / {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -37,20 +39,6 @@ http {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; 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 { location /hls {
types { types {
application/vnd.apple.mpegurl m3u8; application/vnd.apple.mpegurl m3u8;

View File

@@ -1,44 +1,46 @@
'use strict'; 'use strict';
module.exports = function(grunt) { // path to store the transpiled es6 files
// files for this project const transpiledPath = "src/webserver/transpiled/";
var files = {
compiledFrontendJS: ['bin/webserver/public/scripts/**/*.js', 'bin/executors/**/gui/js/*.js'], const files = {
es6Src: ['**/*.js'], //workaround to keep correct order
stylesheets: ['static/webserver/public/css/*.css'] 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 // Project Configuration
grunt.initConfig({ 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
*/ */
@@ -46,14 +48,11 @@ module.exports = function(grunt) {
start: { start: {
command: 'npm start' command: 'npm start'
}, },
removeOldBinFolder: { removeTempTranspilingFolder: {
command: 'rm -Rf bin/' command: `rm -Rf ${transpiledPath}`
}, },
createBinFolder: { createTempTranspilingFolder: {
command: 'mkdir bin/' command: `mkdir ${transpiledPath}`
},
copyStatics: {
command: 'cp -R static/* bin'
}, },
bower: { bower: {
command: 'bower install --allow-root' command: 'bower install --allow-root'
@@ -63,7 +62,7 @@ module.exports = function(grunt) {
}, },
//temp workaround - https://github.com/clappr/clappr/issues/709 //temp workaround - https://github.com/clappr/clappr/issues/709
clappr: { 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'
} }
}, },
@@ -80,8 +79,8 @@ module.exports = function(grunt) {
{ {
expand: true, expand: true,
cwd: 'src/', cwd: 'src/',
src:'<%= es6Src %>', src: '<%= es6Src %>',
dest: 'bin/' dest: transpiledPath
} }
] ]
} }
@@ -105,7 +104,7 @@ module.exports = function(grunt) {
csslintrc: '.csslintrc' csslintrc: '.csslintrc'
}, },
all: { all: {
src: ['static/webserver/public/css/*.css'] src: ['src/webserver/public/css/*.css']
} }
}, },
@@ -118,7 +117,7 @@ module.exports = function(grunt) {
mangle: true mangle: true
}, },
files: { 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'
} }
} }
}, },
@@ -129,7 +128,7 @@ module.exports = function(grunt) {
cssmin: { cssmin: {
combine: { combine: {
files: { files: {
'bin/webserver/public/css/restreamer.min.css': '<%= stylesheets %>' 'src/webserver/public/css/restreamer.min.css': '<%= stylesheets %>'
} }
} }
}, },
@@ -140,7 +139,7 @@ module.exports = function(grunt) {
ngAnnotate: { ngAnnotate: {
production: { production: {
files: { files: {
'bin/webserver/public/dist/application.js': '<%= compiledFrontendJS %>' 'src/webserver/public/dist/application.js': '<%= transpiledFrontendJs %>'
} }
} }
} }
@@ -150,9 +149,9 @@ module.exports = function(grunt) {
Load NPM tasks Load NPM tasks
*/ */
require('load-grunt-tasks')(grunt); 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('es6Src', files.es6Src);
grunt.config.set('compiledFrontendJS', files.compiledFrontendJS); grunt.config.set('transpiledFrontendJs', files.transpiledFrontendJs);
grunt.config.set('stylesheets', files.stylesheets); grunt.config.set('stylesheets', files.stylesheets);
}); });
grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-shell');
@@ -163,8 +162,8 @@ module.exports = function(grunt) {
*/ */
// lint // lint
grunt.registerTask('lint', ['csslint', 'shell:eslint']); grunt.registerTask('lint', ['csslint', 'shell:eslint']);
// clear old bin folder and create new one // clear old transpile folder and create new one
grunt.registerTask('clearOldBuild', ['shell:removeOldBinFolder', 'shell:createBinFolder']); grunt.registerTask('clearOldBuild', ['shell:removeTempTranspilingFolder', 'shell:createTempTranspilingFolder']);
// install frontendlibraries (atm through bower) // install frontendlibraries (atm through bower)
grunt.registerTask('installFrontendLibraries', ['shell:bower', 'shell:clappr']); grunt.registerTask('installFrontendLibraries', ['shell:bower', 'shell:clappr']);
// minify the frontend files // minify the frontend files
@@ -173,17 +172,16 @@ module.exports = function(grunt) {
/* /*
Build Tasks Build Tasks
*/ */
grunt.registerTask('build', ['loadConfig','clearOldBuild', 'shell:copyStatics', 'babel', 'minifyFrontendFiles', 'installFrontendLibraries']); grunt.registerTask('build', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles', 'installFrontendLibraries', 'shell:removeTempTranspilingFolder']);
grunt.registerTask('compile-code', ['loadConfig','babel', 'shell:copyStatics', 'minifyFrontendFiles']);
grunt.registerTask('copy-statics', ['loadConfig', 'shell:copyStatics']); /*
Just Compile
*/
grunt.registerTask('compile', ['loadConfig', 'clearOldBuild', 'babel', 'minifyFrontendFiles']);
/* /*
Run Tasks Run Tasks
*/ */
// run current build in /bin
grunt.registerTask('run', ['shell:start']); 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,23 +1,26 @@
{ {
"name": "Restreamer", "name": "Restreamer",
"version": "0.1.0-RC5", "version": "0.1.0-RC6",
"description": "Allows you to do h.264 real-time video streaming on your website without a streaming provider", "description": "Allows you to do h.264 real-time video streaming on your website without a streaming provider",
"author": "datarhei.org", "author": "datarhei.org",
"repository": {
"type": "git",
"url": "git://github.com/datarhei/restreamer.git"
},
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"start": "node ./bin/start" "start": "node ./src/start"
}, },
"dependencies": { "dependencies": {
"body-parser": "1.14.2", "body-parser": "1.15.0",
"compression": "~1.6.0", "compression": "~1.6.1",
"connect-flash": "^0.1.1",
"cookie-parser": "1.4.1", "cookie-parser": "1.4.1",
"express": "4.13.4", "express": "4.13.4",
"express-session": "^1.12.1", "express-session": "^1.13.0",
"fluent-ffmpeg": "git://github.com/datarhei/node-fluent-ffmpeg", "fluent-ffmpeg": "git://github.com/datarhei/node-fluent-ffmpeg",
"jsonschema": "^1.0.2", "jsonschema": "^1.1.0",
"moment-timezone": "^0.5.0", "moment-timezone": "^0.5.0",
"node-json-db": "^0.5.1", "node-json-db": "git://github.com/andrew-shulgin/node-json-db",
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"ps-find": "^1.1.0", "ps-find": "^1.1.0",
@@ -25,16 +28,16 @@
"socket.io": "1.4.5" "socket.io": "1.4.5"
}, },
"devDependencies": { "devDependencies": {
"babel-preset-es2015": "^6.1.2", "eslint": "2.3.0",
"eslint": "^2.0.0-beta.3", "babel-preset-es2015": "^6.5.0",
"grunt": "^0.4.5", "grunt": "0.4.5",
"grunt-babel": "^6.0.0", "grunt-babel": "^6.0.0",
"grunt-contrib-csslint": "^0.5.0", "grunt-contrib-csslint": "^1.0.0",
"grunt-contrib-cssmin": "~0.14.0", "grunt-contrib-cssmin": "~1.0.0",
"grunt-contrib-uglify": "~0.11.0", "grunt-contrib-uglify": "~1.0.0",
"grunt-contrib-watch": "^0.6.1", "grunt-contrib-watch": "^0.6.1",
"grunt-ng-annotate": "~1.0.1", "grunt-ng-annotate": "~1.0.1",
"grunt-shell": "^1.1.2", "grunt-shell": "1.2.1",
"load-grunt-tasks": "~3.4.0" "load-grunt-tasks": "~3.4.0"
} }
} }

4
run.sh
View File

@@ -15,7 +15,7 @@ then
sleep 5 sleep 5
fi fi
done 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" ]; elif [ "${MODE}" == "USBCAM" ];
then then
apt-get update && apt-get install -y v4l-utils libv4l-0 apt-get update && apt-get install -y v4l-utils libv4l-0
@@ -30,7 +30,7 @@ then
sleep 5 sleep 5
fi fi
done 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 else
npm start npm start
fi fi

View File

@@ -4,24 +4,51 @@
* @copyright 2015 datarhei.org * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
const logger = require('./Logger')('EnvVar');
/** /**
* Class for environment variables with default values * Class for environment variables with default values
*/ */
class EnvVar { class EnvVar {
static init (config) {
var killProcess = false;
/** for (let envVar of config.envVars) {
* constructs an envvar if (typeof process.env[envVar.alias] !== 'undefined') {
* @param {string} name process.env[envVar.name] = process.env[envVar.alias];
* @param {string} required delete process.env[envVar.alias];
* @param {string} defaultValue }
* @param {string} description
*/ if (typeof process.env[envVar.name] !== 'undefined') {
constructor (name, required, defaultValue, description) { logger.info(`ENV "${envVar.name} = ${process.env[envVar.name]}"`, envVar.description);
this.name = name; } else if (envVar.required === true) {
this.required = required; logger.error(`No value set for env "${envVar.name}", but it is required`);
this.defaultValue = defaultValue; killProcess = true;
this.description = description; } 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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
const moment = require('moment-timezone'); const moment = require('moment-timezone');
const LEVEL_MUTE = 0;
const LEVEL_ERROR = 1; const LEVEL_ERROR = 1;
const LEVEL_WARN = 2; const LEVEL_WARN = 2;
const LEVEL_INFO = 3; const LEVEL_INFO = 3;
const LEVEL_DEBUG = 4; const LEVEL_DEBUG = 4;
// set default timezone to use the timezone before the default values are // set default timezone to use the timezone before the default values are
process.env.TIMEZONE = process.env.TIMEZONE ? process.env.TIMEZONE : 'Europe/Berlin'; // @todo: it is really ugly and wrong to log with hardcoded timezone before environment is read
if (typeof process.env.LOGGER_LEVEL === 'undefined') { process.env.RS_TIMEZONE = process.env.RS_TIMEZONE || 'Europe/Berlin';
process.env.LOGGER_LEVEL = '3'; process.env.RS_LOGGER_LEVEL = process.env.RS_LOGGER_LEVEL || 3;
}
/** /**
* Class for logger * Class for logger
@@ -27,7 +28,7 @@ class Logger {
* @returns {boolean} * @returns {boolean}
*/ */
static isMuted () { 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) * @param {string} context context of the log message (classname.methodname)
*/ */
constructor (context) { constructor (context) {
process.env.RS_LOGGER_LEVEL = process.env.RS_LOGGER_LEVEL || LEVEL_INFO;
this.context = context; this.context = context;
} }
@@ -45,76 +47,100 @@ class Logger {
* @param {string} type * @param {string} type
*/ */
stdout (message, context, 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()) { if (Logger.isMuted()) {
return; return;
} }
if (context === false) { if (context) {
context = ''; process.stdout.write(`[${time}] [${type}] ${message} [${loggerContext}]\n`);
} else { } 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 * print an info message if LOG_LEVEL >= LEVEL_INFO
* @param {string} message * @param {string} message
* @param {string} context * @param {string=} context
* @param {boolean=} alertGui
*/ */
info (message, context, alertGui) { info (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') { if (typeof context === 'undefined') {
context = this.context; loggerContext = this.context;
} }
if (typeof alertGui === 'undefined') { 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 * print a warning message if LOG_LEVEL >= LEVEL_WARN
* @param {string} message * @param {string} message
* @param {string} context * @param {string=} context
* @param {boolean=} alertGui
*/ */
warn (message, context, alertGui) { warn (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') { if (typeof context === 'undefined') {
context = this.context; loggerContext = this.context;
} }
if (typeof alertGui === 'undefined') { 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 * print a debug message if LOG_LEVEL >= LEVEL_DEBUG
* @param {string} message * @param {string} message
* @param {string} context * @param {string=} context
* @param {boolean=} alertGui
*/ */
debug (message, context, alertGui) { debug (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') { if (typeof context === 'undefined') {
context = this.context; loggerContext = this.context;
} }
if (typeof alertGui === 'undefined') { 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 * print a debug message if LOG_LEVEL >= LEVEL_ERROR
* sends a string to * sends a string to
* @param {string} message * @param {string} message
* @param {string} context * @param {string=} context
* @param {boolean=} alertGui
*/ */
error (message, context, alertGui) { error (message, context, alertGui) {
var loggerContext = context;
var loggerAlertGui = alertGui;
if (typeof context === 'undefined') { if (typeof context === 'undefined') {
context = this.context; loggerContext = this.context;
} }
if (typeof alertGui === 'undefined') { 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_ERROR = LEVEL_ERROR;
Logger.LEVEL_WARN = LEVEL_WARN; Logger.LEVEL_WARN = LEVEL_WARN;
Logger.LEVEL_INFO = LEVEL_INFO; Logger.LEVEL_INFO = LEVEL_INFO;
Logger.LEVEL_DEBUG = LEVEL_DEBUG; Logger.LEVEL_DEBUG = LEVEL_DEBUG;
module.exports = (context)=>{ module.exports = (context) => {
return new Logger(context); return new Logger(context);
}; };

View File

@@ -4,6 +4,7 @@
* @copyright 2016 datarhei.org * @copyright 2016 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
const Q = require('q'); const Q = require('q');
const psFind = require('ps-find'); const psFind = require('ps-find');
@@ -11,7 +12,7 @@ const spawn = require('child_process').spawn;
const logger = require('./Logger')('Nginxrtmp'); 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 { class Nginxrtmp {
@@ -30,24 +31,19 @@ class Nginxrtmp {
* @returns {string} * @returns {string}
*/ */
init () { init () {
let promise = null; return this.getState()
.then((state) => {
promise = this.getState() switch (state) {
.then((state)=>{
switch(state) {
case 'not_running': case 'not_running':
this.start(); this.start();
break; break;
case 'running': case 'running':
this.logger.info('NGINX allready started...'); this.logger.info('NGINX already started');
break; break;
default: default:
throw new Error('state could not be detected'); throw new Error('NGINX state could not be detected');
} }
}); });
return promise;
} }
/** /**
@@ -86,15 +82,15 @@ class Nginxrtmp {
*/ */
bindNginxProcessEvents (process) { bindNginxProcessEvents (process) {
process.stdout.on('data', (data) => { 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) => { 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) => { 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(); this.start();
}); });
} }
@@ -110,26 +106,23 @@ class Nginxrtmp {
let deferred = Q.defer(); let deferred = Q.defer();
// delay the state detection if waiting for process is needed // delay the state detection if waiting for process is needed
Q Q.delay(delay)
.delay(delay)
.then(()=> { .then(()=> {
return Q.nfcall(psFind.find, nginxProcessString); return Q.nfcall(psFind.find, nginxProcessString);
}) })
.then(()=>{ .then(()=> {
state = 'running'; state = 'running';
}) })
.catch(()=> { // ps-find throws exception in case of 'not found' so we have to handle that
// ps-find throws exception in case of 'not found' so we have to handle that
.catch(()=>{
state = 'not_running'; state = 'not_running';
}) })
.finally(()=>{ .finally(()=> {
deferred.resolve(state); deferred.resolve(state);
}); });
return deferred.promise; return deferred.promise;
} }
} }
module.exports = (config)=>{ module.exports = (config)=> {
return new Nginxrtmp(config); return new Nginxrtmp(config);
}; };

View File

@@ -4,12 +4,16 @@
* @copyright 2015 datarhei.org * @copyright 2015 datarhei.org
* @license Apache-2.0 * @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 logger = require('./Logger')('Restreamer');
const WebsocketsController = require('./WebsocketController'); const WebsocketsController = require('./WebsocketsController');
const FfmpegCommand = require('fluent-ffmpeg'); const FfmpegCommand = require('fluent-ffmpeg');
const Q = require('q'); 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 * class Restreamer creates and manages streams through ffmpeg
@@ -31,9 +35,7 @@ class Restreamer {
* @returns {string} * @returns {string}
*/ */
static generateSnapshotPath () { static generateSnapshotPath () {
const path = require('path'); return path.join(global.__public, 'images', 'live.jpg');
return path.join(__dirname, '..', 'webserver', 'public', 'images', 'live.jpg');
} }
/** /**
@@ -41,17 +43,18 @@ class Restreamer {
* @param {boolean} firstSnapshot * @param {boolean} firstSnapshot
*/ */
static fetchSnapshot (firstSnapshot) { static fetchSnapshot (firstSnapshot) {
var command = null;
if (Restreamer.data.states.repeatToLocalNginx.type === 'connected' || firstSnapshot) { if (Restreamer.data.states.repeatToLocalNginx.type === 'connected' || firstSnapshot) {
var command = new FfmpegCommand(Restreamer.generateOutputHLSPath()); command = new FfmpegCommand(Restreamer.generateOutputHLSPath());
command.output(Restreamer.generateSnapshotPath()); command.output(Restreamer.generateSnapshotPath());
command.outputOption(config.ffmpeg.options.snapshot); command.outputOption(config.ffmpeg.options.snapshot);
command.on('error', (error)=> { command.on('error', (error)=> {
logger.error('Error on fetching snapshot: ' + error.toString()); logger.error('Error on fetching snapshot: ' + error.toString());
}); });
command.on('end', () =>{ command.on('end', () => {
logger.info('updated snapshot'); logger.info('updated snapshot');
Q.delay(process.env.SNAPSHOT_REFRESH_INTERVAL).then(function () { Q.delay(this.calculateSnapshotRefreshInterval()).then(() => {
Restreamer.fetchSnapshot(false); 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 * stop stream
* @param {string} processName * @param {string} processName
@@ -71,47 +89,54 @@ class Restreamer {
if (processHasBeenSpawned) { if (processHasBeenSpawned) {
Restreamer.data.processes[processName].kill(); Restreamer.data.processes[processName].kill();
Restreamer.data.processes[processName] = { Restreamer.data.processes[processName] = {
state: 'not_connected' 'state': 'not_connected'
}; };
} }
} }
/** /**
* restore the ffmpeg processes from jsondb (called on app start to restore ffmpeg processes * 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 () { static restoreFFMpegProcesses () {
var JsonDB = require('node-json-db');
var db = new JsonDB(config.jsondb, true, false); 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.addresses = db.getData('/addresses');
Restreamer.data.states = db.getData('/states'); Restreamer.data.states = db.getData('/states');
Restreamer.data.options = db.getData('/options');
Restreamer.data.userActions = db.getData('/userActions'); Restreamer.data.userActions = db.getData('/userActions');
// check if the srcAddress has been repeated to Local Nginx repeatToLocalNginxIsConnected = Restreamer.data.states.repeatToLocalNginx.type === 'connected';
var repeatToLocalNginxIsConnected = Restreamer.data.states.repeatToLocalNginx.type === 'connected'; repeatToLocalNginxIsConnecting = Restreamer.data.states.repeatToLocalNginx.type === 'connecting';
var repeatToLocalNginxIsConnecting = Restreamer.data.states.repeatToLocalNginx.type === 'connecting'; repeatToOptionalOutputIsConnected = Restreamer.data.states.repeatToOptionalOutput.type === 'connected';
var repeatToOptionalOutputIsConnected = Restreamer.data.states.repeatToOptionalOutput.type === 'connected'; repeatToOptionalOutputIsConnecting = Restreamer.data.states.repeatToOptionalOutput.type === 'connecting';
var repeatToOptionalOutputIsConnecting = Restreamer.data.states.repeatToOptionalOutput.type === 'connecting';
if (Restreamer.data.addresses.srcAddress && (!!repeatToLocalNginxIsConnected || !!repeatToLocalNginxIsConnecting)) { // check if the srcAddress has been repeated to Local Nginx
Restreamer.startStream(Restreamer.data.addresses.srcAddress, 'repeatToLocalNginx'); if (Restreamer.data.addresses.srcAddress &&
(repeatToLocalNginxIsConnected || repeatToLocalNginxIsConnecting)) {
Restreamer.startStream(
Restreamer.data.addresses.srcAddress,
'repeatToLocalNginx'
);
} }
if (Restreamer.data.addresses.optionalOutputAddress && (!!repeatToOptionalOutputIsConnected || !!repeatToOptionalOutputIsConnecting)) { if (Restreamer.data.addresses.optionalOutputAddress &&
Restreamer.startStream(Restreamer.data.addresses.srcAddress, 'repeatToOptionalOutput', Restreamer.data.addresses.optionalOutputAddress); (repeatToOptionalOutputIsConnected || repeatToOptionalOutputIsConnecting)) {
} Restreamer.startStream(
} Restreamer.data.addresses.srcAddress,
catch(error) { 'repeatToOptionalOutput',
logger.error('error restoring ffmpeg process: ' + error); Restreamer.data.addresses.optionalOutputAddress
);
} }
} }
/** /**
* write JSON file for persistency * write JSON file for persistence
*/ */
static writeToDB () { static writeToDB () {
var JsonDB = require('node-json-db');
var db = new JsonDB(config.jsondb, true, false); var db = new JsonDB(config.jsondb, true, false);
db.push('/', Restreamer.dataForJsonDb()); db.push('/', Restreamer.dataForJsonDb());
@@ -135,26 +160,32 @@ class Restreamer {
* add output to ffmpeg command * add output to ffmpeg command
* @param {FfmpegCommand} ffmpegCommand * @param {FfmpegCommand} ffmpegCommand
* @param {string} outputAddress * @param {string} outputAddress
* @return {promise} * @return {Promise}
*/ */
static addOutput (ffmpegCommand, outputAddress) { static addOutput (ffmpegCommand, outputAddress) {
ffmpegCommand.output(outputAddress); ffmpegCommand.output(outputAddress);
return Restreamer.appendOutputOptionFromConfig(ffmpegCommand); 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 * append the ffmpeg options of the config file to an output
* @param {FfmpegCommand} ffmpegCommand * @param {FfmpegCommand} ffmpegCommand
* @return {promise} * @return {Promise}
*/ */
static appendOutputOptionFromConfig (ffmpegCommand) { static appendOutputOptionFromConfig (ffmpegCommand) {
var deferred = Q.defer(); var deferred = Q.defer();
var ffmpegOptions = [];
ffmpegCommand.ffprobe(function (err, data) { ffmpegCommand.ffprobe((err, data) => {
if (err) { if (err) {
return deferred.reject(err); return deferred.reject(err);
} else { }
var ffmpegOptions;
if (data.streams.length > 1) { if (data.streams.length > 1) {
ffmpegOptions = config.ffmpeg.options.native_h264; ffmpegOptions = config.ffmpeg.options.native_h264;
@@ -167,7 +198,6 @@ class Restreamer {
ffmpegCommand.outputOption(option); ffmpegCommand.outputOption(option);
} }
return deferred.resolve(); return deferred.resolve();
}
}); });
return deferred.promise; return deferred.promise;
} }
@@ -176,14 +206,14 @@ class Restreamer {
* update the state of the stream * update the state of the stream
* @param {string} processName * @param {string} processName
* @param {string} state * @param {string} state
* @param {string} message * @param {string=} message
* @return {string} name of the new state * @return {string} name of the new state
*/ */
static updateState (processName, state, message) { static updateState (processName, state, message) {
logger.debug(`Update state of "${processName}" from state "${Restreamer.data.states[processName].type}" to state "${state}"`); logger.debug(`Update state of "${processName}" from state "${Restreamer.data.states[processName].type}" to state "${state}"`);
Restreamer.data.states[processName] = { Restreamer.data.states[processName] = {
type: state, 'type': state,
message: message 'message': message
}; };
Restreamer.writeToDB(); Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui(); 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} processName
* @param {string} action useraction * @param {string} action user action
* @return {string} name of the new user action * @return {string} name of the new user action
*/ */
static updateUserAction (processName, 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.data.userActions[processName] = action;
Restreamer.writeToDB(); Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui(); Restreamer.updateStreamDataOnGui();
return action; 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} src src-address of the ffmpeg stream
* @param {string} streamType repeatToOptionalOutput or repeatToLocalNginx * @param {string} streamType repeatToOptionalOutput or repeatToLocalNginx
* @param {string} optionalOutput address of the optional output * @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=} retryCounter current value of the retry counter (startStream retries automatically if anything fails)
*/ */
static startStream (src, streamType, optionalOutput, retryCounter) { 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}"`); logger.info(`Start stream "${streamType}"`);
// update the retry counter for the streamType // update the retry counter for the streamType
Restreamer.data.retryCounter[streamType].current = typeof retryCounter === 'undefined' ? 0 : retryCounter; 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} */ /** @var {Boolean} */
const repeatToLocalNginx = streamType === 'repeatToLocalNginx'; const repeatToLocalNginx = streamType === 'repeatToLocalNginx';
@@ -243,36 +283,42 @@ class Restreamer {
// repeat to local nginx server // repeat to local nginx server
if (repeatToLocalNginx) { if (repeatToLocalNginx) {
command = new FfmpegCommand(src, { command = new FfmpegCommand(src, {
outputLineLimit: 1 'outputLineLimit': 1
}); });
// add outputs to the ffmpeg stream // add outputs to the ffmpeg stream
addOutputPromise = Restreamer.addOutput(command, Restreamer.generateOutputHLSPath()) addOutputPromise = Restreamer.addOutput(command, Restreamer.generateOutputHLSPath()).catch((error) => {
.catch(function (error) {
logger.error(`Error adding one or more outputs: ${error.toString}`); logger.error(`Error adding one or more outputs: ${error.toString}`);
}); });
// repeat to optional output // repeat to optional output
} else if (repeatToOptionalOutput) { } else if (repeatToOptionalOutput) {
command = new FfmpegCommand(Restreamer.generateOutputHLSPath(), { command = new FfmpegCommand(Restreamer.generateOutputHLSPath(), {
outputLineLimit: 1 'outputLineLimit': 1
}); });
Restreamer.data.addresses.optionalOutputAddress = optionalOutput; Restreamer.data.addresses.optionalOutputAddress = optionalOutput;
addOutputPromise = Restreamer.addOutput(command, optionalOutput); addOutputPromise = Restreamer.addOutput(command, optionalOutput);
} }
// after adding outputs, define events on the new FFmpeg stream // 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 command
// stream started
/* .on('start', (commandLine) => {
* STREAMING STARTED
*/
.on('start', function (commandLine) {
if (Restreamer.data.userActions[streamType] === 'stop') { if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping on "start" event of FFmpeg command since "stopped" has been clicked'); logger.debug('Skipping on "start" event of FFmpeg command since "stopped" has been clicked');
return; return;
} else { }
logger.debug(`FFmpeg spawned: ${commandLine}`); logger.debug(`FFmpeg spawned: ${commandLine}`);
Restreamer.data.processes[streamType] = command; Restreamer.data.processes[streamType] = command;
@@ -280,13 +326,27 @@ class Restreamer {
if (repeatToLocalNginx) { if (repeatToLocalNginx) {
Restreamer.fetchSnapshot(true); Restreamer.fetchSnapshot(true);
} }
})
// 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
* ERROR HANDLER .on('error', (error) => {
*/
.on('error', (error)=>{
if (error.toString().indexOf('SIGKILL') > -1) { if (error.toString().indexOf('SIGKILL') > -1) {
Restreamer.updateState(streamType, 'disconnected'); Restreamer.updateState(streamType, 'disconnected');
logger.info(`FFmpeg streaming stopped for "${streamType}"`); logger.info(`FFmpeg streaming stopped for "${streamType}"`);
@@ -296,79 +356,47 @@ class Restreamer {
if (Restreamer.data.userActions[streamType] === 'stop') { if (Restreamer.data.userActions[streamType] === 'stop') {
logger.debug('Skipping retry since "stopped" has been clicked'); logger.debug('Skipping retry since "stopped" has been clicked');
return; return;
}
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(() => {
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 { } else {
Restreamer.updateState(streamType, 'error', error.toString()); 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 {
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);
});
} }
} }
}); });
/*
* 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); command.on('progress', progressMethod);
setInterval(()=>{ setInterval(()=> {
if (command.listeners('progress').length === 0) { if (command.listeners('progress').length === 0) {
command.on('progress', progressMethod); command.on('progress', progressMethod);
} }
}, 1000); }, 1000);
command.exec(); command.exec();
}).catch(function (error) { }).catch((error) => {
logger.error(`Error starting FFmpeg command: ${error.toString()}`); 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 () { static bindWebsocketEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', function (socket) { WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => {
socket.on('startStream', (options)=> { socket.on('startStream', (options)=> {
Restreamer.updateUserAction(options.streamType, 'start'); Restreamer.updateUserAction(options.streamType, 'start');
Restreamer.updateOptions(options.options);
Restreamer.startStream(options.src, options.streamType, options.optionalOutput); Restreamer.startStream(options.src, options.streamType, options.optionalOutput);
}); });
socket.on('stopStream', (streamType)=>{ socket.on('stopStream', (streamType)=> {
Restreamer.updateUserAction(streamType, 'stop'); Restreamer.updateUserAction(streamType, 'stop');
Restreamer.stopStream(streamType); Restreamer.stopStream(streamType);
}); });
socket.on('checkForAppUpdates', ()=>{ socket.on('checkForAppUpdates', ()=> {
const app = require('../webserver/app');
socket.emit('checkForAppUpdatesResult', app.get('updateAvailable')); socket.emit('checkForAppUpdatesResult', app.get('updateAvailable'));
}); });
socket.on('checkStates', Restreamer.updateStreamDataOnGui); socket.on('checkStates', Restreamer.updateStreamDataOnGui);
@@ -381,12 +409,13 @@ class Restreamer {
* @returns {object} * @returns {object}
*/ */
static extractDataOfStreams () { static extractDataOfStreams () {
var sData = {}; return {
sData.userActions = Restreamer.data.userActions; 'addresses': Restreamer.data.addresses,
sData.addresses = Restreamer.data.addresses; 'options': Restreamer.data.options,
sData.states = Restreamer.data.states; 'userActions': Restreamer.data.userActions,
sData.retryCounter = Restreamer.data.retryCounter; 'states': Restreamer.data.states,
return sData; 'retryCounter': Restreamer.data.retryCounter
};
} }
/** /**
@@ -394,61 +423,69 @@ class Restreamer {
* @return {object} * @return {object}
*/ */
static dataForJsonDb () { static dataForJsonDb () {
var dbData = {}; return {
dbData.addresses = Restreamer.data.addresses; 'addresses': Restreamer.data.addresses,
dbData.userActions = Restreamer.data.userActions; 'options': Restreamer.data.options,
dbData.states = Restreamer.data.states; 'userActions': Restreamer.data.userActions,
return dbData; 'states': Restreamer.data.states
};
} }
} }
/* /*
define data structure of Restreamer Data define data structure of Restreamer Data
*/ */
Restreamer.data = { Restreamer.data = {
retryCounter: { 'retryCounter': {
repeatToLocalNginx: { 'repeatToLocalNginx': {
current: 0, 'current': 0,
max: config.ffmpeg.monitor.retries 'max': config.ffmpeg.monitor.retries
}, },
repeatToOptionalOutput: { 'repeatToOptionalOutput': {
current: 0, 'current': 0,
max: config.ffmpeg.monitor.retries 'max': config.ffmpeg.monitor.retries
} }
}, },
states: { 'options': {
repeatToLocalNginx: { 'rtspTcp': false
type: 'disconnected',
message: ''
}, },
repeatToOptionalOutput: { 'states': {
type: 'disconnected', 'repeatToLocalNginx': {
message: '' 'type': 'disconnected',
'message': ''
},
'repeatToOptionalOutput': {
'type': 'disconnected',
'message': ''
} }
}, },
userActions: { 'userActions': {
repeatToLocalNginx: 'start', 'repeatToLocalNginx': 'start',
repeatToOptionalOutput: 'start' 'repeatToOptionalOutput': 'start'
}, },
processes: { 'processes': {
// overwritten with ffmpeg process if stream has been started // overwritten with ffmpeg process if stream has been started
repeatToLocalNginx: { 'repeatToLocalNginx': {
state: 'not_connected' 'state': 'not_connected'
}, },
// overwritten with ffmpeg process if stream has been started // overwritten with ffmpeg process if stream has been started
repeatToOptionalOutput: { 'repeatToOptionalOutput': {
state: 'not_connected' 'state': 'not_connected'
} }
}, },
progresses: { 'progresses': {
// overwritten with ffmpeg process if stream has been started // overwritten with ffmpeg process if stream has been started
repeatToLocalNginx: {}, 'repeatToLocalNginx': {},
// overwritten with ffmpeg process if stream has been started // overwritten with ffmpeg process if stream has been started
repeatToOptionalOutput: {} 'repeatToOptionalOutput': {}
}, },
addresses: { 'addresses': {
srcAddress: '', 'srcAddress': '',
optionalOutputAddress: '' '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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @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 * 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) * that websocket events are bound, 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 * @todo since currently the Restreamer is a single page application, there is no need to use different namespaces
*/ */
class WebsocketsController { class WebsocketsController {
@@ -21,10 +24,8 @@ class WebsocketsController {
* @param {object} data data to emit to the client event listener * @param {object} data data to emit to the client event listener
*/ */
static emitToNamespace (namespace, event, data) { static emitToNamespace (namespace, event, data) {
var app = require('../webserver/app'); app.get('websocketsReady').promise.then((io) => {
logger.debug(`websocket got event ${event} to namespace ${namespace}`, 'Websockets');
app.get('websocketsReady').promise.then(function (io) {
logger.debug('websocket got event ' + event + ' to namespace ' + namespace + '', 'Websockets');
io.of(namespace).emit(event, data); io.of(namespace).emit(event, data);
}); });
} }
@@ -35,29 +36,23 @@ class WebsocketsController {
* @param {function} callback * @param {function} callback
*/ */
static addOnConnectionEventToNamespace (namespace, callback) { static addOnConnectionEventToNamespace (namespace, callback) {
var app = require('../webserver/app'); app.get('websocketsReady').promise.then((io) => {
app.get('websocketsReady').promise.then(function (io) {
var nsp = io.of(namespace); var nsp = io.of(namespace);
nsp.on('connection', function (socket) { nsp.on('connection', (socket) => {
callback(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 () { static bindDefaultEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', function (socket) { WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => {
socket.on('getVersion', (options)=> { socket.on('getVersion', () => {
var packageJson = require('../../package.json');
socket.emit('version', packageJson.version); socket.emit('version', packageJson.version);
}); });
var app = require('../webserver/app');
socket.emit('publicIp', app.get('publicIp')); socket.emit('publicIp', app.get('publicIp'));
}); });
require('./Restreamer').bindWebsocketEvents(); 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 * @link https://github.com/datarhei/restreamer
* @copyright 2015 datarhei.org * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
const logger = require('./classes/Logger')('start'); 'use strict';
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');
const path = require('path'); const path = require('path');
/* global.__src = __dirname;
init simple_streamer with environments 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); 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('Restreamer v' + packageJson.version, false);
logger.info('', false); logger.info('', false);
logger.info('ENVIRONMENTS', false); logger.info('ENVIRONMENTS', false);
logger.info('More informations in our Docs', false); logger.info('More information in our Docs', false);
logger.info('', false); logger.info('', false);
// define environment variables // setup environment vars
var env_vars = []; EnvVar.init(config);
env_vars.push(new EnvVar('NODEJS_PORT', false, 3000, 'Webserver port of application')); // check for app updates
env_vars.push(new EnvVar('LOGGER_LEVEL', true, '3', 'Logger level to defined, what should be logged')); restreamerApp.checkForRestreamerUpdates();
env_vars.push(new EnvVar('TIMEZONE', true, 'Europe/Berlin', 'Set the timezone')); // Check for updates each 12 hours
env_vars.push(new EnvVar('SNAPSHOT_REFRESH_INTERVAL', false, 60000, 'Interval to create a new Snapshot')); setInterval(restreamerApp.checkForRestreamerUpdates, 12 * 3600 * 1000);
env_vars.push(new EnvVar('CREATE_HEAPDUMPS', false, 'false', 'Create Heapdumps of application'));
// manage all environments // add default websocket events, @todo this will be removed, when the new websocket workflow is implemented
var killProcess = false; WebsocketsController.bindDefaultEvents();
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
*/
// start the app
nginxrtmp.init() nginxrtmp.init()
.then(checkJsonDb) .then(()=> {
.then(startWebserver) return RestreamerData.checkJSONDb();
.then(checkJsonDb) })
.then(restoreFFMPEGProcesses) .then(()=> {
.then(getPublicIp) return restreamerApp.startWebserver();
.catch(function (error) { })
logger.error(`Error starting webserver and nginx for application: ${error}`); .then(() => {
setTimeout(()=> { return Q.fcall(Restreamer.restoreFFMpegProcesses);
process.exit(); })
}, 500); .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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
/*
* libraries
*/
// auth stuff // auth stuff
const passport = require('passport'); const passport = require('passport');
const flash = require('connect-flash'); const passportConfig = require('./config/passport');
// express // express
const express = require('express'); const express = require('express');
@@ -23,81 +21,143 @@ const https = require('https');
const path = require('path'); const path = require('path');
const Q = require('q'); const Q = require('q');
const crypto = require('crypto'); 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'); class RestreamerExpressApp {
const logger = require('../classes/Logger')('Webserver');
// 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 if (process.env.RS_NODE_ENV === 'dev') {
const app = express(); this.initDev();
} else {
this.initProd();
}
}
// generate random key /**
const secretKey = crypto.randomBytes(16).toString('hex'); * use sessions for the express app
*/
useSessions () {
this.app.use(session({
'secret': this.secretKey,
'resave': false,
'saveUninitialized': true // session secret
}));
}
app.use(session({ /**
secret: secretKey, * use passport auth
resave: false, */
useAuth () {
// add passport auth
this.app.use(passport.initialize());
// session secret // persistent login sessions
saveUninitialized: true})); this.app.use(passport.session());
// create promise for 'websockets ready' // add config to passport
app.set('websocketsReady', Q.defer()); passportConfig(passport);
}
// add passport auth
app.use(passport.initialize());
// use connect-flash for flash messages stored in session /**
app.use(flash()); * add automatic parsers for the body
*/
addParsers () {
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({'extended': true}));
this.app.use(cookieParser());
}
// persistent login sessions /**
app.use(passport.session()); * add content compression on responses
require('./config/passport')(passport); */
addCompression () {
this.app.use(compression());
}
// configure express app /**
app.use(bodyParser.json()); * add express logger
app.set('views', __dirname + '/views'); */
app.set('view engine', 'jade'); addExpressLogger () {
app.use(bodyParser.urlencoded({extended: true})); this.app.use('/', expressLogger);
app.use(cookieParser()); }
app.use(compression());
app.set('json spaces', 4);
require('./controllers/index')(app, passport); /**
app.use('/', expressLogger); * beautify json response
*/
beautifyJSONResponse () {
this.app.set('json spaces', 4);
}
// catch 404 and forward to error handler /**
app.use(function (req, res, next) { * 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); var err = new Error('Not Found ' + req.url);
err.status = 404; err.status = 404;
next(err); next(err);
}); });
}
// no stacktraces leaked to user /**
app.use(function (err, req, res, next) { * add ability for internal server errors
*/
add500ErrorHandling () {
this.app.use((err, req, res, next) => {
logger.error(err);
res.status(err.status || 500); res.status(err.status || 500);
res.send({ res.send({
message: err.message, 'message': err.message,
error: {} 'error': {}
}); });
}); });
}
var checkForAppUpdates = function () { /**
* check for app updates
*/
checkForRestreamerUpdates () {
const url = {'host': 'datarhei.org', 'path': '/apps.json'}; const url = {'host': 'datarhei.org', 'path': '/apps.json'};
logger.debug('Checking app for updates...'); logger.debug('Checking app for updates...');
https.get(url, function (response) { https.get(url, (response)=> {
if (response.statusCode === 200) { if (response.statusCode === 200) {
response.on('data', function (body) { response.on('data', (body)=> {
var updateCheck = JSON.parse(body); var updateCheck = JSON.parse(body);
var updateAvailable = false; var updateAvailable = false;
if (updateCheck.restreamer.version === packageJson.version) { if (updateCheck.restreamer.version === packageJson.version) {
updateAvailable = false; updateAvailable = false;
logger.debug(`Checking app for updates successful. Update is not available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`); logger.debug(`Checking app for updates successful. Update is not available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
@@ -105,19 +165,94 @@ var checkForAppUpdates = function () {
updateAvailable = updateCheck.restreamer.version; updateAvailable = updateCheck.restreamer.version;
logger.debug(`Checking app for updates successful. Update is available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.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'); logger.info('Checking app for updates successful');
app.set('updateAvailable', updateAvailable); this.app.set('updateAvailable', updateAvailable);
}); });
} else { } else {
logger.info('Update check failed', false); logger.info('Update check failed', false);
} }
}).on('error', function () { }).on('error', () => {
logger.info('Update check failed', false); logger.info('Update check failed', false);
}); });
}; }
// start interval to check for updates /**
checkForAppUpdates(); * get public ip of the app
setInterval(checkForAppUpdates, 24 * 60 * 60 * 1000); */
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]);
});
}
module.exports = app; /**
* 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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
var LocalStrategy = require('passport-local').Strategy; var LocalStrategy = require('passport-local').Strategy;
var auth = require('../../../conf/live.json').auth; var auth = require(require('path').join(global.__base, '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;
}
module.exports = (passport) => { module.exports = (passport) => {
// used to serialize the user for the session // used to serialize the user for the session
passport.serializeUser(function (user, done) { passport.serializeUser(function serializeUser (user, done) {
done(null, user); done(null, user);
}); });
passport.deserializeUser(function (user, done) { passport.deserializeUser(function deserializeUser (user, done) {
done(null, user); done(null, user);
}); });
passport.use('local-login', new LocalStrategy({ passport.use('local-login', new LocalStrategy(
usernameField: 'user', {
passwordField: 'pass', 'usernameField': 'user',
'passwordField': 'pass',
// allows us to pass back the entire request to the callback 'passReqToCallback': true // allows us to pass back the entire request to the callback
passReqToCallback: true
}, },
// callback with user and pass from our form // callback with user and pass from our form
function (req, user, pass, done) { function checkLogin (req, user, pass, done) {
var username = process.env.RS_USERNAME || auth.username;
var password = process.env.RS_PASSWORD || auth.password;
// login success // login success
if (user === username && pass === password) { if (user === username && pass === password) {
// WEBSOCKET SECURITY HERE
/*
* WEBSOCKET SECURITY HERE
*/
done(null, auth); done(null, auth);
} else { } else {
done(null, false, req.flash('wrong password or wrong user')); 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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
/* eslint no-unused-vars: 0 */
'use strict';
const express = require('express');
const path = require('path'); const path = require('path');
const Restreamer = require('../../classes/Restreamer');
module.exports = (app, passport) => { module.exports = (app, passport) => {
// static paths // static paths
app.use('/css', express.static(path.join(__dirname, '../', 'public', 'css'))); app.get('/favicon.ico', (req, res) => {
app.use('/libs', express.static(path.join(__dirname, '../', 'public', 'libs'))); res.sendFile(path.join(global.__public, 'images', 'favicon.ico'));
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('/', (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 */ /* Handle Login POST */
app.post('/login', app.post('/login',
passport.authenticate('local-login', { passport.authenticate('local-login', {
successRedirect: '/', 'successRedirect': '/',
failureRedirect: '/', 'failureRedirect': '/#/login_invalid'
failureFlash : true
}) })
); );
app.get('/logout', (req, res)=>{ app.get('/logout', (req, res) => {
req.logout(); req.logout();
res.redirect('/'); 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 * @copyright 2015 datarhei.org
* @license Apache-2.0 * @license Apache-2.0
*/ */
'use strict';
/* eslint vars-on-top: 0 */
const Logger = require('../../classes/Logger'); const logger = require.main.require('./classes/Logger')('webserver');
const logger = new Logger('webserver');
module.exports = (req, res, next)=>{ module.exports = (req, res, next)=> {
req._startTime = new Date(); req._startTime = new Date();
var log = () =>{ var log = () => {
var code = res.statusCode; var code = res.statusCode;
var len = parseInt(res.getHeader('Content-Length'), 10); 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)) { if (isNaN(len)) {
len = ''; len = '';
} else { } else {
len = ' - ' + len; 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'); logger.debug(method + ' \'' + url + '\' ' + code + ' ' + duration + ' ' + req.ip + ' ' + len, 'Webserver');
}; };

View File

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

View File

@@ -1,7 +1,7 @@
html, body { html, body {
height:100%; height: 100%;
margin:0; margin: 0;
padding:0 padding: 0
} }
body { body {
@@ -9,32 +9,29 @@ body {
} }
.container-fluid { .container-fluid {
height:100%; height: 100%;
display:table; display: table;
width: 100%; width: 100%;
padding: 0; 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; -webkit-background-size: cover;
-moz-background-size: cover; -moz-background-size: cover;
-o-background-size: cover; -o-background-size: cover;
background-size: cover; background-size: cover;
background-color: #3d3d39;
} }
.container-body { .container-body {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
max-width: 500px; max-width: 500px;
border-color: #2c2c28; border: 1px solid #2c2c28;
border-width: 1px;
border-style: solid;
border-radius: 10px; border-radius: 10px;
padding: 50px; padding: 50px;
} }
.row-fluid { .row-fluid {
height: 100%; height: 100%;
display:table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
} }
@@ -53,7 +50,7 @@ h1, h2, h3, h4, h5, h6 {
} }
h1 { h1 {
text-shadow: 0px 1px #000; text-shadow: 0 1px #000;
font-weight: 100; font-weight: 100;
margin-bottom: 30px; margin-bottom: 30px;
font-size: 46px; font-size: 46px;
@@ -62,9 +59,10 @@ h1 {
a { a {
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
cursor: pointer;
} }
a:visted { a:visited {
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
} }
@@ -74,7 +72,6 @@ a:hover, a:active, a:focus {
text-decoration: none; text-decoration: none;
} }
.locales { .locales {
color: #60605e; color: #60605e;
text-decoration: none; text-decoration: none;
@@ -120,9 +117,9 @@ a:hover, a:active, a:focus {
color: #373734; color: #373734;
} }
.form-control[disabled]{ .form-control[disabled] {
color: #60605e; color: #60605e;
font-weight:bold; font-weight: bold;
background-color: #373734; background-color: #373734;
border: 2px solid #434341; border: 2px solid #434341;
} }
@@ -157,14 +154,12 @@ a:hover, a:active, a:focus {
background-color: #ac5647; background-color: #ac5647;
color: #fff; color: #fff;
} }
.container .jumbotron, .container-fluid .jumbotron, .jumbotron { .container .jumbotron, .container-fluid .jumbotron, .jumbotron {
padding-top: 18px;
padding-bottom: 18px;
margin-top: -4px; margin-top: -4px;
margin-bottom: 18px; margin-bottom: 18px;
text-align: center; text-align: center;
padding-right: 20px; padding: 18px 20px;
padding-left: 20px;
} }
.player-link { .player-link {
@@ -187,12 +182,13 @@ label {
.modal-content { .modal-content {
background-color: #373734; background-color: #373734;
} }
.modal-header { .modal-header {
border-bottom: 0px solid #e5e5e5; border-bottom: 0 solid #e5e5e5;
} }
.modal-footer { .modal-footer {
border-top: 0px solid #e5e5e5; border-top: 0 solid #e5e5e5;
text-align: left; text-align: left;
} }
@@ -209,7 +205,7 @@ label {
pre { pre {
display: block; display: block;
padding: 9.5px; padding: 10px;
margin: 0 0 10px; margin: 0 0 10px;
font-size: 12px; font-size: 12px;
line-height: 1.42857143; line-height: 1.42857143;
@@ -224,3 +220,23 @@ pre {
.jwplayer:focus, .jwplayer:active { .jwplayer:focus, .jwplayer:active {
outline: 0; 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="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Restreamer"> <meta name="description" content="Restreamer">
<meta name="author" content="datarhei"> <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> <title>Restreamer</title>
<link href="/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <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-bootstrap/ui-bootstrap-tpls.min.js"></script>
<script src="/libs/angular-translate/angular-translate.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="/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> </head>
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
<div class="container-body"> <div class="container-body">
<div ng-controller="languageCtrl"> <div header></div>
<p class="pull-right"> <div id="content" ui-view></div>
<a href="#" ng-click="switchLanguage('en_US')" class="locales" ng-class="{'active': langIs('en_US')}">EN</a> <hr/>
/ <div footer></div>
<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> </div>
</div> </div>
</div> </div>
@@ -62,17 +49,27 @@
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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> <h4 class="modal-title">Player</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="embed-responsive embed-responsive-16by9"> <div class="embed-responsive embed-responsive-16by9">
<div id="player" class="embed-responsive-item"></div> <div id="player" class="embed-responsive-item"></div>
</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;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> <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> </div>
</div> </div>

View File

@@ -1,15 +1,18 @@
{ {
"help_titel": "Hilfe", "help_title": "Hilfe",
"issue_tracker": "Fehler melden", "issue_tracker": "Fehler melden",
"project_page": "Hilfe", "project_page": "Hilfe",
"update_btn": "Update verfügbar", "update_btn": "Update verfügbar",
"login_username": "Benutzername", "login_username": "Benutzername",
"login_password": "Passwort", "login_password": "Passwort",
"login_invalid": "Benutzername oder Passwort ist ungültig",
"login_btn": "Anmelden", "login_btn": "Anmelden",
"logout": "Ausloggen",
"button_start": "Start", "button_start": "Start",
"button_stop": "Stop", "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", "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_input_invalid": "Die Stream-Adresse ist nicht valide. Bitte überprüfe die Eingabe",
"process_success": "Das Streaming wurde erfolgreich aufgebaut.", "process_success": "Das Streaming wurde erfolgreich aufgebaut.",
"process_failed": "Auf den Stream konnte nicht zugegriffen werden.", "process_failed": "Auf den Stream konnte nicht zugegriffen werden.",
@@ -17,7 +20,7 @@
"process_init": "Der Streaming-Prozess wird erstellt. Bitte warten...", "process_init": "Der Streaming-Prozess wird erstellt. Bitte warten...",
"output_optional": "Externer RTMP-Streaming-Server", "output_optional": "Externer RTMP-Streaming-Server",
"output_optional_example": "z.b. rtmp://live.youtube.com/channelId", "output_optional_example": "z.b. rtmp://live.youtube.com/channelId",
"player_link_titel": "Player öffnen", "player_link_title": "Player öffnen",
"player_modal_help_titel": "Der iFrame-Code und ein Preview-Image zur Einbettung in der Webseite:", "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" "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", "issue_tracker": "Issue alert",
"project_page": "Help", "project_page": "Help",
"update_btn": "Update available", "update_btn": "Update available",
"login_username": "Username", "login_username": "Username",
"login_password": "Password", "login_password": "Password",
"login_invalid": "Username or Password is invalid",
"login_btn": "Login", "login_btn": "Login",
"logout": "Logout",
"button_start": "Start", "button_start": "Start",
"button_stop": "Stop", "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", "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_input_invalid": "Invalid stream address, please check your input",
"process_success": "Streaming is successfully initiated.", "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_failed_retry": "Retry count",
"process_init": "Streaming process is initiating. Please wait...", "process_init": "Streaming process is initiating. Please wait...",
"output_optional": "External RTMP-Streaming-Server", "output_optional": "External RTMP-Streaming-Server",
"output_optional_example": "e.g. rtmp://live.youtube.com/channelId", "output_optional_example": "e.g. rtmp://live.youtube.com/channelId",
"player_link_titel": "Open player", "player_link_title": "Open player",
"player_modal_help_titel": "iFrame code and preview of image to embed in website:", "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" "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/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="/libs/clappr/dist/clappr.min.js"></script> <script src="/libs/clappr/dist/clappr.min.js"></script>
</head> </head>
<body style="margin:0px;background-color:#000;"> <body style="margin: 0; background-color: #000;">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div> <div>
@@ -20,14 +20,15 @@
<div id="player" class="embed-responsive-item"></div> <div id="player" class="embed-responsive-item"></div>
</div> </div>
<script> <script>
var player = new Clappr.Player({ var player = new window.Clappr.Player({
source: ("https:" === window.location.protocol ? "https:" : "http:") + "//" + window.location.hostname + ":" + window.location.port + "/hls/live.stream.m3u8", 'source': (window.location.protocol === 'https:' ? 'https:' : 'http:') +
parentId: "#player", '//' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8',
baseUrl: '/libs/clappr/dist/', 'parentId': '#player',
poster: "images/live.jpg", 'baseUrl': '/libs/clappr/dist/',
mediacontrol: {seekbar: "#3daa48", buttons: "#3daa48"}, 'poster': 'images/live.jpg',
height: "100%", 'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'},
width: "100%" 'height': '100%',
'width': '100%'
}); });
</script> </script>
</div> </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 * @license Apache-2.0
*/ */
/* /* eslint no-console: 0*/
styles of the logging outputs 'use strict';
*/
// styles of the logging output
const INFO = 'color: #0000FF; font-weight: bold'; const INFO = 'color: #0000FF; font-weight: bold';
const DEBUG = 'color: #AABBCC; font-weight: bold'; const DEBUG = 'color: #AABBCC; font-weight: bold';
const ERROR = 'color: #FF0011d; 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_OUT = 'color: #00BF00; font-weight: bold';
const WEBSOCKETS_NAMESPACE = 'color: #00BF00; font-weight: bold'; const WEBSOCKETS_NAMESPACE = 'color: #00BF00; font-weight: bold';
class loggerService { const LoggerService = function loggerService () {
/**
/*
* no further dependencies needed
*/
constructor () {}
/*
* log an info message * log an info message
* @param {string} message * @param {string} message
*/ */
info (message) { this.info = (message) => {
this.log(INFO, message, 'INFO'); this.log(INFO, message, 'INFO');
} };
/* /**
* log an info message * log an debug message
* @param {string} message * @param {string} message
*/ */
debug (message) { this.debug = (message) => {
this.log(DEBUG, message, 'DEBUG'); this.log(DEBUG, message, 'DEBUG');
} };
/* /**
* log an info message * log an error message
* @param {string} message * @param {string} message
*/ */
error (message) { this.error = (message) => {
this.log(ERROR, message, 'ERROR'); this.log(ERROR, message, 'ERROR');
} };
/* /**
* log an info message * log an websocket in message
* @param {string} message * @param {string} message
*/ */
websockets_in (message) { this.websocketsIn = (message) => {
this.log(WEBSOCKETS_IN, message, 'WS_IN'); this.log(WEBSOCKETS_IN, message, 'WS_IN');
} };
/* /**
* log an info message * log an websocket out message
* @param {string} message * @param {string} message
*/ */
websockets_out (message) { this.websocketsOut = (message) => {
this.log(WEBSOCKETS_OUT, message, 'WS_OUT'); this.log(WEBSOCKETS_OUT, message, 'WS_OUT');
} };
/* /**
* log an info message * log an websocket namespace message
* @param {string} message * @param {string} message
*/ */
websockets_namespace (message) { this.websocketsNamespace = (message) => {
this.log(WEBSOCKETS_NAMESPACE, message, 'WS_CONNECT'); this.log(WEBSOCKETS_NAMESPACE, message, 'WS_CONNECT');
} };
/* /**
* log a message with style * log a message with style
* @param {string} style * @param {string} style
* @param {string} message * @param {string} message
* @param {string} type
*/ */
log (style, message, type) { this.log = (style, message, type) => {
console.log('%c ' + '[' + type + ']' + message, style); console.log('%c [' + type + ']' + message, style);
} };
} };
/* // configure loggerService as AngularJS Service
* configure loggerService as angulerjs Service window.angular.module('app').factory('loggerService', () => {
*/ return new LoggerService();
window.app.factory('loggerService', function () {
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>