├── LICENSE ├── README.md ├── backup_publish_done.sh ├── config.sh ├── init.sh ├── main_publish.sh ├── main_publish_done.sh ├── nginx_example.conf └── utils.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gorokhova-Alekseeva Nastia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX-RTMP-BACKUP 2 | This is a set of shell scripts that allows you to implement elementary backups for your RTMP streams. 3 | ## Prerequisites 4 | You have to have [avconv](https://libav.org/avconv.html) (or [ffmpeg](https://www.ffmpeg.org/)) and [nginx](https://nginx.ru/en/) with [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) installed on your machine. 5 | 6 | ## Installation 7 | 1. Download files from this repo (or clone it locally); 8 | 2. Place them into a directory readable by nginx (e.g. `/usr/local/share/nginx-rtmp-backup/`); 9 | 3. Make necessary modifications to `config.sh` (see [below](#configure-scripts)); 10 | 4. Set execution permissions for `init.sh` (`chmod a+x init.sh`); 11 | 5. Run `init.sh` (`./init.sh`); 12 | 6. Configure your nginx-rtmp-module (see [below](#configure-nginx-rtmp)). 13 | 14 | ### Configure scripts 15 | It is necessary to set a few parameters for the scripts to work properly: 16 | * `MAIN_STREAM_APPNAME` 17 | Should match the name of a nginx-rtmp application accepting the main stream. 18 | Default is `main`. 19 | * `BACKUP_STREAM_APPNAME` 20 | Should match the name of a nginx-rtmp application accepting the backup stream. 21 | Default is `backup`. 22 | * `OUT_STREAM_APPNAME` 23 | Should match the name of a nginx-rtmp application where the stream should finaly appear. 24 | Default is `out`. 25 | * `MAIN_STREAM_PRIORITY` 26 | `true` or `false`. If set to `true`, the main stream will be pushed to out stream each time it recovers. If set to `false`, once the out stream switches to backup, it will stay there. 27 | Default is `true`. 28 | * `RUNNER` 29 | `avconv` or `ffmpeg`. Defines which program will push streams. 30 | Default is `avconv`. 31 | * `NGINX_USER` 32 | A username nginx workers runs under. Required for setting right permissions for logs and pids folders. 33 | Default is `nobody`. 34 | * `NGINX_GROUP` 35 | A group `NGINX_USER` belongs to. 36 | Default is `nogroup`. 37 | 38 | ### Configure nginx-rtmp 39 | An example configuration matching the default config is presented in `nginx_example.conf`. 40 | Basically, you need to create three applications, one accepting the main stream, another accepting the backup stream, and the third, where your final stream will appear. 41 | 42 | ```rtmp { 43 | server { 44 | listen 1935; 45 | 46 | # An application where the final stream will appear. 47 | # Its name should match $OUT_STREAM_APPNAME in config.sh. 48 | application out { 49 | # Enable live streaming. 50 | live on; 51 | } 52 | 53 | # An application for main incoming streams. 54 | # Its name should match $MAIN_STREAM_APPNAME in config.sh. 55 | application main { 56 | # Enable live streaming. 57 | live on; 58 | 59 | # This will prevent avconv/ffmpeg from hanging when stream ends. 60 | # We will kill it from scripts anyway, but just in case. 61 | play_restart on; 62 | 63 | # You may want this in case not to allow anyone to watch streams from this point. 64 | deny play all; 65 | # However, we need `out` app to have access. 66 | allow play 127.0.0.1; 67 | 68 | # That's where the magic starts. 69 | # Do not forget to change paths. 70 | # Output for scripts is already redirected, see README#Usage#Logs. 71 | 72 | # When any stream starts publishing to this app, 73 | # we call main_publish.sh and provide a streamname as a parameter. 74 | exec_publish /usr/local/share/nginx-rtmp-backup/main_publish.sh $name; 75 | # When stream stops publishing, 76 | # call main_publish_done.sh and pass a streamname to it. 77 | exec_publish_done /usr/local/share/nginx-rtmp-backup/main_publish_done.sh $name; 78 | } 79 | 80 | # An application for backup incoming streams. 81 | # Its name should match $BACKUP_STREAM_APPNAME in config.sh. 82 | # Everything is the same as for `main` app. 83 | application backup { 84 | live on; 85 | play_restart on; 86 | deny play all; 87 | allow play 127.0.0.1; 88 | 89 | # When stream stops publishing, 90 | # call backup_publish_done.sh and pass a streamname to it. 91 | exec_publish_done /usr/local/share/nginx-rtmp-backup/backup_publish_done.sh $name; 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | 98 | ## Usage 99 | After you have installed and configured scripts, simply send two streams to the apps (`$MAIN_STREAM_APPNAME` and `$BACKUP_STREAM_APPNAME`) with identical streamnames (keys) and watch them in final app (`$OUT_STREAM_APPNAME`). 100 | For example, if you have specified the following names for nginx-rtmp apps: 101 | `MAIN_STREAM_APPNAME="main"` 102 | `BACKUP_STREAM_APPNAME="backup"` 103 | `OUT_STREAM_APPNAME="out"`, 104 | and then sent your streams to `rtmp://your.domain/main/test` and `rtmp://your.domain/backup/test`, you can watch the output stream at `rtmp://your.domain/out/test`. 105 | When switching between streams, you may see a slight delay, as avconv/ffmpeg needs time to run. 106 | ### Logs 107 | All logs are stored at `/var/log/nginx-rtmp/backup`. 108 | Logs for avconv/ffmpeg are stored under the names `main_$streamname.log` and `backup_$streamname.log`, where `$streamname` is the RTMP key you send your stream to. 109 | Logs for scripts are stored in the subdirectory `scripts` named after the scripts themselves. 110 | 111 | -------------------------------------------------------------------------------- /backup_publish_done.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script runs if a backup stream stops publishing to nginx 4 | 5 | set -euf 6 | 7 | DIR="$(dirname "$0")" 8 | . "$DIR/config.sh" 9 | . "$DIR/utils.sh" # parse_argv, kill 10 | 11 | exec > "$LOGS_FOLDER/scripts/main_publish.log" 2>&1 12 | 13 | parse_argv "$@" 14 | 15 | kill backup 16 | 17 | exit 0 18 | -------------------------------------------------------------------------------- /config.sh: -------------------------------------------------------------------------------- 1 | # configuration file for nginx-rtmp-backup 2 | 3 | # [true|false] defines whether or not stream changes back to main if it recovers 4 | # with true it will change back, with false -- stay on backup stream 5 | MAIN_STREAM_PRIORITY="true" 6 | 7 | # [avconv|ffmpeg] defines which program will push your stream 8 | # use avconv if not sure 9 | RUNNER="avconv" 10 | 11 | # nginx rtmp application name for main stream 12 | MAIN_STREAM_APPNAME="main" 13 | 14 | # nginx rtmp application name for backup stream 15 | BACKUP_STREAM_APPNAME="backup" 16 | 17 | # nginx rtmp application name for final stream 18 | OUT_STREAM_APPNAME="out" 19 | 20 | # username for nginx worker processes 21 | NGINX_USER="nobody" 22 | # group for NGINX_USER 23 | NGINX_GROUP="nogroup" 24 | 25 | # Following parameters are technical. Please, DO NOT CHANGE them. 26 | # If changed anyway, run init.sh again. 27 | 28 | # app name 29 | CURRENT_APPLICATION_NAME="nginx-rtmp-backup" 30 | 31 | # folder where pidfiles are stored 32 | PIDS_FOLDER="/run/$CURRENT_APPLICATION_NAME" 33 | 34 | # folder where logfiles are stored 35 | LOGS_FOLDER="/var/log/$CURRENT_APPLICATION_NAME" 36 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script creates folders required by other scripts 4 | # and sets correct permissions for these folders and scripts 5 | 6 | set -euf 7 | 8 | DIR="$(dirname "$0")" 9 | . "$DIR/config.sh" 10 | 11 | for folder in "$PIDS_FOLDER" "$LOGS_FOLDER" "$LOGS_FOLDER/scripts" ; do 12 | mkdir -p "$folder" 13 | chown $NGINX_USER:$NGINX_GROUP "$folder" 14 | done 15 | 16 | for file in "main_publish" "main_publish_done" "backup_publish_done" ; do 17 | chmod a+x "$DIR/$file.sh" 18 | done 19 | 20 | exit 0 21 | -------------------------------------------------------------------------------- /main_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script runs when a main stream is published to nginx 4 | 5 | set -euf 6 | 7 | DIR="$(dirname "$0")" 8 | . "$DIR/config.sh" 9 | . "$DIR/utils.sh" # parse_argv, assert_one_of, kill, is_running, push_stream 10 | 11 | exec > "$LOGS_FOLDER/scripts/main_publish.log" 2>&1 12 | 13 | parse_argv "$@" 14 | assert_one_of MAIN_STREAM_PRIORITY true false 15 | 16 | # Kill backup pushing process if priority is for main stream 17 | [ "$MAIN_STREAM_PRIORITY" = true ] && kill backup || : # || : prevents falling because of set -e 18 | # If none of streams is pushing, push main 19 | is_running main || is_running backup || push_stream main 20 | 21 | exit 0 22 | -------------------------------------------------------------------------------- /main_publish_done.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script runs when a main stream stops publishing to nginx 4 | 5 | set -euf 6 | 7 | DIR="$(dirname "$0")" 8 | . "$DIR/config.sh" 9 | . "$DIR/utils.sh" # parse_argv, is_running, kill, push_stream 10 | 11 | exec > "$LOGS_FOLDER/scripts/main_publish_done.log" 2>&1 12 | 13 | parse_argv "$@" 14 | 15 | # Stop pushing main stream. 16 | # In case stopping avconv/ffmpeg needs some time, use a loop. 17 | # Otherwise pushing backup may be denied with 'already publishing' error 18 | while is_running main; do 19 | kill main 20 | sleep 0.1 21 | done 22 | 23 | # If backup is not pushing yet, push it 24 | is_running backup || push_stream backup 25 | 26 | exit 0 27 | -------------------------------------------------------------------------------- /nginx_example.conf: -------------------------------------------------------------------------------- 1 | rtmp { 2 | server { 3 | listen 1935; 4 | 5 | # An application where the final stream will appear. 6 | # Its name should match $OUT_STREAM_APPNAME in config.sh. 7 | application out { 8 | # Enable live streaming. 9 | live on; 10 | } 11 | 12 | # An application for main incoming streams. 13 | # Its name should match $MAIN_STREAM_APPNAME in config.sh. 14 | application main { 15 | # Enable live streaming. 16 | live on; 17 | 18 | # This will prevent avconv/ffmpeg from hanging when stream ends. 19 | # We will kill it from scripts anyway, but just in case. 20 | play_restart on; 21 | 22 | # You may want this in case not to allow anyone to watch streams from this point. 23 | deny play all; 24 | # However, we need `out` app to have access. 25 | allow play 127.0.0.1; 26 | 27 | # That's where the magic starts. 28 | # Do not forget to change paths. 29 | # Output for scripts is already redirected, see README#Usage#Logs. 30 | 31 | # When any stream starts publishing to this app, 32 | # we call main_publish.sh and provide a streamname as a parameter. 33 | exec_publish /usr/local/share/nginx-rtmp-backup/main_publish.sh $name; 34 | # When stream stops publishing, 35 | # call main_publish_done.sh and pass a streamname to it. 36 | exec_publish_done /usr/local/share/nginx-rtmp-backup/main_publish_done.sh $name; 37 | } 38 | 39 | # An application for backup incoming streams. 40 | # Its name should match $BACKUP_STREAM_APPNAME in config.sh. 41 | # Everything is the same as for `main` app. 42 | application backup { 43 | live on; 44 | play_restart on; 45 | deny play all; 46 | allow play 127.0.0.1; 47 | 48 | # When stream stops publishing, 49 | # call backup_publish_done.sh and pass a streamname to it. 50 | exec_publish_done /usr/local/share/nginx-rtmp-backup/backup_publish_done.sh $name; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /utils.sh: -------------------------------------------------------------------------------- 1 | # This file contains functions required by other nginx-rtmp-backup scripts 2 | 3 | die() { # Exit with the proper stderr output 4 | echo "$CURRENT_APPLICATION_NAME: $*" >&2 5 | exit 1 6 | } 7 | 8 | parse_argv() { # Checks that the streamname for scripts is provided 9 | # and set the variable 10 | [ "$#" -ge 1 ] || die "too few arguments" 11 | STREAMNAME=$1 12 | } 13 | 14 | pid_for() { # Gets a pid, according to stream kind (main/backup) and streamname 15 | echo "$PIDS_FOLDER/$1_$STREAMNAME.pid" 16 | } 17 | 18 | is_running() { # Checks if the process pushing stream 19 | # of provided kind (main/backup) is running 20 | pidfile="$(pid_for "$1")" 21 | echo $pidfile 22 | [ -r "$pidfile" ] && pgrep --pidfile "$pidfile" 23 | } 24 | 25 | get_var() { # Gets variable value by its name 26 | eval echo "$"$1"" 27 | } 28 | 29 | kill() { # Kills a process pushing stream of provided kind (main/backup) 30 | # if it is running and removes its pidfile 31 | if is_running "$1"; then 32 | pidfile="$(pid_for "$1")" 33 | echo "got pidfile for killing" 34 | /bin/kill -9 "$(cat "$pidfile")" > /dev/null 35 | rm -f "$pidfile" 36 | fi 37 | } 38 | 39 | assert_one_of() { # Checks that the value for a variable provided in config is rigth 40 | varname="$1"; shift # Get variable name and remove it from arguments list 41 | value="$(get_var $varname)" 42 | expected="$*" # Values left in arguments list are expected values 43 | 44 | while [ "$#" -gt 0 ]; do 45 | if [ "$value" = "$1" ]; then return; fi # If a value of the varibale is one of the expected, return 46 | shift 47 | done 48 | 49 | # If we are here, the variable value do not match any of expected, exit 50 | die "unexpected value \`$value' for \`$varname' (expected one of '$expected')" 51 | } 52 | 53 | push_stream() { # Starts pushing stream 54 | stream_kind="$1" # backup or main 55 | # Get a value of either $MAIN_STREAM_NAME or $BACKUP_STREAM_NAME 56 | appname="$(get_var "$(echo "${stream_kind}_STREAM_APPNAME" | tr '[:lower:]' '[:upper:]')")" 57 | 58 | LOGFILE="$LOGS_FOLDER/${appname}_${STREAMNAME}.log" 59 | 60 | assert_one_of RUNNER avconv ffmpeg 61 | 62 | nohup "$RUNNER" \ 63 | -re -i "rtmp://localhost/$appname/$STREAMNAME" \ 64 | -c copy -f flv \ 65 | "rtmp://localhost/$OUT_STREAM_APPNAME/$STREAMNAME" \ 66 | \ 67 | "$LOGFILE" \ 69 | 2>&1 \ 70 | & 71 | 72 | echo $! > "$(pid_for "$stream_kind")" 73 | } 74 | --------------------------------------------------------------------------------