ADD v0.1.0-rc.7

This commit is contained in:
Jan Stabenow
2016-03-25 19:13:24 +01:00
parent 74bfafb88b
commit 22a7c6f0ee
41 changed files with 734 additions and 556 deletions

View File

@@ -1,3 +1,13 @@
## Changes from 0.1.0-RC6.1 to 0.1.0-rc.7
* security improvements
* FFmpeg and NGINX optimizations
* fixed update check
* added semantic versioning
* several small bugfixes and improvements
* updated dependencies
* added Aarch64 Docker image and reduced Docker layers
## Changes from 0.1.0-RC6 to 0.1.0-RC6.1 ## Changes from 0.1.0-RC6 to 0.1.0-RC6.1
* fixed external streaming with RTSP over TCP input option * fixed external streaming with RTSP over TCP input option

View File

@@ -1,25 +1,25 @@
FROM node:5.7.1-slim FROM node:5.9.0-slim
MAINTAINER datarhei <info@datarhei.org> MAINTAINER datarhei <info@datarhei.org>
ENV FFMPEG_VERSION 2.8.6 ENV FFMPEG_VERSION=2.8.6 \
ENV YASM_VERSION 1.3.0 YASM_VERSION=1.3.0 \
ENV LAME_VERSION 3_99_5 LAME_VERSION=3_99_5 \
ENV NGINX_VERSION 1.9.9 NGINX_VERSION=1.9.9 \
ENV NGINX_RTMP_VERSION 1.1.7.10 NGINX_RTMP_VERSION=1.1.7.10 \
ENV SRC "/usr/local" SRC="/usr/local" \
ENV LD_LIBRARY_PATH "${SRC}/lib" LD_LIBRARY_PATH="${SRC}/lib" \
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig" PKG_CONFIG_PATH="${SRC}/lib/pkgconfig" \
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev" 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 git libpcre3 tar perl ca-certificates ${BUILDDEPS} apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS} && \
# yasm # yasm
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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}" && \
@@ -29,10 +29,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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 \ ./configure \
@@ -43,10 +43,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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}" && \
@@ -58,11 +58,11 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
make distclean && \ make distclean && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
# ffmpeg # ffmpeg
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS # patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
cd "ffmpeg-${FFMPEG_VERSION}" && \ cd "ffmpeg-${FFMPEG_VERSION}" && \
@@ -92,12 +92,12 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
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 "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" && \
RUN ffmpeg -buildconf ffmpeg -buildconf && \
# nginx-rtmp # nginx-rtmp
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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/sergey-dryabzhinsky/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" && \
@@ -108,9 +108,9 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \ --add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \ apt-get purge -y --auto-remove ${BUILDDEPS} && \
rm -rf /tmp/* rm -rf /tmp/*
COPY . /restreamer COPY . /restreamer
@@ -123,8 +123,8 @@ RUN npm install -g bower grunt grunt-cli nodemon public-ip eslint && \
npm cache clean && \ npm cache clean && \
bower cache clean --allow-root bower cache clean --allow-root
ENV RS_USERNAME admin ENV RS_USERNAME admin \
ENV RS_PASSWORD datarhei RS_PASSWORD datarhei
EXPOSE 8080 EXPOSE 8080
VOLUME ["/restreamer/db"] VOLUME ["/restreamer/db"]

149
Dockerfile_aarch64 Normal file
View File

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

View File

@@ -2,27 +2,27 @@ FROM resin/rpi-raspbian:jessie
MAINTAINER datarhei <info@datarhei.org> MAINTAINER datarhei <info@datarhei.org>
ENV NODE_VERSION 5.7.1 ENV NODE_VERSION=5.9.0 \
ENV NPM_VERSION 3.6.0 NPM_VERSION=3.7.3 \
ENV FFMPEG_VERSION 2.8.6 FFMPEG_VERSION=2.8.6 \
ENV YASM_VERSION 1.3.0 YASM_VERSION=1.3.0 \
ENV LAME_VERSION 3_99_5 LAME_VERSION=3_99_5 \
ENV NGINX_VERSION 1.9.9 NGINX_VERSION=1.9.9 \
ENV NGINX_RTMP_VERSION 1.1.7.10 NGINX_RTMP_VERSION=1.1.7.10 \
ENV SRC "/usr/local" SRC="/usr/local" \
ENV LD_LIBRARY_PATH "${SRC}/lib" LD_LIBRARY_PATH="${SRC}/lib" \
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig" PKG_CONFIG_PATH="${SRC}/lib/pkgconfig" \
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev" 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 git libpcre3 tar perl ca-certificates ${BUILDDEPS} apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS} && \
# node # node
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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 xzvf "node-v${NODE_VERSION}-linux-armv6l.tar.gz" \ tar xzvf "node-v${NODE_VERSION}-linux-armv6l.tar.gz" \
@@ -31,10 +31,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
npm install -g "npm@${NPM_VERSION}" --unsafe-perm && \ 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}" && \ 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}" && \
@@ -44,10 +44,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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 \ ./configure \
@@ -58,10 +58,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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}" && \
@@ -73,11 +73,11 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
make distclean && \ make distclean && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
# ffmpeg # ffmpeg
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS # patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
cd "ffmpeg-${FFMPEG_VERSION}" && \ cd "ffmpeg-${FFMPEG_VERSION}" && \
@@ -107,12 +107,12 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
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 "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" && \
RUN ffmpeg -buildconf ffmpeg -buildconf && \
# nginx-rtmp # nginx-rtmp
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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/sergey-dryabzhinsky/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" && \
@@ -123,9 +123,9 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \ --add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \ apt-get purge -y --auto-remove ${BUILDDEPS} && \
rm -rf /tmp/* rm -rf /tmp/*
COPY . /restreamer COPY . /restreamer
@@ -138,8 +138,8 @@ RUN npm install -g bower grunt grunt-cli nodemon public-ip && \
npm cache clean && \ npm cache clean && \
bower cache clean --allow-root bower cache clean --allow-root
ENV RS_USERNAME admin ENV RS_USERNAME=admin \
ENV RS_PASSWORD datarhei RS_PASSWORD=datarhei
EXPOSE 8080 EXPOSE 8080
VOLUME ["/restreamer/db"] VOLUME ["/restreamer/db"]

View File

