Grundig Raspberry Cameras View - Part 2

Introduction

I shelved this project thinking it was complete. After a few days, I noticed the stream would crash after several hours of playback. The second major issue was that when the TV was disconnected from power and then reconnected (via a programmable timer), it would display a blank screen—requiring manual intervention with either the TV button or remote control.

To make matters worse, the SD card became physically damaged and shorted out. When I powered the Raspberry Pi with the damaged card still in the slot, the card got extremely hot. I had to rebuild the entire project from scratch anyway.

Solution

I modified the project to address these issues as follows:


1. Replace Flash Journal with RAM Journal

Disable Swap Service

sudo dphys-swapfile swapoff
sudo dphys-swapfile uninstall
sudo systemctl disable dphys-swapfile

Download and set the zram for swap compression and save into RAM.
sudo apt install zram-tools
sudo nano /etc/default/zramswap # set ALGO=zstd and PERCENT=50
sudo systemctl restart zramswap
swapon —show

Unblock Wi-Fi Radio

sudo raspi-config nonint do_wifi_country PL 
sudo rfkill unblock wifi

Configure Protocol Explicitly via nmcli

nmcli con add type wifi ifname wlan0 con-name "YourWiFiName" ssid "YourWiFiName"
nmcli con modify "YourWiFiName" wifi-sec.key-mgmt wpa-psk
nmcli con modify "YourWiFiName" wifi-sec.psk "YourPassword"
nmcli con up "YourWiFiName"

Configure Systemd Journal

Open /etc/systemd/journald.conf and set the following under the [Journal] section:

Storage=volatile
RuntimeMaxUse=50M

Mount /var/log to RAM

Open /etc/fstab and add this line to the end:

tmpfs /var/log tmpfs defaults,noatime,nosuid,mode=0755,size=50m 0 0

Enable and Start the Service

sudo systemctl daemon-reload
sudo systemctl enable cctv-stream.service
sudo systemctl start cctv-stream.service
sudo systemctl status cctv-stream.service

1.1 Auto-Login Configuration

This feature is very useful for unattended operation.

  1. Run sudo raspi-config
  2. Navigate to System Options → Boot / Auto Login
  3. Select Console Autologin (automatically logs in as the default user)
  4. Select Finish and reboot

2. Remote Power Button Control

The following approaches were attempted:

  1. CEC via Pin 13 was not supported on this older TV model

  2. Attempted to mount a capacitor to the power button, but it charged too quickly even with a 100µF capacitor

  3. Connected a 2N2222 transistor and added code to the streamer.sh script. This requires a common ground between the TV and the 2N2222 emitter:
    bash~~ ~~python3 -c "import RPi.GPIO as g, time; g.setmode(g.BCM); g.setup(17, g.OUT); g.output(17, 1); time.sleep(0.5); g.output(17, 0); g.cleanup()"~~ ~~
    Initially this worked, but the common ground connection likely introduced electrical noise from the TV, causing Wi-Fi jamming.

  4. Recommended solution: Use the PC817 Optocoupler
    With Optocoupler and short Python script that launches after the cctv-stream.service in separate tv-button.service everything works well

     

import RPi.GPIO as GPIO
import time

try:
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT, initial=GPIO.LOW)

    time.sleep(8)
    GPIO.output(17, GPIO.HIGH)
    time.sleep(0.5)  # Holds line HIGH for 500ms
    GPIO.output(17, GPIO.LOW)

finally:
GPIO.cleanup()

