commit 6b3ef62c4b438b7e867c357afae3ab7de0907c44 Author: Herwin Date: Mon May 15 17:07:19 2023 +0200 Initial Commit Update Dockerfile_cleaner, Dockerfile_recorder, and 12 more files... diff --git a/Dockerfile_cleaner b/Dockerfile_cleaner new file mode 100644 index 0000000..8b2027f --- /dev/null +++ b/Dockerfile_cleaner @@ -0,0 +1,7 @@ +FROM python:3-alpine + +RUN apk update && apk upgrade + +WORKDIR /app + +CMD ["python", "-u", "/app/app.py"] diff --git a/Dockerfile_recorder b/Dockerfile_recorder new file mode 100644 index 0000000..e25314e --- /dev/null +++ b/Dockerfile_recorder @@ -0,0 +1,11 @@ +FROM alpine:latest + +RUN apk update && apk upgrade && apk add --no-cache ffmpeg + +WORKDIR /data + +# For .mkv files +CMD ffmpeg -hide_banner -loglevel warning -threads 6 -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1 -i "$NP_CCTV_URL" -f segment -segment_time 10 -reset_timestamps 1 -segment_format matroska -strftime 1 -c:v copy -tag:v hvcl -bsf:v hevc_mp4toannexb -c:a aac ./%Y-%m-%d_%H-%M-%S.mkv + +# For .mp4 files (Unused) +#CMD ffmpeg -hide_banner -loglevel warning -threads 6 -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1 -i "$NP_CCTV_URL" -f segment -segment_time 10 -reset_timestamps 1 -segment_format mp4 -strftime 1 -c:v copy -tag:v hvcl -bsf:v hevc_mp4toannexb -c:a aac ./%Y-%m-%d_%H-%M-%S.mp4 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/apache2.conf b/apache2.conf new file mode 100644 index 0000000..b373b4b --- /dev/null +++ b/apache2.conf @@ -0,0 +1,152 @@ +# +# The directory where shm and other runtime files will be stored. +# + +DefaultRuntimeDir ${APACHE_RUN_DIR} + +# +# PidFile: The file in which the server should record its process +# identification number when it starts. +# This needs to be set in /etc/apache2/envvars +# +PidFile ${APACHE_PID_FILE} + +# +# Timeout: The number of seconds before receives and sends time out. +# +Timeout 300 + +# +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +# +KeepAlive On + +# +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +# +MaxKeepAliveRequests 100 + +# +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +# +KeepAliveTimeout 5 + + +# These need to be set in /etc/apache2/envvars +User ${APACHE_RUN_USER} +Group ${APACHE_RUN_GROUP} + +# +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +# +HostnameLookups Off + +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog ${APACHE_LOG_DIR}/error.log + +# +# LogLevel: Control the severity of messages logged to the error_log. +# Available values: trace8, ..., trace1, debug, info, notice, warn, +# error, crit, alert, emerg. +# It is also possible to configure the log level for particular modules, e.g. +# "LogLevel info ssl:warn" +# +LogLevel warn + +# Include module configuration: +IncludeOptional mods-enabled/*.load +IncludeOptional mods-enabled/*.conf + +# Include list of ports to listen on +Include ports.conf + + +# Sets the default security model of the Apache2 HTTPD server. It does +# not allow access to the root filesystem outside of /usr/share and /var/www. +# The former is used by web applications packaged in Debian, +# the latter may be used for local directories served by the web server. If +# your system is serving content from a sub-directory in /srv you must allow +# access here, or in any related virtual host. + + Options FollowSymLinks + AllowOverride None + Require all denied + + + + AllowOverride None + Require all granted + + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + +# +# Options Indexes FollowSymLinks +# AllowOverride None +# Require all granted +# + + + + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +# +AccessFileName .htaccess + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + + +# +# The following directives define some format nicknames for use with +# a CustomLog directive. +# +# These deviate from the Common Log Format definitions in that they use %O +# (the actual bytes sent including headers) instead of %b (the size of the +# requested file), because the latter makes it impossible to detect partial +# requests. +# +# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended. +# Use mod_remoteip instead. +# +LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined +LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %O" common +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +# Include of directories ignores editors' and dpkg's backup files, +# see README.Debian for details. + +# Include generic snippets of statements +IncludeOptional conf-enabled/*.conf + +# Include the virtual host configurations: +IncludeOptional sites-enabled/*.conf + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/cleaner.py b/cleaner.py new file mode 100644 index 0000000..b8379c2 --- /dev/null +++ b/cleaner.py @@ -0,0 +1,52 @@ +import os +import signal +import sys +import time + +# Keep files for 73 hours (3 days + 1 hours) +MAX_FILE_AGE_SECONDS = (72 + 1) * 60 * 60 + +# Once done cleaning, sleep for 5 minutes +SLEEP_TIME_SECONDS = 5 * 60 + +# Handling shutdown signals +def signal_handler(sig, frame): + sys.exit(0) + +signal.signal(signal.SIGTERM, signal_handler) + +def delete_old_files(directory, current_time): + deletion_count = 0 + + for filename in os.listdir(directory): + filepath = os.path.join(directory, filename) + + if os.path.isfile(filepath): + file_creation_time = os.path.getctime(filepath) + + if (current_time - file_creation_time) > (MAX_FILE_AGE_SECONDS): + os.remove(filepath) + deletion_count = deletion_count + 1 + + return deletion_count + +print("Deleting old files...") + +start_time = time.time() + +file_deleted_count = 0 + +for item in os.listdir("/data/"): + item_path = os.path.join(folder_path, item) + + if os.path.isdir(item_path): + file_deleted_count = file_deleted_count + delete_old_files(item_path, start_time) + else: + # Ignoring files + continue + +end_time = time.time() + +print("Took {} second(s) to delete {} file(s)".format(round(end_time - start_time, 2), file_deleted_count)) + +time.sleep(SLEEP_TIME_SECONDS) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e285bb8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: "3" + +services: + cctv_recorder_cam1: + container_name: cctv-recorder-cam1 + build: + context: . + dockerfile: Dockerfile_recorder + environment: + - TZ=Europe/Brussels + - "NP_CCTV_URL=rtsp://user:password@address:554/sub-path" + volumes: + - ./recordings/cam1:/data + restart: unless-stopped + + cctv_recorder_cam2: + container_name: cctv-recorder-cam2 + build: + context: . + dockerfile: Dockerfile_recorder + environment: + - TZ=Europe/Brussels + - "NP_CCTV_URL=rtsp://user:password@address:554" + volumes: + - ./recordings/cam2:/data + restart: unless-stopped + + cctv_cleaner: + container_name: cctv-cleaner + build: + context: . + dockerfile: Dockerfile_cleaner + environment: + - TZ=Europe/Brussels + volumes: + - ./recordings:/data + - ./cleaner.py:/app/app.py:ro + restart: unless-stopped + + cctv_web: + container_name: cctv-web + image: php:apache + ports: + - 26880:80 + environment: + - TZ=Europe/Brussels + volumes: + - ./htdocs:/var/www/html # Cannot be "ro" since the recordings are mounted into it. + - ./apache2.conf:/etc/apache2/apache2.conf:ro + - ./recordings:/var/www/html/data:ro + restart: unless-stopped diff --git a/htdocs/.htaccess b/htdocs/.htaccess new file mode 100644 index 0000000..95aa776 --- /dev/null +++ b/htdocs/.htaccess @@ -0,0 +1,11 @@ +# Prevent access to .htaccess + + Require all denied + + +# Fixing Apache's autistic fit with .mkv files +AddType video/x-matroska mkv + +# Allowing indexes +Options +Indexes -FollowSymlinks -ExecCGI +ServerSignature Off diff --git a/htdocs/css/simplette.all.min.css b/htdocs/css/simplette.all.min.css new file mode 100644 index 0000000..5394ebe --- /dev/null +++ b/htdocs/css/simplette.all.min.css @@ -0,0 +1 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after{content:'';content:none}q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}html{min-width:calc(100vw - (100vw - 100%));min-height:100vh}body{background-color:#566f7c}hr{border-color:#8eacbb}hr.thin{margin:0}header,main,footer,.margin-container{margin:15px}.indent-container{margin-left:15px}.spacing-container{margin-top:15px;margin-bottom:15px}.subtle{opacity:0.5}main ul>li,main li{margin-bottom:10px}.space-vert-small{margin-bottom:0.5em}.space-vert-normal{margin-bottom:1em}@media (orientation: portrait){.indent-container.no-mobile-indent,.margin-container.no-mobile-indent{margin-left:0;margin-right:0}}html *{color:#eee;text-shadow:1px 1px 1px #1c313a;cursor:default}header,footer{text-align:center}h1,.h1{font-size:2em}h2,.h2{font-size:1.8em}h3,.h3{font-size:1.6em}h4,.h4{font-size:1.4em}h5,.h5{font-size:1.2em}h6,.h6{font-size:1em}b,.bold{font-weight:bold}i,.italic{font-style:italic}.underline{text-decoration:underline}.striketrough{text-decoration:line-through}.overline{text-decoration:overline}.oblique{font-style:oblique}.justify{text-align:justify}*>.auto-paragraph-margin p:not(:last-child){margin-bottom:1em}a{color:#bbdefb;cursor:pointer}ul.link-list,ol.link-list{font-size:0}ul.link-list li,ol.link-list li{margin:0;display:inline-block;font-size:initial}ul.link-list li:first-of-type:before,ol.link-list li:first-of-type:before{content:"[";margin-right:5px}ul.link-list li:not(:last-of-type):after,ol.link-list li:not(:last-of-type):after{content:"|"}ul.link-list li:last-of-type:after,ol.link-list li:last-of-type:after{content:"]";margin-left:5px}sup{vertical-align:top;font-size:smaller}sub{vertical-align:bottom;font-size:smaller}summary::-webkit-details-marker,summary::marker{display:none}details>summary{list-style:none}summary:after{background-color:#33424a;width:1em;float:left;padding:0;margin:0;margin-right:10px;text-align:center;color:#eee;font-weight:bold;content:"+"}details[open] summary:after{content:"-"}button{padding:0;margin:0;border:none;background:none;height:1em;cursor:pointer;font-weight:bold;user-select:none}button:before{content:"❰";margin-right:5px}button:after{content:"❱";margin-left:5px}button:disabled{text-decoration:line-through;cursor:default}label:before{content:"$";margin-right:5px}input{background-color:#445863;border:none;-webkit-box-shadow:inset 0px 0px 0px 1px #33424a;-moz-box-shadow:inset 0px 0px 0px 1px #33424a;box-shadow:inset 0px 0px 0px 1px #33424a}input[type="text"],input[type="password"],input[type="number"],input[type="file"]{height:1em;padding:1.5px 2px}.full-width{width:100%}.float-right,.fr{float:right}.float-left,.fl{float:left}.text-right,.tr{text-align:right}.text-left,.tl{text-align:left}.grid{display:grid}.grid:not(.spaced){grid-gap:0}.grid:not(.spaced).col-4{grid-template-columns:repeat(4, 25%)}.grid:not(.spaced).col-8{grid-template-columns:repeat(8, 12.5%)}.grid:not(.spaced).col-12{grid-template-columns:repeat(12, calc(100% / 12))}.grid.spaced{grid-gap:1em}.grid.spaced.col-4{grid-template-columns:repeat(4, calc(25% - (3em / 4)))}.grid.spaced.col-8{grid-template-columns:repeat(8, calc(12.5% - (7em / 8)))}.grid.spaced.col-12{grid-template-columns:repeat(12, calc((100% / 12) - (11em / 8)))}.grid>*.from-1{grid-column-start:1}.grid>*.to-1{grid-column-end:2}.grid>*.row-1{grid-row:1}.grid>*.from-2{grid-column-start:2}.grid>*.to-2{grid-column-end:3}.grid>*.row-2{grid-row:2}.grid>*.from-3{grid-column-start:3}.grid>*.to-3{grid-column-end:4}.grid>*.row-3{grid-row:3}.grid>*.from-4{grid-column-start:4}.grid>*.to-4{grid-column-end:5}.grid>*.row-4{grid-row:4}.grid>*.from-5{grid-column-start:5}.grid>*.to-5{grid-column-end:6}.grid>*.row-5{grid-row:5}.grid>*.from-6{grid-column-start:6}.grid>*.to-6{grid-column-end:7}.grid>*.row-6{grid-row:6}.grid>*.from-7{grid-column-start:7}.grid>*.to-7{grid-column-end:8}.grid>*.row-7{grid-row:7}.grid>*.from-8{grid-column-start:8}.grid>*.to-8{grid-column-end:9}.grid>*.row-8{grid-row:8}.grid>*.from-9{grid-column-start:9}.grid>*.to-9{grid-column-end:10}.grid>*.row-9{grid-row:9}.grid>*.from-10{grid-column-start:10}.grid>*.to-10{grid-column-end:11}.grid>*.row-10{grid-row:10}.grid>*.from-11{grid-column-start:11}.grid>*.to-11{grid-column-end:12}.grid>*.row-11{grid-row:11}.gradient-background{background:linear-gradient(90deg, #4458637F 0%, #778b967F 100%)}.gradient-background.flipped{background:linear-gradient(90deg, #778b967F 0%, #4458637F 100%)}.nice-border{border:2px solid transparent;border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGBAMAAAAS4vJ7AAAAD1BMVEUzQkqZqLBEWGN3i5ZWb3yl0XayAAAAGklEQVQI12MQFBRgEDZWYBB2gWAhJQUGIAAAGHwB28+3mzIAAAAASUVORK5CYII=) 2 stretch;image-rendering:smooth}.nice-border.inverted{border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGBAMAAAAS4vJ7AAAAD1BMVEUzQkqZqLBEWGN3i5ZWb3yl0XayAAAAGUlEQVQI12MAASYlRQYmF0MINjZkYBQUBAASCAG9ILHkHAAAAABJRU5ErkJggg==) 2 stretch}.nice-border.thick{border-width:4px}.nice-border.thick:not(.low-res){border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAG1BMVEUzQkqZqLA8TVaImqNWb3xEWGN3i5ZNY29mfYk9f23rAAAAM0lEQVQI12MQBAEBBmFjIFBgEE5LS0sFUhkdHaUgysUFJ5VeXg6iQoFAgUFICQgUGCAAALvsD+2aGpV6AAAAAElFTkSuQmCC) 4 stretch}.nice-border.thick:not(.low-res).inverted{border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAG1BMVEUzQkqZqLA8TVaImqNWb3xEWGN3i5ZNY29mfYk9f23rAAAAMklEQVQI12OAAiYlIFBkYAoFAkMGpvDy8jIQ5eLShpPq6ABRYWlpaUDKGAgMGRgFQQAAbWoPsYoOn3UAAAAASUVORK5CYII=) 4 stretch}.frame{position:relative;border:2px solid transparent;border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGAgMAAACdogfbAAAACVBMVEWZqLB3i5ZWb3wnVbTDAAAAA3RSTlN/f3/NxuInAAAAFUlEQVQI12NgEGAQFWCQAiEQOzQAAAdAAT8Ytr6/AAAAAElFTkSuQmCC) 2 stretch}.frame.spaced{margin-top:0.5em}.frame.padded{padding:0.5em}.frame.dark{border-image:url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGAgMAAACdogfbAAAACVBMVEVEWGMzQkpWb3y7E9vOAAAAA3RSTlN/f3/NxuInAAAAFUlEQVQI12NgEGAQFWCQAiEQOzQAAAdAAT8Ytr6/AAAAAElFTkSuQmCC) 2 stretch}.frame>legend,.frame .title{background-color:#566f7c;display:inline-block;position:absolute;top:-0.5em;left:0.75em;padding:0 0.125em 0 0.125em;z-index:1}.frame.fun>legend ::before,.frame.fun .title ::before{content:"> "} diff --git a/htdocs/favicon.ico b/htdocs/favicon.ico new file mode 100644 index 0000000..3c4f0ee Binary files /dev/null and b/htdocs/favicon.ico differ diff --git a/htdocs/favicon.svg b/htdocs/favicon.svg new file mode 100644 index 0000000..acc8f6a --- /dev/null +++ b/htdocs/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/htdocs/index.php b/htdocs/index.php new file mode 100644 index 0000000..f09b4fc --- /dev/null +++ b/htdocs/index.php @@ -0,0 +1,365 @@ +$filename){ + if($filename!=".." && $filename!="."){ + if(is_dir($dir."/".$filename)){ + $new_foldersize = foldersize($dir."/".$filename); + $count_size = $count_size+ $new_foldersize; + } else if(is_file($dir."/".$filename)) { + $count_size = $count_size + filesize($dir."/".$filename); + $count++; + } + } + } + return $count_size; +} + +// Function used to format the disk space taken by recordings later on. +function sizeFormat($bytes) { + $kb = 1024; + $mb = $kb * 1024; + $gb = $mb * 1024; + $tb = $gb * 1024; + if (($bytes >= 0) && ($bytes < $kb)) { + return $bytes . ' B'; + } elseif (($bytes >= $kb) && ($bytes < $mb)) { + return ceil($bytes / $kb) . ' KiB'; + } elseif (($bytes >= $mb) && ($bytes < $gb)) { + return ceil($bytes / $mb) . ' MiB'; + } elseif (($bytes >= $gb) && ($bytes < $tb)) { + return ceil($bytes / $gb) . ' GiB'; + } elseif ($bytes >= $tb) { + return ceil($bytes / $tb) . ' TiB'; + } else { + return $bytes . ' B'; + } +} + +?> + + + + + + + NibblePoker's Mini CCTV NVR + + + + + +