@@ -2,27 +2,27 @@ FROM resin/rpi-raspbian:jessie
MAINTAINER datarhei <info@datarhei.org> MAINTAINER datarhei <info@datarhei.org>
ENV NODE_VERSION 5.7.1 ENV NODE_VERSION=5.9.0 \
ENV NPM_VERSION 3.6.0 NPM_VERSION=3.7.3 \
ENV FFMPEG_VERSION 2.8.6 FFMPEG_VERSION=2.8.6 \
ENV YASM_VERSION 1.3.0 YASM_VERSION=1.3.0 \
ENV LAME_VERSION 3_99_5 LAME_VERSION=3_99_5 \
ENV NGINX_VERSION 1.9.9 NGINX_VERSION=1.9.9 \
ENV NGINX_RTMP_VERSION 1.1.7.10 NGINX_RTMP_VERSION=1.1.7.10 \
ENV SRC "/usr/local" SRC="/usr/local" \
ENV LD_LIBRARY_PATH "${SRC}/lib" LD_LIBRARY_PATH="${SRC}/lib" \
ENV PKG_CONFIG_PATH "${SRC}/lib/pkgconfig" PKG_CONFIG_PATH="${SRC}/lib/pkgconfig" \
ENV BUILDDEPS "autoconf automake gcc g++ libtool make nasm zlib1g-dev libssl-dev xz-utils cmake build-essential libpcre3-dev" 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 git libpcre3 tar perl ca-certificates ${BUILDDEPS} apt-get install -y --force-yes curl git libpcre3 tar perl ca-certificates ${BUILDDEPS} && \
# node # node
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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 xzvf "node-v${NODE_VERSION}-linux-armv7l.tar.gz" \ tar xzvf "node-v${NODE_VERSION}-linux-armv7l.tar.gz" \
@@ -31,10 +31,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
npm install -g "npm@${NPM_VERSION}" --unsafe-perm && \ 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}" && \ 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}" && \
@@ -44,10 +44,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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 \ ./configure \
@@ -58,10 +58,10 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ 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}" && \ 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}" && \
@@ -73,11 +73,11 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
make distclean && \ make distclean && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
# ffmpeg # ffmpeg
# patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS # patch: andrew-shulgin Ignore invalid sprop-parameter-sets missing PPS
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ DIR="$(mktemp -d)" && cd "${DIR}" && \
curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ curl -LOks "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \ tar xzvf "ffmpeg-${FFMPEG_VERSION}.tar.gz" && \
cd "ffmpeg-${FFMPEG_VERSION}" && \ cd "ffmpeg-${FFMPEG_VERSION}" && \
@@ -107,12 +107,12 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
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 "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" echo "${SRC}/lib" > "/etc/ld.so.conf.d/libc.conf" && \
RUN ffmpeg -buildconf ffmpeg -buildconf && \
# nginx-rtmp # nginx-rtmp
RUN DIR="$(mktemp -d)" && cd "${DIR}" && \ 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/sergey-dryabzhinsky/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" && \
@@ -123,9 +123,9 @@ RUN DIR="$(mktemp -d)" && cd "${DIR}" && \
--add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \ --add-module="../nginx-rtmp-module-${NGINX_RTMP_VERSION}" && \
make -j"$(nproc)" && \ make -j"$(nproc)" && \
make install && \ make install && \
rm -rf "${DIR}" rm -rf "${DIR}" && \
RUN apt-get purge -y --auto-remove ${BUILDDEPS} && \ apt-get purge -y --auto-remove ${BUILDDEPS} && \
rm -rf /tmp/* rm -rf /tmp/*
COPY . /restreamer COPY . /restreamer
@@ -138,8 +138,8 @@ RUN npm install -g bower grunt grunt-cli nodemon public-ip && \
npm cache clean && \ npm cache clean && \
bower cache clean --allow-root bower cache clean --allow-root
ENV RS_USERNAME admin ENV RS_USERNAME=admin \
ENV RS_PASSWORD datarhei RS_PASSWORD=datarhei
EXPOSE 8080 EXPOSE 8080
VOLUME ["/restreamer/db"] VOLUME ["/restreamer/db"]

View File

@@ -13,14 +13,14 @@ Datarhei/Restreamer offers smart free video streaming in real time. Stream H.264
## Upcomming releases ## Upcomming releases
- RC7 (tba) - rc.8 (next 14 days)
## Roadmap ## Roadmap
- optimizing FFmpeg handling
- backend refactoring - backend refactoring
- full REST API - full REST API
- security improvements - optimizing FFmpeg handling
- debugging features
##Documentation ##Documentation

View File

@@ -1,10 +1,10 @@
{ {
"name": "Restreamer", "name": "Restreamer",
"version": "0.1.0-RC6.1", "version": "0.1.0-rc.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"bootstrap": "3.3.6", "bootstrap": "3.3.6",
"jquery": "2.2.1", "jquery": "2.2.2",
"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",
@@ -14,7 +14,6 @@
"angular-translate-loader-static-files": "2.9.2" "angular-translate-loader-static-files": "2.9.2"
}, },
"resolutions": { "resolutions": {
"angular": "1.4.8", "angular": "1.5.0"
"angular-bootstrap": "~0.14.3"
} }
} }

View File