```

tv-button.service

[Unit]
Description=Press TV power button via optocoupler
Requires=cctv-stream.service
Wants=multi-user.target
 
[Service]
Type=oneshot
User=raspberry
WorkingDirectory=/home/raspberry
ExecStart=/usr/bin/python3 /home/raspberry/button.py
 
[Install]
WantedBy=multi-user.target
 

3. Crashing after time

a) Your network timeout is probably not being applied at all. stimeout was deprecated in FFmpeg 5.0 and removed in favour of timeout for the RTSP demuxer. You’re on nano 8.4, which means Debian trixie and FFmpeg 7.x — so --stream-lavf-o=stimeout=5000000 is very likely being silently rejected. That means when the NVR stops sending data without closing the TCP socket, mpv waits forever. Check it:

bash

mpv --stream-lavf-o=stimeout=5000000 --no-config rtsp://invalid 2>&1 | head -20

If you see an option error, that’s your hang. The fix is mpv’s own --network-timeout=10, which is version-stable and maps to the right lavf option automatically.

b) --length=15 is not a wall-clock timer. It measures playback position from stream PTS. If the RTSP stream stalls, playback position stops advancing, and the 15-second limit is never reached — mpv sits there indefinitely. --untimed makes this worse by decoupling output from real time. A wall-clock timeout wrapper is the only reliable bound here.

c) Hard kills leave RTSP sessions open on the NVR. Reolink NVRs cap concurrent RTSP sessions. If mpv is SIGKILLed it never sends TEARDOWN, and the NVR holds that slot until its own timeout. Do that a few times an hour and the NVR eventually refuses new connections — which looks exactly like “it crashes after two hours.” So: SIGTERM first with a real grace period, SIGKILL only as a last resort.

To resolve the issue I made several updates to streamer.sh

 
        set -uo pipefail
 
        # Configuration Variables
        NVR_IP=""  # Ensure this matches your NVR's static IP
        USER=""              # URL-encode special characters if necessary
        PASS=""
 
        # Solar Camera Power Management
        # Set how often to wake the battery camera (in seconds).
        # 1800 seconds = 30 minutes.
        SOLAR_WAKE_INTERVAL=1800
        LAST_WAKE_TIME=0
 
        # Enable the Mesa GPU HUD globally for all mpv instances
        export GALLIUM_HUD=".x950.y40fps,GPU-load"
 
        # Clean array containing your specific GPU setup + the freeze watchdogs
        MPV_FLAGS=(
          --vo=gpu
          --gpu-context=drm
          --hwdec=no
          --video-zoom=-0.07
          --fs
          --ontop
          --no-osc
          --no-osd-bar
          --profile=low-latency
          --cache=no
          --no-audio
          --rtsp-transport=tcp
          --length=15
          --network-timeout=10
          --msg-level=all=warn
        )
 
        #FUNCTION
        run_mpv() {
            local limit="$1"; shift
            timeout --signal=TERM --kill-after=15 "$limit" mpv "${MPV_FLAGS[@]}" "$@"
        }
 
        #Safety kill
        pkill -x mpv 2>/dev/null && sleep 3
 
        # The Master Time Loop
        while true; do
            # 1. Network Pre-Flight (Wait until NVR is reachable)
            while ! ping -c 1 -W 2 "$NVR_IP" &> /dev/null; do
                sleep 2
            done
 
            # 2. Check if the Solar Camera has rested long enough
            CURRENT_TIME=$(date +%s)
            if (( CURRENT_TIME - LAST_WAKE_TIME >= SOLAR_WAKE_INTERVAL )); then
                # Stream the battery camera for a single 15-second block
                run_mpv 45 "rtsp://${USER}:${PASS}@${NVR_IP}:554/h264Preview_05_sub"
                # Log the exact time it went back to sleep
                LAST_WAKE_TIME=$(date +%s)
            fi
 
            # 3. Stream the hardwired cameras (Cams 1-4)
            # --loop-playlist=20 loops the 4 wired cameras 20 times.
            # 4 cameras * 15 seconds = 1 minute per loop. 20 loops = ~20 minutes of continuous rotation.
            START=$(date +%s)
            run_mpv 1800 --loop-playlist=20 \
              "rtsp://${USER}:${PASS}@${NVR_IP}:554/h264Preview_01_sub" \
              "rtsp://${USER}:${PASS}@${NVR_IP}:554/h264Preview_02_sub" \
              "rtsp://${USER}:${PASS}@${NVR_IP}:554/h264Preview_03_sub" \
              "rtsp://${USER}:${PASS}@${NVR_IP}:554/h264Preview_04_sub"
            rc=$?
            ELAPSED=$(( $(date +%s) - START ))
 
            # Give the NVR a moment to release the RTSP session before reconnecting
            if (( rc != 0 )); then
                pkill -x mpv 2>/dev/null
                # Failed fast means the NVR is refusing connections, not a
                # mid-stream stall. Back off hard so its session table can drain.
                if (( ELAPSED < 60 )); then
                    sleep 60
                else
                    sleep 5
                fi
            fi
        done
 
 

It didn’t helped. I had to add the crash exception in case

Outcome