├── .gitignore ├── merger ├── log-date.sh ├── entrypoint.sh ├── cronfile ├── Dockerfile ├── start.sh ├── cleanup.sh ├── upload.sh ├── timelapse.sh ├── merge.sh └── mosaic.sh ├── recorder ├── Dockerfile └── record.sh ├── example.env ├── LICENCE.md ├── docker-compose.yml ├── README.md └── .github └── workflows └── build.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /merger/log-date.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export TZ=Asia/Kolkata 3 | echo $(date) 4 | -------------------------------------------------------------------------------- /merger/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | declare -x >> /etc/environment 3 | cron -f -L /dev/stdout 4 | -------------------------------------------------------------------------------- /merger/cronfile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | BASH_ENV=/etc/environment 3 | 0 * * * * /app/log-date.sh > /proc/1/fd/1 2>/proc/1/fd/2 4 | 20 0 * * * /app/start.sh > /proc/1/fd/1 2>/proc/1/fd/2 5 | -------------------------------------------------------------------------------- /recorder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ENV TZ=Asia/Kolkata 5 | RUN apt-get update \ 6 | && apt-get install -yq software-properties-common tzdata 7 | RUN add-apt-repository ppa:jonathonf/ffmpeg-4 8 | RUN apt-get install ffmpeg -y 9 | 10 | COPY record.sh /app/record.sh 11 | ENV TIMEOUT_BUFFER 30 12 | ENV LOG_LEVEL error 13 | 14 | ENTRYPOINT ["/app/record.sh"] 15 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | ############### DOCKER COMPOSE ############ 2 | OUT=/mnt/data/cctv 3 | 4 | ################### COMMON ############# 5 | TZ=Asia/Kolkata 6 | 7 | ################### RECORDER ################# 8 | FORMAT=mkv 9 | DURATION=600 10 | SEGMENT_FORMAT=%d-%m-%Y_%H%M%S 11 | 12 | ##################### MERGE ######################### 13 | # CLEANUP_SPACE is in GBs 14 | CLEANUP_SPACE=45 15 | CLEANUP_REGEX=.*-[0-9]+_\(cam[0-9]\|mosaic\).* 16 | CLEANUP_DIR=/out/ 17 | EMAIL= 18 | ACCESS_TOKEN= 19 | -------------------------------------------------------------------------------- /merger/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update \ 5 | && apt-get install -yq software-properties-common tzdata cron curl 6 | ENV TZ=Asia/Kolkata 7 | RUN add-apt-repository ppa:jonathonf/ffmpeg-4 8 | RUN apt-get install ffmpeg -y 9 | 10 | 11 | COPY *.sh /app/ 12 | 13 | RUN chmod 0744 /app/*.sh 14 | 15 | COPY cronfile /etc/cron.d/cronfile 16 | RUN chmod 0644 /etc/cron.d/cronfile 17 | RUN touch /var/log/cron.log 18 | 19 | RUN crontab /etc/cron.d/cronfile 20 | 21 | ENTRYPOINT ["/app/entrypoint.sh"] 22 | -------------------------------------------------------------------------------- /merger/start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | /app/cleanup.sh 4 | 5 | echo "starting to merge videos for last 10 days" 6 | for day in {1..10}; do 7 | for d in /out/*; do 8 | if [ -d "$d" ]; then 9 | /app/merge.sh $day "$d"; 10 | fi 11 | done 12 | done 13 | echo "merging complete" 14 | 15 | echo "generate timelapse video for last 3 days" 16 | for day in {1..3}; do 17 | for d in /out/*; do 18 | if [ -d "$d" ]; then 19 | /app/timelapse.sh $day "$d"; 20 | fi 21 | done 22 | done 23 | echo "timelapse generation complete" 24 | 25 | echo "creating mosaic video" 26 | for day in {1..2}; do 27 | /app/mosaic.sh $day /out; 28 | done 29 | -------------------------------------------------------------------------------- /recorder/record.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #export TZ=Asia/Kolkata 3 | #export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/ 4 | echo $(date) 5 | #NAME=$1 6 | #DURATION=$2 7 | #RTSP_URL=$3 8 | #FORMAT=$4 9 | #SEGMENT_FORMAT=$5 10 | #TIMEOUT_BUFFER=$6 11 | if [ ! -e /out/$NAME ]; then 12 | mkdir /out/$NAME 13 | fi 14 | chmod 775 /out/$NAME 15 | TIMEOUT=$(( $DURATION + $TIMEOUT_BUFFER )) 16 | 17 | while true 18 | do 19 | echo "started at $(date)" 20 | echo "should timeout after $DURATION" 21 | timeout --kill-after $TIMEOUT_BUFFER -v $DURATION \ 22 | ffmpeg -nostdin -loglevel $LOG_LEVEL \ 23 | -y -i $RTSP_URL -timeout 60000000 \ 24 | -vsync 1 -async -1 -an -vcodec copy \ 25 | -t $DURATION \ 26 | -f segment -strftime 1 -segment_time $DURATION \ 27 | -segment_format $FORMAT "/out/${NAME}/${SEGMENT_FORMAT}_${NAME}.$FORMAT" \ 28 | -reset_timestamps 1 -segment_atclocktime 1 29 | 30 | echo "stopped at $(date)" 31 | sleep 2; 32 | done 33 | -------------------------------------------------------------------------------- /merger/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export TZ=Asia/Kolkata 3 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/ 4 | DIR=$CLEANUP_DIR 5 | LIMIT=$CLEANUP_SPACE 6 | DEL_PATTERN=$CLEANUP_REGEX 7 | DELETED=0 8 | echo "Running on $(date)" 9 | 10 | if [[ ! "$DIR" =~ ^/out/ ]]; then 11 | echo "DIR does not started with /out quitting" 12 | exit 0 13 | fi 14 | 15 | AVAIL=$(df $DIR --output=avail | tail -n 1) 16 | AVAIL_GB=$(($AVAIL/1024/1024)) 17 | echo "${AVAIL_GB} GB Available" 18 | while [ $AVAIL_GB -lt $LIMIT ]; do 19 | echo "$LIMIT GB needed, need cleanup" 20 | FILE=$(find $DIR -type f -regex $DEL_PATTERN -printf "%T+ %p\n" | sort | cut -c 32- | head -n 1) 21 | echo "removing $FILE" 22 | echo "$(du -h $FILE)" 23 | rm $FILE 24 | AVAIL=$(df $DIR --output=avail | tail -n 1) 25 | AVAIL_GB=$(($AVAIL/1024/1024)) 26 | echo "${AVAIL_GB} GB Available" 27 | ((DELETED++)) 28 | done 29 | echo "deleted $DELETED files" 30 | -------------------------------------------------------------------------------- /merger/upload.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | CAMID=$1 3 | DATE=$2 4 | FILE=$3 5 | 6 | if [ "$CAMID" == "cam1" ]; then 7 | LOCATION="Indian City" 8 | else 9 | LOCATION="Indian Village" 10 | fi 11 | 12 | TITLE="[$CAMID] ${LOCATION} - ${DATE}" 13 | 14 | DESCRIPTION="Live CCTV footage from ${LOCATION} captured on ${DATE}. Watch and stay updated with real-time events in the ${LOCATION}." 15 | 16 | 17 | PAYLOAD=$(cat <> $OUT 22 | else 23 | echo "corrupted file '$f' ignored" 24 | fi 25 | echo "$f" >> $DELETE 26 | done 27 | 28 | if [ -f $OUT ]; then 29 | 30 | cat $OUT 31 | 32 | LINES=$(wc -l < $OUT) 33 | if [ $LINES -ge "2" ]; then 34 | echo "merging videos" 35 | ffmpeg -loglevel error -f concat -safe 0 -i $OUT -c copy $MERGED 36 | echo "videos merged to $MERGED" 37 | 38 | if [ $? -eq 0 ] && [ -f $MERGED ] ; then 39 | echo "deleting segment videos" 40 | for f in $(cat $DELETE) ; do 41 | rm "$f" 42 | done 43 | fi 44 | fi 45 | rm $OUT 46 | 47 | fi 48 | 49 | if [ -f $DELETE ]; then 50 | rm $DELETE 51 | fi 52 | 53 | #echo "convert to 720p" 54 | #ffmpeg -loglevel error -i $MERGED_TMP -filter:v "scale=-1:720" $MERGED 55 | #if [ $? -eq 0 ] && [ -f $MERGED ] ; then 56 | # echo "vide converted to 720 $MERGED" 57 | # echo "deleting original video" 58 | # rm "$MERGED_TMP" 59 | #fi 60 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | cam1: 5 | build: ./recorder 6 | network_mode: host 7 | environment: 8 | - NAME=cam1 9 | - RTSP_URL=rtsp://10.0.0.254:8554/ald-1 10 | env_file: 11 | - .env 12 | volumes: 13 | - ${OUT}:/out/ 14 | restart: always 15 | 16 | cam2: 17 | build: ./recorder 18 | environment: 19 | - NAME=cam2 20 | - RTSP_URL=rtsp://10.0.0.254:8554/ah-car 21 | env_file: 22 | - .env 23 | network_mode: host 24 | volumes: 25 | - ${OUT}:/out/ 26 | restart: always 27 | 28 | cam3: 29 | build: ./recorder 30 | environment: 31 | - NAME=cam3 32 | - RTSP_URL=rtsp://10.0.0.254:8554/ah-inside 33 | env_file: 34 | - .env 35 | network_mode: host 36 | volumes: 37 | - ${OUT}:/out/ 38 | restart: always 39 | 40 | cam4: 41 | build: ./recorder 42 | environment: 43 | - NAME=cam4 44 | - RTSP_URL=rtsp://10.0.0.254:8554/ah-outside 45 | env_file: 46 | - .env 47 | network_mode: host 48 | volumes: 49 | - ${OUT}:/out/ 50 | restart: always 51 | 52 | cam5: 53 | build: ./recorder 54 | environment: 55 | - NAME=cam5 56 | - RTSP_URL=rtsp://10.0.0.254:8554/ah-godrej 57 | env_file: 58 | - .env 59 | network_mode: host 60 | volumes: 61 | - ${OUT}:/out/ 62 | restart: always 63 | 64 | merge: 65 | build: ./merger 66 | env_file: 67 | - .env 68 | volumes: 69 | - ${OUT}:/out/ 70 | restart: always 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTSP Stream Recorder 2 | 3 | This is a Docker and Docker Compose based solution for recording 5 RTSP streams using FFmpeg and Bash scripts. It provides the following functionality: 4 | 5 | ## Recording 6 | 7 | - Record each stream as 10 minutes clips 8 | 9 | ## Merging 10 | 11 | - At the end of the day, the clips will be merged into one 24 hour video 12 | 13 | ## Timelapse 14 | 15 | - Create a timelapse video of the past day's footage 16 | 17 | ## Disk Space Management 18 | 19 | - Cleanup old recorded videos if disk space is running low 20 | 21 | ## Getting Started 22 | 23 | 1. Make sure you have [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. 24 | 2. Update the RTSP stream URLs in the `docker-compose.yml` configuration file. 25 | 3. Run `docker-compose up -d` to start the recording and merging containers. 26 | 4. The recorded videos will be saved in the `output` directory. 27 | 28 | ## Images 29 | 30 | - recorder: container that records the RTSP streams 31 | - merger: container that runs a cronjob at the end of the day to merge the clips and create a timelapse video 32 | 33 | ## Troubleshooting 34 | 35 | - If you encounter any issues while running the containers, you can check the logs by running `docker logs [container-name]`. 36 | - If you continue to have trouble, please open an issue on the project's Github page. 37 | 38 | ## Contribute 39 | 40 | If you want to contribute to this project, you can create a fork and send a pull request. 41 | 42 | ## License 43 | 44 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). 45 | -------------------------------------------------------------------------------- /merger/mosaic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export TZ=Asia/Kolkata 3 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/ 4 | 5 | MDAYS=$1 6 | DIR=$2 7 | DATE=$(date +"%d-%m-%Y" -d "$MDAYS days ago") 8 | echo "Running on $(date) - for ${DATE}" 9 | 10 | OUT_VID="${DIR}/${DATE}_mosaic.mkv" 11 | if [ -f $OUT_VID ]; then 12 | echo "mosaic file '$OUT_VID' already exists! quitting" 13 | exit 14 | fi 15 | 16 | IN_VID1="${DIR}/cam1/${DATE}_cam1_timelapse.mkv" 17 | IN_VID2="${DIR}/cam2/${DATE}_cam2_timelapse.mkv" 18 | IN_VID3="${DIR}/cam3/${DATE}_cam3_timelapse.mkv" 19 | IN_VID4="${DIR}/cam4/${DATE}_cam4_timelapse.mkv" 20 | 21 | if [ ! -f $IN_VID1 ]; then 22 | echo "source file '$IN_VID1' does not exists! using nullsrc" 23 | IN_VID1=nullsrc=size=960x540 24 | fi 25 | if [ ! -f $IN_VID2 ]; then 26 | echo "source file '$IN_VID2' does not exists! using nullsrc" 27 | IN_VID2=nullsrc=size=960x540 28 | fi 29 | if [ ! -f $IN_VID3 ]; then 30 | echo "source file '$IN_VID3' does not exists! using nullsrc" 31 | IN_VID3=nullsrc=size=960x540 32 | fi 33 | if [ ! -f $IN_VID4 ]; then 34 | echo "source file '$IN_VID4' does not exists! using nullsrc" 35 | IN_VID4=nullsrc=size=960x540 36 | fi 37 | 38 | 39 | echo "mosaic file does not exists, proceeding to create one" 40 | ffmpeg -loglevel error -i $IN_VID1 -i $IN_VID2 -i $IN_VID3 -i $IN_VID4 -filter_complex "nullsrc=size=640x480 [base]; [0:v] setpts=PTS-STARTPTS, scale=320x240 [upperleft]; [1:v] setpts=PTS-STARTPTS, scale=320x240 [upperright]; [2:v] setpts=PTS-STARTPTS, scale=320x240 [lowerleft]; [3:v] setpts=PTS-STARTPTS, scale=320x240 [lowerright]; [base][upperleft] overlay=shortest=1 [tmp1]; [tmp1][upperright] overlay=shortest=1:x=320 [tmp2]; [tmp2][lowerleft] overlay=shortest=1:y=240 [tmp3]; [tmp3][lowerright] overlay=shortest=1:x=320:y=240" -c:v libx264 $OUT_VID 41 | echo "mosaic video created at $OUT_VID" 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build-and-push-merger: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Set output for version 19 | id: date 20 | run: echo "::set-output name=version::$(date +'%Y.%m.%d-%H%M')" 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Login to GitHub Container Registry 29 | uses: docker/login-action@v2 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.repository_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Build and push Docker image for merger 36 | uses: docker/build-push-action@v3 37 | with: 38 | context: merger 39 | push: true 40 | platforms: linux/amd64,linux/arm64 41 | cache-from: type=gha 42 | cache-to: type=gha,mode=max 43 | tags: | 44 | ghcr.io/${{github.repository}}-merger:${{ steps.date.outputs.version }} 45 | ghcr.io/${{github.repository}}-merger:latest 46 | 47 | build-and-push-recorder: 48 | runs-on: ubuntu-latest 49 | permissions: 50 | packages: write 51 | steps: 52 | - name: Checkout code 53 | uses: actions/checkout@v3 54 | 55 | - name: Set output for version 56 | id: date 57 | run: echo "::set-output name=version::$(date +'%Y.%m.%d-%H%M')" 58 | 59 | - name: Set up QEMU 60 | uses: docker/setup-qemu-action@v2 61 | 62 | - name: Set up Docker Buildx 63 | uses: docker/setup-buildx-action@v2 64 | 65 | - name: Login to GitHub Container Registry 66 | uses: docker/login-action@v2 67 | with: 68 | registry: ghcr.io 69 | username: ${{ github.repository_owner }} 70 | password: ${{ secrets.GITHUB_TOKEN }} 71 | - name: Build and push Docker image for recorder 72 | uses: docker/build-push-action@v3 73 | with: 74 | context: recorder 75 | push: true 76 | platforms: linux/amd64,linux/arm64 77 | cache-from: type=gha 78 | cache-to: type=gha,mode=max 79 | tags: | 80 | ghcr.io/${{github.repository}}-recorder:${{ steps.date.outputs.version }} 81 | ghcr.io/${{github.repository}}-recorder:latest 82 | --------------------------------------------------------------------------------