diff options
-rw-r--r-- | doc/ipcam-streaming.md | 6 | ||||
-rw-r--r-- | localwebsite/config.php | 7 | ||||
-rw-r--r-- | localwebsite/engine/tpl.php | 2 | ||||
-rw-r--r-- | localwebsite/handlers/MiscHandler.php | 13 | ||||
-rw-r--r-- | localwebsite/htdocs/assets/app.css | 15 | ||||
-rw-r--r-- | localwebsite/htdocs/index.php | 1 | ||||
-rw-r--r-- | localwebsite/templates-web/cams.twig | 64 | ||||
-rw-r--r-- | localwebsite/templates-web/index.twig | 1 | ||||
-rw-r--r-- | systemd/ipcam-rtsp2hls@.service | 13 | ||||
-rwxr-xr-x | tools/ipcam-rtsp2hls.sh | 95 |
10 files changed, 215 insertions, 2 deletions
diff --git a/doc/ipcam-streaming.md b/doc/ipcam-streaming.md new file mode 100644 index 0000000..524e0e2 --- /dev/null +++ b/doc/ipcam-streaming.md @@ -0,0 +1,6 @@ +Let's assume IP cameras stream h264 via rtsp. + +To `/etc/fstab`: +``` +tmpfs /var/ipcamfs tmpfs mode=1755,uid=1000,gid=1000 0 0 +```
\ No newline at end of file diff --git a/localwebsite/config.php b/localwebsite/config.php index 47be2f7..262aeae 100644 --- a/localwebsite/config.php +++ b/localwebsite/config.php @@ -49,10 +49,15 @@ return [ ], 'static' => [ - 'app.css' => 6, + 'app.css' => 8, 'app.js' => 1, 'polyfills.js' => 1, 'modem.js' => 1, 'inverter.js' => 2, + ], + + 'cam_hls_host' => '192.168.1.1', + 'cam_list' => [ + // fill me with names ] ]; diff --git a/localwebsite/engine/tpl.php b/localwebsite/engine/tpl.php index a12807d..3d18c9a 100644 --- a/localwebsite/engine/tpl.php +++ b/localwebsite/engine/tpl.php @@ -20,7 +20,7 @@ abstract class base_tpl { public function __construct($templates_dir, $cache_dir) { global $config; - $cl = get_called_class(); + // $cl = get_called_class(); $this->twig = self::twig_instance($templates_dir, $cache_dir, $config['is_dev']); $this->static_time = time(); diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php index 4f35981..314920a 100644 --- a/localwebsite/handlers/MiscHandler.php +++ b/localwebsite/handlers/MiscHandler.php @@ -49,4 +49,17 @@ class MiscHandler extends RequestHandler $this->tpl->render_page('pump.twig'); } + public function GET_cams() { + global $config; + + $this->tpl->add_external_static('js', 'https://cdn.jsdelivr.net/npm/hls.js@latest'); + + $this->tpl->set([ + 'hls_host' => $config['cam_hls_host'], + 'cams' => $config['cam_list'] + ]); + $this->tpl->set_title('Камеры'); + $this->tpl->render_page('cams.twig'); + } + }
\ No newline at end of file diff --git a/localwebsite/htdocs/assets/app.css b/localwebsite/htdocs/assets/app.css index cab674e..73e0667 100644 --- a/localwebsite/htdocs/assets/app.css +++ b/localwebsite/htdocs/assets/app.css @@ -150,4 +150,19 @@ @keyframes sk-circleFadeDelay { 0%, 39%, 100% { opacity: 0; } 40% { opacity: 1; } +} + +/* cams page */ +.camfeeds { + display: flex; + flex-wrap: wrap; + flex-direction: row; +} +.camfeeds > video { + display: flex; + flex-basis: calc(50% - 20px); + justify-content: center; + flex-direction: column; + width: calc(50% - 10px); + margin: 5px; }
\ No newline at end of file diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php index 3961c2c..65afc72 100644 --- a/localwebsite/htdocs/index.php +++ b/localwebsite/htdocs/index.php @@ -25,6 +25,7 @@ $router->add('/', 'Misc main'); $router->add('sensors/', 'Misc sensors_page'); $router->add('pump/', 'Misc pump_page'); $router->add('phpinfo/', 'Misc phpinfo'); +$router->add('cams/', 'Misc cams'); $route = routerFind($router); diff --git a/localwebsite/templates-web/cams.twig b/localwebsite/templates-web/cams.twig new file mode 100644 index 0000000..2963fdb --- /dev/null +++ b/localwebsite/templates-web/cams.twig @@ -0,0 +1,64 @@ +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="/">Главная</a></li> + <li class="breadcrumb-item active" aria-current="page">Камеры</li> + </ol> +</nav> + +<div id="videos" class="camfeeds"></div> +<video height="300" id="video"></video> + +<script> +function hasFallbackSupport() { + var video = document.createElement('video'); + return video.canPlayType('application/vnd.apple.mpegurl'); +} + +function setupHls(video, name, useHls) { + var src = 'http://{{ hls_host }}/ipcam/'+name+'/live.m3u8'; + + // hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled. + + // When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element through the `src` property. + // This is using the built-in support of the plain video element, without using hls.js. + + if (useHls) { + var hls = new Hls({ + // debug: true, + startPosition: -1, + }); + hls.loadSource(src); + hls.attachMedia(video); + hls.on(Hls.Events.MEDIA_ATTACHED, function () { + video.muted = true; + video.play(); + }); + } else { + video.src = src; + video.addEventListener('canplay', function () { + video.play(); + }); + } +} + +function init() { + let useHls = Hls.isSupported(); + if (!useHls && !hasFallbackSupport()) { + alert('Neither HLS nor vnd.apple.mpegurl is not supported by your browser.'); + return; + } + + var cams = {{ cams|json_encode|raw }}; + for (var i = 0; i < cams.length; i++) { + var name = cams[i]; + var video = document.createElement('video'); + // video.setAttribute('height', '400'); + video.setAttribute('id', 'video-'+name); + document.getElementById('videos').appendChild(video); + + setupHls(video, name, useHls); + } +} + +init(); +</script>
\ No newline at end of file diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig index d293cfd..1635459 100644 --- a/localwebsite/templates-web/index.twig +++ b/localwebsite/templates-web/index.twig @@ -17,5 +17,6 @@ <li class="list-group-item"><a href="/inverter/">Инвертор</a></li> <li class="list-group-item"><a href="/pump/">Насос</a></li> <li class="list-group-item"><a href="/sensors/">Датчики</a></li> + <li class="list-group-item"><a href="/cams/">Камеры</a></li> </ul> </div>
\ No newline at end of file diff --git a/systemd/ipcam-rtsp2hls@.service b/systemd/ipcam-rtsp2hls@.service new file mode 100644 index 0000000..5ab205b --- /dev/null +++ b/systemd/ipcam-rtsp2hls@.service @@ -0,0 +1,13 @@ +[Unit] +Description=convert rtsp to hls for viewing live camera feeds in browser +After=network-online.target + +[Service] +Restart=on-failure +User=user +Group=user +EnvironmentFile=/etc/ipcam-rtsp2hls.conf.d/%i.conf +ExecStart=/home/user/homekit/tools/ipcam-rtsp2hls.sh --name %i --user $USER --password $PASSWORD --ip $IP --port $PORT $ARGS + +[Install] +WantedBy=multi-user.target diff --git a/tools/ipcam-rtsp2hls.sh b/tools/ipcam-rtsp2hls.sh new file mode 100755 index 0000000..d52fb8a --- /dev/null +++ b/tools/ipcam-rtsp2hls.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +PROGNAME="$0" +OUTDIR=/var/ipcamfs # should be tmpfs +PORT=554 +NAME= +IP= +USER= +PASSWORD= +DEBUG=0 +CHANNEL=1 + +die() { + echo >&2 "error: $@" + exit 1 +} + +usage() { + cat <<EOF +usage: $PROGNAME [OPTIONS] COMMAND + +Options: + --ip camera IP + --port RTSP port (default: 554) + --name camera name (chunks will be stored under $OUTDIR/{name}/) + --user + --password + --debug + --channel 1|2 + +EOF + exit +} + +validate_channel() { + local c="$1" + case "$c" in + 1|2) + : + ;; + *) + die "Invalid channel" + ;; + esac +} + +[ -z "$1" ] && usage + +while [[ $# -gt 0 ]]; do + case "$1" in + --ip|--port|--name|--user|--password) + _var=${1:2} + _var=${_var^^} + printf -v "$_var" '%s' "$2" + shift + ;; + + --debug) + DEBUG=1 + ;; + + --channel) + CHANNEL="$2" + shift + ;; + + *) + die "Unrecognized argument: $1" + ;; + esac + shift +done + +[ -z "$IP" ] && die "You must specify camera IP address (--ip)." +[ -z "$PORT" ] && die "Port can't be empty." +[ -z "$NAME" ] && die "You must specify camera name (--name)." +[ -z "$USER" ] && die "You must specify username (--user)." +[ -z "$PASSWORD" ] && die "You must specify username (--password)." +validate_channel "$CHANNEL" + +if [ ! -d "${OUTDIR}/${NAME}" ]; then + mkdir "${OUTDIR}/${NAME}" || die "Failed to create ${OUTDIR}/${NAME}!" +fi + +if [ "$DEBUG" = "1" ]; then + ffmpeg_args="-v info" +else + ffmpeg_args="-nostats -loglevel error" +fi + +ffmpeg $ffmpeg_args -i rtsp://${USER}:${PASSWORD}@${IP}:${PORT}/Streaming/Channels/${CHANNEL} \ + -c:v copy -c:a copy -bufsize 1835k \ + -pix_fmt yuv420p \ + -flags -global_header -hls_time 5 -hls_list_size 6 -hls_wrap 5 \ + ${OUTDIR}/${NAME}/live.m3u8 |