NibblePoker's Mini CCTV NVR

+

+
+ + + + + +
+

Caméra:

+
+ +
+
+
+
+ Select one camera:

"); + echo(""); + } else { + // We have selected one, we add the video, slider, jumpers and filename placeholder. + echo("
"); + echo(""); + echo("
"); + echo(""); + echo("
"); + echo("

"); + echo("<<<< 25"); + echo("<<< 10"); + echo("<< 5"); + echo("< 1"); + echo("1 >"); + echo("5 >>"); + echo("10 >>>"); + echo("25 >>>>"); + echo("

"); + echo("
"); + echo("

File: Non définis (0/0)

"); + echo("
"); + echo("
"); + } + ?> +
+

+ + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0ed463f --- /dev/null +++ b/readme.md @@ -0,0 +1,77 @@ +# Mini CCTV NRV +A mini docker stack that allows you to easily record, clean and serve CCTV recordings made over RSTP while using a +minimal amount of system resources. + +## Preamble +This stack records the camera's streams as-is and doens't re-encode or compress it which can cause it to use more disk space.
+The highest I've got on my side is around 70 GiB per day for a 4K-ish cam with AAC audio. + +It is highly recommended to put the web page behind a secure reverse-proxy that requires authentication for aditional security. + +## Setup +Since the stack is a bit rough around the edges for simplicity''s sake you **will** need to setup a couple of things beforehand. + +It should only take 2-3 minutes if you already have the RTSP URL on hand however. + +### Cameras +Each recording container needs to be given a RSTP stream URL and a unique folder into which the recordings will go. + +The URL must be given via the `NP_CCTV_URL` environment variable, and the output folder via a mounted volume that is +mounted as `/data` in the container. + +Here is a simple example: +```yaml + cctv_recorder_cam1: + container_name: cctv-recorder-cam1 + build: + context: . + dockerfile: Dockerfile_recorder + environment: + - TZ=Europe/Brussels + - "NP_CCTV_URL=rtsp://user:password@address:554/sub-path" + volumes: + - ./recordings/cam1:/data + restart: unless-stopped +``` + +This example will use the `rtsp://user:password@address:554/sub-path` URL and will put its recordings in `./recordings/cam1`. + +### Cleaner +The cleaner script named [cleaner.py](cleaner.py) only requires you to change 1 variable located near the top of the file. + +The variable named `MAX_FILE_AGE_SECONDS` is used to indicate how long recordings should be kept for and is set to +73 hours by default. + +It can also be changed as you wish without recreating the stack since the script is "reloaded" on each run. + +### Web interface +The web interface only requires you to give it the list of all cams' IDs and a friendly name in the `$camsInfo` variable. + +This variable is located at the top of the [htdocs/index.php](htdocs/index.php) file and should look like this: +```php +$camsInfo = [ + # Format: [camId, camName] + ["cam1", "Cam #1"], + ["cam2", "Cam #2"] +]; +``` + +The cam's ID refers to a subfolder of `./recordings` into which this cam's recordings are found as `.mp4` or `.mkv` files. + +## Startup +Once you have finished setting up the stack, you can simply run the following command: +```bash +docker-compose up --build -d +``` + +## Screenshots +### Home page +![alt text](screenshots/home.png) + +### Camera's page with blurred preview +![alt text](screenshots/cam.png) + + +## License +This software, as well as the [Simplette CSS Stylesheet](https://github.com/aziascreations/Simplette-CSS-Stylesheet) +used for the web interface are both licensed under [Unlicense](LICENSE). diff --git a/screenshots/cam.png b/screenshots/cam.png new file mode 100644 index 0000000..cf5bb0a Binary files /dev/null and b/screenshots/cam.png differ diff --git a/screenshots/home.png b/screenshots/home.png new file mode 100644 index 0000000..c78e369 Binary files /dev/null and b/screenshots/home.png differ