@@ -9,7 +9,7 @@ rtmp {
chunk_size 4000; chunk_size 4000;
application live { application live {
live on; live on;
meta copy; idle_streams off;
} }
application hls { application hls {
live on; live on;
@@ -18,6 +18,7 @@ rtmp {
hls_playlist_length 60s; hls_playlist_length 60s;
hls_fragment 2s; hls_fragment 2s;
hls_path /tmp/hls; hls_path /tmp/hls;
idle_streams off;
} }
} }
} }
@@ -26,13 +27,14 @@ 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;
root /restreamer/src/webserver/public; include /usr/local/nginx/conf/mime.types;
allow all; location / {
include /usr/local/nginx/conf/mime.types; try_files $uri @node;
add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Origin *;
} }
location / { location @node {
add_header Access-Control-Allow-Origin *;
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;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;

View File

@@ -1,6 +1,6 @@
{ {
"name": "Restreamer", "name": "Restreamer",
"version": "0.1.0-RC6.1", "version": "0.1.0-rc.7",
"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": { "repository": {
@@ -14,30 +14,30 @@
"dependencies": { "dependencies": {
"body-parser": "1.15.0", "body-parser": "1.15.0",
"compression": "~1.6.1", "compression": "~1.6.1",
"cookie": "^0.2.3",
"cookie-parser": "1.4.1", "cookie-parser": "1.4.1",
"express": "4.13.4", "express": "4.13.4",
"express-session": "^1.13.0", "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.1.0", "jsonschema": "^1.1.0",
"moment-timezone": "^0.5.0", "moment-timezone": "^0.5.2",
"node-json-db": "git://github.com/andrew-shulgin/node-json-db", "node-json-db": "git://github.com/andrew-shulgin/node-json-db",
"passport": "^0.3.2", "semver": "^5.1.0",
"passport-local": "^1.0.0",
"ps-find": "^1.1.0", "ps-find": "^1.1.0",
"q": "1.4.1", "q": "1.4.1",
"socket.io": "1.4.5" "socket.io": "1.4.5"
}, },
"devDependencies": { "devDependencies": {
"eslint": "2.3.0", "eslint": "2.4.0",
"babel-preset-es2015": "^6.5.0", "babel-preset-es2015": "^6.6.0",
"grunt": "0.4.5", "grunt": "0.4.5",
"grunt-babel": "^6.0.0", "grunt-babel": "^6.0.0",
"grunt-contrib-csslint": "^1.0.0", "grunt-contrib-csslint": "^1.0.0",
"grunt-contrib-cssmin": "~1.0.0", "grunt-contrib-cssmin": "~1.0.1",
"grunt-contrib-uglify": "~1.0.0", "grunt-contrib-uglify": "~1.0.1",
"grunt-contrib-watch": "^0.6.1", "grunt-contrib-watch": "^1.0.0",
"grunt-ng-annotate": "~1.0.1", "grunt-ng-annotate": "~2.0.1",
"grunt-shell": "1.2.1", "grunt-shell": "1.2.1",
"load-grunt-tasks": "~3.4.0" "load-grunt-tasks": "~3.4.1"
} }
} }

31
run.sh
View File

@@ -15,7 +15,36 @@ 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 - -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
if [ "$RS_RASPICAM_WIDTH" = "" ]
then
RASPICAM_WIDTH=1280
else
RASPICAM_WIDTH=$RS_RASPICAM_WIDTH
fi
if [ "$RS_RASPICAM_HEIGHT" = "" ]
then
RASPICAM_HEIGHT=720
else
RASPICAM_HEIGHT=$RS_RASPICAM_HEIGHT
fi
if [ "$RS_RASPICAM_FPS" = "" ]
then
RASPICAM_FPS=25
else
RASPICAM_FPS=$RS_RASPICAM_FPS
fi
if [ "$RS_RASPICAM_BITRATE" = "" ]
then
RASPICAM_BITRATE=50000
else
RASPICAM_BITRATE=$RS_RASPICAM_BITRATE
fi
/opt/vc/bin/raspivid -t 0 -w $RASPICAM_WIDTH -h $RASPICAM_HEIGHT -fps $RASPICAM_FPS -b $RASPICAM_BITRATE -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

View File

@@ -6,6 +6,7 @@
*/ */
'use strict'; 'use strict';
const logger = require('./Logger')('EnvVar'); const logger = require('./Logger')('EnvVar');
const logBlacklist = ['RS_PASSWORD'];
/** /**
* Class for environment variables with default values * Class for environment variables with default values
@@ -19,15 +20,14 @@ class EnvVar {
process.env[envVar.name] = process.env[envVar.alias]; process.env[envVar.name] = process.env[envVar.alias];
delete process.env[envVar.alias]; delete process.env[envVar.alias];
} }
if (typeof process.env[envVar.name] !== 'undefined') { if (typeof process.env[envVar.name] !== 'undefined') {
logger.info(`ENV "${envVar.name} = ${process.env[envVar.name]}"`, envVar.description); logger.info(`ENV "${envVar.name} = ${(logBlacklist.indexOf(envVar.name) === -1 ? process.env[envVar.name] : '[hidden]')}"`, envVar.description);
} else if (envVar.required === true) { } else if (envVar.required === true) {
logger.error(`No value set for env "${envVar.name}", but it is required`); logger.error(`No value set for env "${envVar.name}", but it is required`);
killProcess = true; killProcess = true;
} else { } else {
process.env[envVar.name] = envVar.defaultValue; process.env[envVar.name] = envVar.defaultValue;
logger.info(`ENV "${envVar.name} = ${process.env[envVar.name]}", set to default value`, envVar.description); logger.info(`ENV "${envVar.name} = ${(logBlacklist.indexOf(envVar.name) === -1 ? process.env[envVar.name] : '[hidden]')}", set to default value`, envVar.description);
} }
if (typeof process.env[envVar.name] !== 'undefined') { if (typeof process.env[envVar.name] !== 'undefined') {

View File

@@ -12,8 +12,10 @@ const logger = require('./Logger')('Restreamer');
const WebsocketsController = require('./WebsocketsController'); 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'); const JsonDB = require('node-json-db');
const exec = require('child_process').exec;
const packageJson = require(path.join(global.__base, 'package.json'));
const https = require('https');
/** /**
* class Restreamer creates and manages streams through ffmpeg * class Restreamer creates and manages streams through ffmpeg
@@ -40,11 +42,10 @@ class Restreamer {
/** /**
* receive snapshot by using first frame of repeated video * receive snapshot by using first frame of repeated video
* @param {boolean} firstSnapshot
*/ */
static fetchSnapshot (firstSnapshot) { static fetchSnapshot () {
var command = null; var command = null;
if (Restreamer.data.states.repeatToLocalNginx.type === 'connected' || firstSnapshot) { if (Restreamer.data.states.repeatToLocalNginx.type === 'connected') {
command = new FfmpegCommand(Restreamer.generateOutputHLSPath()); command = new FfmpegCommand(Restreamer.generateOutputHLSPath());
command.output(Restreamer.generateSnapshotPath()); command.output(Restreamer.generateSnapshotPath());
@@ -53,9 +54,10 @@ class Restreamer {
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');
WebsocketsController.emit('snapshot', null);
Q.delay(this.calculateSnapshotRefreshInterval()).then(() => { Q.delay(this.calculateSnapshotRefreshInterval()).then(() => {
Restreamer.fetchSnapshot(false); Restreamer.fetchSnapshot();
}); });
}); });
command.exec(); command.exec();
@@ -70,7 +72,7 @@ class Restreamer {
return snapshotRefreshInterval[1]; return snapshotRefreshInterval[1];
} else if (snapshotRefreshInterval[2] === 'm') { } else if (snapshotRefreshInterval[2] === 'm') {
return snapshotRefreshInterval[1] * 1000 * 60; return snapshotRefreshInterval[1] * 1000 * 60;
} else if (snapshotRefreshInterval[2] === 's' && snapshotRefreshInterval[1] > 30) { } else if (snapshotRefreshInterval[2] === 's' && snapshotRefreshInterval[1] >= 10) {
return snapshotRefreshInterval[1] * 1000; return snapshotRefreshInterval[1] * 1000;
} }
@@ -86,6 +88,7 @@ class Restreamer {
Restreamer.updateState(processName, 'stopped'); Restreamer.updateState(processName, 'stopped');
logger.info('stopStream ' + processName); logger.info('stopStream ' + processName);
clearTimeout(Restreamer.data.retryTimeouts[processName]);
if (processHasBeenSpawned) { if (processHasBeenSpawned) {
Restreamer.data.processes[processName].kill(); Restreamer.data.processes[processName].kill();
Restreamer.data.processes[processName] = { Restreamer.data.processes[processName] = {
@@ -146,14 +149,14 @@ class Restreamer {
* send websocket event to GUI to update the state of the streams * send websocket event to GUI to update the state of the streams
*/ */
static updateStreamDataOnGui () { static updateStreamDataOnGui () {
WebsocketsController.emitToNamespace('/', 'updateStreamData', Restreamer.extractDataOfStreams()); WebsocketsController.emit('updateStreamData', Restreamer.extractDataOfStreams());
} }
/** /**
* send websocket event to GUI to update the state of the streams * send websocket event to GUI to update the state of the streams
*/ */
static updateProgressOnGui () { static updateProgressOnGui () {
WebsocketsController.emitToNamespace('/', 'updateProgress', Restreamer.data.progresses); WebsocketsController.emit('updateProgress', Restreamer.data.progresses);
} }
/** /**
@@ -168,6 +171,7 @@ class Restreamer {
} }
static applyOptions (ffmpegCommand, streamType) { static applyOptions (ffmpegCommand, streamType) {
ffmpegCommand.native(); // add -re
if (streamType === 'repeatToLocalNginx') { if (streamType === 'repeatToLocalNginx') {
if (Restreamer.data.options.rtspTcp && Restreamer.data.addresses.srcAddress.indexOf('rtsp') === 0) { if (Restreamer.data.options.rtspTcp && Restreamer.data.addresses.srcAddress.indexOf('rtsp') === 0) {
ffmpegCommand.inputOptions('-rtsp_transport tcp'); ffmpegCommand.inputOptions('-rtsp_transport tcp');
@@ -217,6 +221,9 @@ class Restreamer {
'type': state, 'type': state,
'message': message 'message': message
}; };
if (processName === 'repeatToLocalNginx' && state === 'connected') {
Restreamer.fetchSnapshot();
}
Restreamer.writeToDB(); Restreamer.writeToDB();
Restreamer.updateStreamDataOnGui(); Restreamer.updateStreamDataOnGui();
return state; return state;
@@ -315,7 +322,6 @@ class Restreamer {
}; };
Restreamer.applyOptions(command, streamType); Restreamer.applyOptions(command, streamType);
command command
// stream started
.on('start', (commandLine) => { .on('start', (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');
@@ -323,11 +329,6 @@ class Restreamer {
} }
logger.debug(`FFmpeg spawned: ${commandLine}`); logger.debug(`FFmpeg spawned: ${commandLine}`);
Restreamer.data.processes[streamType] = command; Restreamer.data.processes[streamType] = command;
// fetch snapshot only, if repeated to local nginx
if (repeatToLocalNginx) {
Restreamer.fetchSnapshot(true);
}
}) })
// stream ended // stream ended
@@ -340,10 +341,10 @@ class Restreamer {
return; return;
} }
logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`); logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(() => { Restreamer.data.retryTimeouts[streamType] = setTimeout(() => {
logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`); logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`);
Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current); Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current);
}); }, config.ffmpeg.monitor.restart_wait);
} }
}) })
@@ -362,10 +363,10 @@ class Restreamer {
Restreamer.updateState(streamType, 'error', error.toString()); Restreamer.updateState(streamType, 'error', error.toString());
logger.error(`Error on stream ${streamType}: ${error.toString()}`); logger.error(`Error on stream ${streamType}: ${error.toString()}`);
logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`); logger.info(`Retrying FFmpeg connection to "${src}" after "${config.ffmpeg.monitor.restart_wait}" ms`);
Q.delay(config.ffmpeg.monitor.restart_wait).then(() => { Restreamer.data.retryTimeouts[streamType] = setTimeout(() => {
logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`); logger.info(`Retry FFmpeg connection to "${src}" retry counter: ${Restreamer.data.retryCounter[streamType].current}`);
Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current); Restreamer.startStream(src, streamType, optionalOutput, Restreamer.data.retryCounter[streamType].current);
}); }, config.ffmpeg.monitor.restart_wait);
} else { } else {
Restreamer.updateState(streamType, 'error', error.toString()); Restreamer.updateState(streamType, 'error', error.toString());
} }
@@ -388,7 +389,8 @@ class Restreamer {
* bind websocket events on application start * bind websocket events on application start
*/ */
static bindWebsocketEvents () { static bindWebsocketEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => { WebsocketsController.setConnectCallback((socket) => {
socket.emit('publicIp', Restreamer.data.publicIp);
socket.on('startStream', (options)=> { socket.on('startStream', (options)=> {
Restreamer.updateUserAction(options.streamType, 'start'); Restreamer.updateUserAction(options.streamType, 'start');
Restreamer.updateOptions(options.options); Restreamer.updateOptions(options.options);
@@ -398,9 +400,6 @@ class Restreamer {
Restreamer.updateUserAction(streamType, 'stop'); Restreamer.updateUserAction(streamType, 'stop');
Restreamer.stopStream(streamType); Restreamer.stopStream(streamType);
}); });
socket.on('checkForAppUpdates', ()=> {
socket.emit('checkForAppUpdatesResult', app.get('updateAvailable'));
});
socket.on('checkStates', Restreamer.updateStreamDataOnGui); socket.on('checkStates', Restreamer.updateStreamDataOnGui);
}); });
} }
@@ -432,6 +431,44 @@ class Restreamer {
'states': Restreamer.data.states 'states': Restreamer.data.states
}; };
} }
/**
* check for updates
*/
static checkForUpdates () {
const url = {'host': 'datarhei.org', 'path': '/apps.json'};
logger.debug('Checking for updates...');
https.get(url, (response) => {
if (response.statusCode === 200) {
response.on('data', (body) => {
var updateCheck = JSON.parse(body);
var updateAvailable = require('semver').lt(packageJson.version, updateCheck.restreamer.version);
logger.info(`Update checking succeeded. ${updateAvailable ? 'Update' : 'No updates'} available`, 'checkForUpdates');
logger.debug(`local: ${packageJson.version}; remote: ${updateCheck.restreamer.version}`, 'checkForUpdates');
Restreamer.data.updateAvailable = updateAvailable;
WebsocketsController.emit('update', updateAvailable);
});
} else {
logger.warn(`Got ${String(response.statusCode)} status while trying to fetch update info`, 'checkForUpdates');
}
}).on('error', () => {
logger.warn('Failed fetching update info', 'checkForUpdates');
});
setTimeout(Restreamer.checkForUpdates, 12 * 3600 * 1000);
}
/**
* get public ip
*/
static getPublicIp () {
logger.info('Getting public ip...', 'start.publicip');
exec('public-ip', (err, stdout, stderr) => {
if (err) {
logger.error(err);
}
Restreamer.data.publicIp = stdout.split('\n')[0];
});
}
} }
/* /*
@@ -448,6 +485,10 @@ Restreamer.data = {
'max': config.ffmpeg.monitor.retries 'max': config.ffmpeg.monitor.retries
} }
}, },
'retryTimeouts': {
'repeatToLocalNginx': null,
'repeatToOptionalOutput': null
},
'options': { 'options': {
'rtspTcp': false 'rtspTcp': false
}, },
@@ -488,7 +529,9 @@ Restreamer.data = {
'addresses': { 'addresses': {
'srcAddress': '', 'srcAddress': '',
'optionalOutputAddress': '' 'optionalOutputAddress': ''
} },
'updateAvailable': false,
'publicIp': '127.0.0.1'
}; };
module.exports = Restreamer; module.exports = Restreamer;

View File

@@ -8,55 +8,36 @@
const logger = require.main.require('./classes/Logger')('WebsocketsController'); const logger = require.main.require('./classes/Logger')('WebsocketsController');
const app = require.main.require('./webserver/app').app; 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 bound, 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 Restreamer is a single page application, there is no need to use different namespaces
*/ */
class WebsocketsController { class WebsocketsController {
/** /**
* * emit an event to WS
* @param {string} namespace namespace to emit the event to
* @param {string} event name of the event * @param {string} event name of the event
* @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 emit (event, data) {
app.get('websocketsReady').promise.then((io) => { app.get('websocketsReady').promise.then((io) => {
logger.debug(`websocket got event ${event} to namespace ${namespace}`, 'Websockets'); logger.debug(`Emitting ${event}`);
io.of(namespace).emit(event, data); io.sockets.emit(event, data);
}); });
} }
/** /**
* add event, that is emmi * add callback on WS connection
* @param {string} namespace
* @param {function} callback * @param {function} callback
*/ */
static addOnConnectionEventToNamespace (namespace, callback) { static setConnectCallback (callback) {
app.get('websocketsReady').promise.then((io) => { app.get('websocketsReady').promise.then((io) => {
var nsp = io.of(namespace); io.on('connection', (socket) => {
nsp.on('connection', (socket) => {
callback(socket); callback(socket);
}); });
}); });
} }
/**
* bind default events of all classes that are using websockets events
*/
static bindDefaultEvents () {
WebsocketsController.addOnConnectionEventToNamespace('/', (socket) => {
socket.on('getVersion', () => {
socket.emit('version', packageJson.version);
});
socket.emit('publicIp', app.get('publicIp'));
});
require('./Restreamer').bindWebsocketEvents();
}
} }
module.exports = WebsocketsController; module.exports = WebsocketsController;

View File

@@ -19,7 +19,6 @@ const nginxrtmp = require('./classes/Nginxrtmp')(config);
const Q = require('q'); const Q = require('q');
const Restreamer = require('./classes/Restreamer'); const Restreamer = require('./classes/Restreamer');
const RestreamerData = require('./classes/RestreamerData'); const RestreamerData = require('./classes/RestreamerData');
const WebsocketsController = require('./classes/WebsocketsController');
const restreamerApp = require('./webserver/app'); const restreamerApp = require('./webserver/app');
// show start message // show start message
@@ -38,28 +37,20 @@ logger.info('', false);
// setup environment vars // setup environment vars
EnvVar.init(config); EnvVar.init(config);
// check for app updates
restreamerApp.checkForRestreamerUpdates();
// Check for updates each 12 hours
setInterval(restreamerApp.checkForRestreamerUpdates, 12 * 3600 * 1000);
// add default websocket events, @todo this will be removed, when the new websocket workflow is implemented
WebsocketsController.bindDefaultEvents();
// start the app // start the app
nginxrtmp.init() nginxrtmp.init()
.then(()=> { .then(() => {
return RestreamerData.checkJSONDb(); return RestreamerData.checkJSONDb();
}) })
.then(()=> { .then(() => {
Restreamer.checkForUpdates();
Restreamer.getPublicIp();
Restreamer.bindWebsocketEvents();
return restreamerApp.startWebserver(); return restreamerApp.startWebserver();
}) })
.then(() => { .then(() => {
return Q.fcall(Restreamer.restoreFFMpegProcesses); return Q.fcall(Restreamer.restoreFFMpegProcesses);
}) })
.then(()=> {
restreamerApp.getPublicIp();
})
.catch((error)=> { .catch((error)=> {
let errorMessage = `Error starting webserver and nginx for application: ${error}`; let errorMessage = `Error starting webserver and nginx for application: ${error}`;
throw new Error(errorMessage); throw new Error(errorMessage);

View File

@@ -5,26 +5,20 @@
*/ */
'use strict'; 'use strict';
// auth stuff
const passport = require('passport');
const passportConfig = require('./config/passport');
// express // express
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const cookie = require('cookie');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const compression = require('compression'); const compression = require('compression');
const https = require('https');
// other // other
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 logger = require.main.require('./classes/Logger')('RestreamerExpressApp');
const indexRouter = require('./controllers/index'); const indexRouter = require('./controllers/index');
const apiV1 = require('./controllers/api/v1'); const apiV1 = require('./controllers/api/v1');
@@ -43,6 +37,8 @@ class RestreamerExpressApp {
constructor () { constructor () {
this.app = express(); this.app = express();
this.secretKey = crypto.randomBytes(16).toString('hex'); this.secretKey = crypto.randomBytes(16).toString('hex');
this.sessionKey = 'restreamer-session';
this.sessionStore = new session.MemoryStore();
if (process.env.RS_NODE_ENV === 'dev') { if (process.env.RS_NODE_ENV === 'dev') {
this.initDev(); this.initDev();
@@ -56,33 +52,20 @@ class RestreamerExpressApp {
*/ */
useSessions () { useSessions () {
this.app.use(session({ this.app.use(session({
'resave': true,
'saveUninitialized': false,
'key': this.sessionKey,
'secret': this.secretKey, 'secret': this.secretKey,
'resave': false, 'unset': 'destroy',
'saveUninitialized': true // session secret 'store': this.sessionStore
})); }));
} }
/**
* use passport auth
*/
useAuth () {
// add passport auth
this.app.use(passport.initialize());
// persistent login sessions
this.app.use(passport.session());
// add config to passport
passportConfig(passport);
}
/** /**
* add automatic parsers for the body * add automatic parsers for the body
*/ */
addParsers () { addParsers () {
this.app.use(bodyParser.json()); this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({'extended': true}));
this.app.use(cookieParser()); this.app.use(cookieParser());
} }
@@ -118,7 +101,7 @@ class RestreamerExpressApp {
* add the restreamer routes * add the restreamer routes
*/ */
addRoutes () { addRoutes () {
indexRouter(this.app, passport); indexRouter(this.app);
this.app.use('/v1', apiV1); this.app.use('/v1', apiV1);
} }
@@ -148,44 +131,21 @@ class RestreamerExpressApp {
} }
/** /**
* check for app updates * enable websocket session validation
*/ */
checkForRestreamerUpdates () { secureSockets () {
const url = {'host': 'datarhei.org', 'path': '/apps.json'}; this.app.get('io').set('authorization', (handshakeData, accept) => {
logger.debug('Checking app for updates...'); if (handshakeData.headers.cookie) {
https.get(url, (response)=> { this.sessionStore.get(cookieParser.signedCookie(
if (response.statusCode === 200) { cookie.parse(handshakeData.headers.cookie)[this.sessionKey], this.secretKey
response.on('data', (body)=> { ), (err, s) => {
var updateCheck = JSON.parse(body); if (!err && s && s.authenticated) {
var updateAvailable = false; return accept(null, true);
if (updateCheck.restreamer.version === packageJson.version) {
updateAvailable = false;
logger.debug(`Checking app for updates successful. Update is not available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
} else {
updateAvailable = updateCheck.restreamer.version;
logger.debug(`Checking app for updates successful. Update is available (remote: ${updateCheck.restreamer.version}, local: ${packageJson.version})`);
} }
logger.info('Checking app for updates successful');
this.app.set('updateAvailable', updateAvailable);
}); });
} else { } else {
logger.info('Update check failed', false); return accept(null, false);
} }
}).on('error', () => {
logger.info('Update check failed', false);
});
}
/**
* get public ip of the app
*/
getPublicIp () {
logger.info('Getting public ip...', 'start.publicip');
exec('public-ip', (err, stdout, stderr) => {
if (err) {
logger.error(err);
}
this.app.set('publicIp', stdout.split('\n')[0]);
}); });
} }
@@ -201,9 +161,10 @@ class RestreamerExpressApp {
this.app.set('port', process.env.RS_NODE_PORT); this.app.set('port', process.env.RS_NODE_PORT);
server = this.app.listen(this.app.get('port'), ()=> { server = this.app.listen(this.app.get('port'), ()=> {
this.app.set('io', require('socket.io')(server)); this.app.set('io', require('socket.io')(server));
this.secureSockets();
this.app.set('server', server.address()); this.app.set('server', server.address());
// promise to determine if the webserver has been started to avoid ws binding before // promise to avoid ws binding before the webserver has been started
this.app.get('websocketsReady').resolve(this.app.get('io')); this.app.get('websocketsReady').resolve(this.app.get('io'));
logger.info(`Webserver running on port ${process.env.RS_NODE_PORT}`); logger.info(`Webserver running on port ${process.env.RS_NODE_PORT}`);
deferred.resolve(server.address().port); deferred.resolve(server.address().port);
@@ -217,7 +178,6 @@ class RestreamerExpressApp {
*/ */
initAlways () { initAlways () {
this.useSessions(); this.useSessions();
this.useAuth();
this.addParsers(); this.addParsers();
this.addCompression(); this.addCompression();
this.addExpressLogger(); this.addExpressLogger();
@@ -233,7 +193,7 @@ class RestreamerExpressApp {
logger.debug('init webserver with PROD environment'); logger.debug('init webserver with PROD environment');
this.initAlways(); this.initAlways();
this.app.get('/', (req, res)=> { this.app.get('/', (req, res)=> {
res.sendFile(path.join(global.__public, 'index.html')); res.sendFile(path.join(global.__public, 'index.prod.html'));
}); });
this.add404ErrorHandling(); this.add404ErrorHandling();
this.add500ErrorHandling(); this.add500ErrorHandling();
@@ -246,7 +206,7 @@ class RestreamerExpressApp {
logger.debug('init webserver with DEV environment'); logger.debug('init webserver with DEV environment');
this.initAlways(); this.initAlways();
this.app.get('/', (req, res)=> { this.app.get('/', (req, res)=> {
res.sendFile(path.join(global.__public, 'index-dev.html')); res.sendFile(path.join(global.__public, 'index.dev.html'));
}); });
this.add404ErrorHandling(); this.add404ErrorHandling();
this.add500ErrorHandling(); this.add500ErrorHandling();

View File

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

View File

@@ -8,9 +8,19 @@
const express = require('express'); const express = require('express');
const router = new express.Router(); const router = new express.Router();
const version = require(require('path').join(global.__base, 'package.json')).version;
// TODO: solve the circular dependency problem and place Restreamer require here // TODO: solve the circular dependency problem and place Restreamer require here
router.get('/version', (req, res) => {
res.json({
'version': version,
'update': require.main.require('./classes/Restreamer').data.updateAvailable
});
});
router.get('/ip', (req, res) => {
res.end(require.main.require('./classes/Restreamer').data.publicIp);
});
router.get('/states', (req, res) => { router.get('/states', (req, res) => {
const states = require.main.require('./classes/Restreamer').data.states; const states = require.main.require('./classes/Restreamer').data.states;

View File

@@ -8,30 +8,33 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
var auth = require(require('path').join(global.__base, 'conf', 'live.json')).auth;
module.exports = (app, passport) => { module.exports = (app) => {
// static paths
app.get('/favicon.ico', (req, res) => {
res.sendFile(path.join(global.__public, 'images', 'favicon.ico'));
});
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', (req, res, next) => {
passport.authenticate('local-login', { var username = process.env.RS_USERNAME || auth.username;
'successRedirect': '/', var password = process.env.RS_PASSWORD || auth.password;
'failureRedirect': '/#/login_invalid' var success = false;
}) var message = '';
); if (req.body.user === username && req.body.pass === password) {
req.session.authenticated = true;
success = true;
} else {
message = 'login_invalid';
req.session.destroy();
success = false;
}
res.json({
'success': success,
'message': message
});
});
app.get('/authenticated', (req, res) => {
res.json(req.session.authenticated === true);
});
app.get('/logout', (req, res) => { app.get('/logout', (req, res) => {
req.logout(); req.session.destroy();
res.redirect('/'); res.end();
}); });
}; };

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="app"> <html ng-app="app" ng-controller="appController">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -36,8 +36,8 @@
<!-- HEADER MODULE --> <!-- HEADER MODULE -->
<script src="/scripts/Header/HeaderModule.js"></script> <script src="/scripts/Header/HeaderModule.js"></script>
<script src="/scripts/Header/HeaderDirective.js"></script>
<script src="/scripts/Header/HeaderController.js"></script> <script src="/scripts/Header/HeaderController.js"></script>
<script src="/scripts/Header/HeaderDirective.js"></script>
<!-- MAIN MODULE --> <!-- MAIN MODULE -->
<script src="/scripts/Main/MainModule.js"></script> <script src="/scripts/Main/MainModule.js"></script>
@@ -49,8 +49,8 @@
<!-- FOOTER MODULE --> <!-- FOOTER MODULE -->
<script src="/scripts/Footer/FooterModule.js"></script> <script src="/scripts/Footer/FooterModule.js"></script>
<script src="/scripts/Footer/FooterDirective.js"></script>
<script src="/scripts/Footer/FooterController.js"></script> <script src="/scripts/Footer/FooterController.js"></script>
<script src="/scripts/Footer/FooterDirective.js"></script>
<!-- STREAMING INTERFACE MODULE --> <!-- STREAMING INTERFACE MODULE -->
<script src="/scripts/StreamingInterface/StreamingInterfaceModule.js"></script> <script src="/scripts/StreamingInterface/StreamingInterfaceModule.js"></script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="app"> <html ng-app="app" ng-controller="appController">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@@ -1,15 +0,0 @@
<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

@@ -7,32 +7,31 @@
<meta name="description" content="Restreamer"> <meta name="description" content="Restreamer">
<meta name="author" content="datarhei"> <meta name="author" content="datarhei">
<title>Restreamer</title> <title>Restreamer</title>
<link href="/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="/libs/jquery/dist/jquery.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>
<style>
.player-poster[data-poster] .poster-background[data-poster] {
height: initial !important;
}
</style>
</head> </head>
<body style="margin: 0; background-color: #000;"> <body>
<div class="container-fluid"> <div id="player" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
<div class="row"> <script>
<div> var player = new window.Clappr.Player({
<div class="embed-responsive embed-responsive-16by9"> 'source': (window.location.protocol === 'https:' ? 'https:' : 'http:') +
<div id="player" class="embed-responsive-item"></div> '//' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8',
</div> 'parentId': '#player',
<script> 'baseUrl': '/libs/clappr/dist/',
var player = new window.Clappr.Player({ 'poster': 'images/live.jpg?t=' + String(new Date().getTime()),
'source': (window.location.protocol === 'https:' ? 'https:' : 'http:') + 'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'},
'//' + window.location.hostname + ':' + window.location.port + '/hls/live.stream.m3u8', 'height': '100%',
'parentId': '#player', 'width': '100%'
'baseUrl': '/libs/clappr/dist/', });
'poster': 'images/live.jpg', var posterPlugin = player.core.mediaControl.container.getPlugin('poster');
'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'}, player.on(window.Clappr.Events.PLAYER_STOP, function updatePoster () {
'height': '100%', posterPlugin.options.poster = 'images/live.jpg?t=' + String(new Date().getTime());
'width': '100%' posterPlugin.render();
}); });
</script> </script>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -11,28 +11,30 @@ var app = window.angular.module('app', [
'pascalprecht.translate', 'pascalprecht.translate',
'Footer', 'Footer',
'Header', 'Header',
'Login',
'Main', 'Main',
'StreamingInterface', 'StreamingInterface'
'Login']); ]);
app.config(($stateProvider) => {
app.config(($stateProvider, $urlRouterProvider) => {
$urlRouterProvider.otherwise('/');
$stateProvider $stateProvider
.state('main', { .state('login', {
'templateUrl': 'main.html', 'controller': 'loginController',
'url': '/:error', 'templateUrl': 'views/login.html'
'controller': 'mainController'
}) })
.state('helpSource', { .state('logged-in', {
'templateUrl': 'help/source.html', 'controller': 'mainController',
'url': '/help/source' 'templateUrl': 'views/main.html'
})
.state('helpOptionalOutput', {
'templateUrl': 'help/optionalOutput.html',
'url': '/help/optionalOutput'
}); });
}); });
app.controller('appController',
['$rootScope', '$state', '$http', ($rootScope, $state, $http) => {
$http.get('/authenticated').then((response) => {
$rootScope.loggedIn = response.data;
});
$rootScope.$watch('loggedIn', (value) => {
$state.go(value ? 'logged-in' : 'login');
});
}]
);

View File

@@ -5,10 +5,17 @@
*/ */
'use strict'; 'use strict';
window.angular.module('Footer').controller('footerController', ['ws', '$scope', 'config', (ws, $scope, config) => { window.angular.module('Footer').controller('footerController',
ws.emit('getVersion'); ['ws', '$scope', '$http', '$rootScope', 'config', (ws, $scope, $http, $rootScope, config) => {
ws.on('version', (version) => { $http.get('/v1/version').then((response) => {
$scope.version = version; $scope.version = response.data.version;
$scope.config = config; $rootScope.checkForAppUpdatesResult = response.data.update;
}); $scope.config = config;
}]); });
$scope.logout = () => {
$http.get('/logout').then(() => {
$rootScope.loggedIn = false;
});
};
}]
);

View File

@@ -9,7 +9,7 @@ window.angular.module('Footer').directive('footer', () => {
return { return {
'restrict': 'A', 'restrict': 'A',
'replace': true, 'replace': true,
'templateUrl': '/scripts/Footer/_footer.html', 'templateUrl': '/views/footer.html',
'controller': 'footerController' 'controller': 'footerController'
}; };
}); });

View File

@@ -5,19 +5,21 @@
*/ */
'use strict'; 'use strict';
window.angular.module('Header').controller('headerController', ['$scope', '$translate', 'loggerService', ($scope, $translate, loggerService) => { window.angular.module('Header').controller('headerController',
$scope.currentLocale = $translate.preferredLanguage(); ['$scope', '$translate', 'loggerService', ($scope, $translate, loggerService) => {
$scope.switchLanguage = (locale) => { $scope.currentLocale = $translate.preferredLanguage();
$scope.currentLocale = locale; $scope.switchLanguage = (locale) => {
$translate.use(locale).then( $scope.currentLocale = locale;
() => { $translate.use(locale).then(
loggerService.info('Switched language to ' + locale); () => {
}, loggerService.info('Switched language to ' + locale);
(error) => { },
loggerService.error('INFO', 'Switching language to ' + locale + ' failed: ' + error); (error) => {
}); loggerService.error('INFO', 'Switching language to ' + locale + ' failed: ' + error);
}; });
$scope.langIs = function langIs (locale) { };
return locale === $scope.currentLocale; $scope.langIs = (locale) => {
}; return locale === $scope.currentLocale;
}]); };
}]
);

View File

@@ -9,7 +9,7 @@ window.angular.module('Header').directive('header', () => {
return { return {
'restrict': 'A', 'restrict': 'A',
'replace': true, 'replace': true,
'templateUrl': '/scripts/Header/_header.html', 'templateUrl': '/views/header.html',
'controller': 'headerController' 'controller': 'headerController'
}; };
}); });

View File

@@ -5,4 +5,13 @@
*/ */
'use strict'; 'use strict';
window.angular.module('Login').controller('loginController', [], () => {}); window.angular.module('Login').controller('loginController',
['$scope', '$http', '$rootScope', function loginController ($scope, $http, $rootScope) {
$scope.submit = function submit () {
$http.post('/login', {'user': $scope.user, 'pass': $scope.pass}).then((response) => {
$scope.message = response.data.message;
$rootScope.loggedIn = response.data.success;
});
};
}]
);

View File

@@ -5,5 +5,4 @@
*/ */
'use strict'; 'use strict';
// create new angular module
window.angular.module('Login', []); window.angular.module('Login', []);

View File

@@ -1,5 +1,5 @@
/** /**
* @file holds the Angularjs mainController * @file holds the AngularJS mainController
* @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
@@ -7,151 +7,153 @@
'use strict'; 'use strict';
window.angular.module('Main').controller('mainController', window.angular.module('Main').controller('mainController',
['ws', '$scope', '$location', '$rootScope', '$stateParams', 'config', ['ws', '$scope', '$location', '$rootScope', '$stateParams', 'config', function mainController (ws, $scope, $location, $rootScope, $stateParams, config) {
function mainController (ws, $scope, $location, $rootScope, $stateParams, config) { let setup = false;
let setup = false; let player = null;
let player = null; let posterPlugin = null;
if ($stateParams.error === 'login_invalid') { $scope.config = config;
$scope.login_invalid = 'login_invalid';
const updateSnapshot = () => {
if (posterPlugin !== null) {
posterPlugin.options.poster = 'images/live.jpg?t=' + String(new Date().getTime());
if (!player.isPlaying()) {
posterPlugin.render();
}
} }
};
$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?t=' + String(new Date().getTime()),
'mediacontrol': {'seekbar': '#3daa48', 'buttons': '#3daa48'},
'height': '100%',
'width': '100%'
});
posterPlugin = player.core.mediaControl.container.getPlugin('poster');
player.on(window.Clappr.Events.PLAYER_STOP, () => {
posterPlugin.render();
});
};
const initClappr = () => { $scope.optionalOutputInputInvalid = false;
player = new window.Clappr.Player({ $scope.nginxRepeatStreamInputInvalid = false;
'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.reStreamerData = {
'retryCounter': {
$scope.optionalOutputInputInvalid = false; 'repeatToLocalNginx': 0,
$scope.nginxRepeatStreamInputInvalid = false; 'repeatToOptionalOutput': 0
},
$scope.reStreamerData = { 'options': {
'retryCounter': { 'rtspTcp': false
'repeatToLocalNginx': 0, },
'repeatToOptionalOutput': 0 'states': {
'repeatToLocalNginx': {
'type': ''
}, },
'options': { 'repeatToOptionalOutput': {
'rtspTcp': false 'type': ''
},
'states': {
'repeatToLocalNginx': {
'type': ''
},
'repeatToOptionalOutput': {
'type': ''
}
},
'userActions': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'progresses': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'addresses': {
'optionalOutputAddress': '',
'srcAddress': ''
} }
}; },
'userActions': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'progresses': {
'repeatToLocalNginx': '',
'repeatToOptionalOutput': ''
},
'addresses': {
'optionalOutputAddress': '',
'srcAddress': ''
}
};
$rootScope.windowLocationPort = $location.port(); $rootScope.windowLocationPort = $location.port();
$scope.optionalOutput = ''; $scope.optionalOutput = '';
$scope.showStopButton = (streamType) => { $scope.showStopButton = (streamType) => {
return $scope.reStreamerData.userActions[streamType] === 'start'; return $scope.reStreamerData.userActions[streamType] === 'start';
}; };
$scope.showStartButton = (streamType) => { $scope.showStartButton = (streamType) => {
return $scope.reStreamerData.userActions[streamType] === 'stop'; return $scope.reStreamerData.userActions[streamType] === 'stop';
}; };
$scope.openPlayer = () => { $scope.openPlayer = () => {
if (player === null) { if (player === null) {
initClappr(); initClappr();
} }
$('#player-modal').modal('show').on('hide.bs.modal', function closeModal (e) { $('#player-modal').modal('show').on('hide.bs.modal', function closeModal (e) {
player.stop(); player.stop();
$(this).off('hide.bs.modal'); $(this).off('hide.bs.modal');
$(this).modal('hide'); $(this).modal('hide');
return e.preventDefault(); return e.preventDefault();
}); });
}; };
/**
* Configure Websockets
*/
ws.emit('checkStates'); // check states of hls and rtmp stream
// prohibit double binding of events
if (!setup) {
/* /*
* Configure Websockets * 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('snapshot', updateSnapshot);
}
// check states of hls and rtmp stream $scope.startStream = (streamType) => {
ws.emit('checkStates'); 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 = '';
// check for app updates if ($scope.activateOptionalOutput === true) {
ws.emit('checkForAppUpdates'); optionalOutput = $scope.reStreamerData.addresses.optionalOutputAddress;
// 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) => { if (streamType === 'repeatToOptionalOutput') {
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])?(?:\/.*)?/; $scope.optionalOutputInputInvalid = !rtmpRegex.test(optionalOutput);
var optionalOutput = ''; if ($scope.optionalOutputInputInvalid) {
return;
if ($scope.activateOptionalOutput === true) {
optionalOutput = $scope.reStreamerData.addresses.optionalOutputAddress;
} }
} else {
if (streamType === 'repeatToOptionalOutput') { $scope.nginxRepeatStreamInputInvalid = !rtmpRegex.test($scope.reStreamerData.addresses.srcAddress);
$scope.optionalOutputInputInvalid = !rtmpRegex.test(optionalOutput); if ($scope.nginxRepeatStreamInputInvalid) {
if ($scope.optionalOutputInputInvalid) { return;
return;
}
} else {
$scope.nginxRepeatStreamInputInvalid = !rtmpRegex.test($scope.reStreamerData.addresses.srcAddress);
if ($scope.nginxRepeatStreamInputInvalid) {
return;
}
} }
}
ws.emit('startStream', { ws.emit('startStream', {
'src': $scope.reStreamerData.addresses.srcAddress, 'src': $scope.reStreamerData.addresses.srcAddress,
'options': $scope.reStreamerData.options, 'options': $scope.reStreamerData.options,
'streamType': streamType, 'streamType': streamType,
'optionalOutput': optionalOutput 'optionalOutput': optionalOutput
}); });
}; };
$scope.stopStream = (streamType) => { $scope.stopStream = (streamType) => {
ws.emit('stopStream', streamType); ws.emit('stopStream', streamType);
}; };
}]); }]
);

View File

@@ -10,7 +10,7 @@
// styles of the logging output // 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: #FF0011; font-weight: bold';
const WEBSOCKETS_IN = 'color: #00BFFF; font-weight: bold'; 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';
@@ -71,7 +71,7 @@ const LoggerService = function loggerService () {
* @param {string} type * @param {string} type
*/ */
this.log = (style, message, type) => { this.log = (style, message, type) => {
console.log('%c [' + type + ']' + message, style); console.log('%c [' + type + '] ' + message, style);
}; };
}; };

View File

@@ -8,22 +8,32 @@
/* eslint no-undef: 0*/ /* eslint no-undef: 0*/
'use strict'; 'use strict';
const WebsocketsService = function websockeService ($rootScope, loggerService) { const WebsocketsService = function websocketsService ($rootScope, loggerService) {
this.$rootScope = $rootScope; this.$rootScope = $rootScope;
this.loggerService = loggerService; this.loggerService = loggerService;
this.socket = io.connect(); this.socket = null;
this.loggerService.websocketsNamespace('websockets connected');
$rootScope.$watch('loggedIn', (loggedIn) => {
if (loggedIn) {
this.socket = io.connect();
this.loggerService.websocketsNamespace('WS connected');
} else if (this.socket !== null) {
this.socket.disconnect();
this.loggerService.websocketsNamespace('WS disconnected');
}
});
/** /**
* emit an event to socket * emit an event to socket
* @param event * @param event
* @param data * @param data
* @returns {WebsocketsService} * @returns {websocketsService}
*/ */
this.emit = (event, data) => { this.emit = (event, data) => {
this.loggerService.websocketsOut(`emit event "${event}"`); if (this.socket) {
this.socket.emit(event, data); this.loggerService.websocketsOut(`emit event "${event}"`);
this.socket.emit(event, data);
}
return this; return this;
}; };
@@ -31,17 +41,19 @@ const WebsocketsService = function websockeService ($rootScope, loggerService) {
* react on an event to socket with callback * react on an event to socket with callback
* @param event * @param event
* @param {function} callback * @param {function} callback
* @returns {WebsocketsService} * @returns {websocketsService}
*/ */
this.on = (event, callback) => { this.on = (event, callback) => {
var self = this; var self = this;
this.loggerService.websocketsIn(`got event "${event}"`); if (this.socket) {
this.socket.on(event, function woEvent () { this.loggerService.websocketsIn(`got event "${event}"`);
var args = arguments; this.socket.on(event, function woEvent () {
self.$rootScope.$apply(function weApply () { var args = arguments;
callback.apply(null, args); self.$rootScope.$apply(function weApply () {
callback.apply(null, args);
});
}); });
}); }
return this; return this;
}; };
@@ -51,7 +63,9 @@ const WebsocketsService = function websockeService ($rootScope, loggerService) {
* @param callback * @param callback
*/ */
this.off = (event, callback) => { this.off = (event, callback) => {
this.socket.removeListener(event, callback); if (this.socket) {
this.socket.removeListener(event, callback);
}
}; };
}; };

View File

@@ -8,7 +8,7 @@
/** /**
* Streaming Status Controller * Streaming Status Controller
* *
* controlls the display of the streaming status (fps, status) * controls the display of the streaming status (fps, status)
*/ */
window.angular.module('StreamingInterface').controller('streamingStatusController', window.angular.module('StreamingInterface').controller('streamingStatusController',
['$scope', ($scope) => { ['$scope', ($scope) => {

View File

@@ -13,7 +13,7 @@ window.angular.module('StreamingInterface').directive('streamingStatus', () => {
}, },
'restrict': 'E', 'restrict': 'E',
'replace': true, 'replace': true,
'templateUrl': '/scripts/StreamingInterface/_streamingStatus.html', 'templateUrl': '/views/status.html',
'controller': 'streamingStatusController' 'controller': 'streamingStatusController'
}; };
}); });

View File

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

View File

@@ -0,0 +1,22 @@
<form ng-submit="submit()">
<div class="form-group ng-scope">
<input id="input_username"
class="form-control input ng-pristine ng-untouched ng-valid"
type="text"
name="user"
ng-model="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"
ng-model="pass"
placeholder="{{'login_password' | translate}}">
</div>
<div class="jumbotron progress-bar-danger progress-bar-striped" ng-if="message">{{message | 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

@@ -1,7 +1,6 @@
<!-- <!--
Repeat to local nginx Repeat to local nginx
--> -->
<meta ng-init="$root.loggedIn = true">
<div class="form-group"> <div class="form-group">
<label> <label>
{{'input_title' | translate}} {{'input_title' | translate}}