#!/usr/bin/env bash # 19-rtmp-uat.sh — evidence UAT for the 24x7 desktop RTMP stream (#798) # # Runs ENTIRELY as reachableceo (no root): brings up the loopback mediamtx # receiver, runs the streamer wrapper pinned to cores 6-7 (same CPUs as the # ukrrs-rt.slice), then proves: # 1. RTMP stream metadata (codec/res/fps) via ffprobe # 2. two frames pulled from the stream (content evidence) # 3. encoder CPU cost (% of one core, measured on the 2 pinned cores) # 4. self-heal: kill ffmpeg -> wrapper respawns -> stream returns # # Usage: ./19-rtmp-uat.sh # ./19-rtmp-uat.sh 2>&1 | tee logs/19-rtmp-uat.out set -uo pipefail here=$(cd "$(dirname "$0")" && pwd) LOG=${LOG:-/dev/stdout} say() { echo "[19-uat] $*" | tee -a /dev/null; } pass() { echo "[19-uat] PASS: $*"; } fail() { echo "[19-uat] FAIL: $*"; FAILURES=$((FAILURES+1)); } FAILURES=0 URL=rtmp://127.0.0.1:1935/live/desktop WPID=0 cleanup() { [ "$WPID" -gt 0 ] && kill "$WPID" 2>/dev/null wait 2>/dev/null true } trap cleanup EXIT # 0) receiver up docker compose -f "$here/rtmp/docker-compose.yml" up -d >/dev/null 2>&1 for i in $(seq 1 15); do ss -ltn 2>/dev/null | grep -q '127.0.0.1:1935' && break sleep 1 done ss -ltn | grep -q '127.0.0.1:1935' && pass "receiver listening on 127.0.0.1:1935" \ || { fail "receiver not listening"; exit 1; } # 1) streamer (cores 6-7, mirrors the ukrrs-rt.slice CPU fence) export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/1001} CREDS_ENV="$here/rtmp/uat-creds.env" taskset -c 6,7 \ bash "$here/staged/ukrrs-rtmp-stream.sh" > "$here/logs/19-rtmp-uat.stream.log" 2>&1 & WPID=$! say "wrapper pid=$WPID (pinned cores 6-7)" sleep 12 FFPID=$(pgrep -P "$WPID" -x ffmpeg | head -1) [ -n "$FFPID" ] && pass "ffmpeg child running (pid=$FFPID)" || fail "no ffmpeg child" # 2) stream metadata META=$(timeout 25 ffprobe -v error -show_entries stream=codec_name,width,height,avg_frame_rate,codec_type -of csv=p=0 "$URL" 2>&1 | sort | tr '\n' ' ') echo "[19-uat] stream: $META" case "$META" in *h264*) pass "h264 video present" ;; *) fail "no h264 in stream: $META" ;; esac echo "$META" | grep -q 1920 && pass "output width 1920 (scaled)" || fail "unexpected width: $META" echo "$META" | grep -qE '1000/33|30/1' && pass "~30 fps" || fail "unexpected fps: $META" # 3) frames from the stream (content evidence) timeout 25 ffmpeg -v error -i "$URL" -frames:v 1 -y "$here/logs/19-rtmp-uat-frame1.png" 2>&1 sleep 8 timeout 25 ffmpeg -v error -i "$URL" -frames:v 1 -y "$here/logs/19-rtmp-uat-frame2.png" 2>&1 F1=$(stat -c%s "$here/logs/19-rtmp-uat-frame1.png" 2>/dev/null || echo 0) F2=$(stat -c%s "$here/logs/19-rtmp-uat-frame2.png" 2>/dev/null || echo 0) [ "$F1" -gt 20000 ] && pass "frame 1 captured (${F1} bytes)" || fail "frame 1 too small/missing (${F1} bytes)" [ "$F2" -gt 20000 ] && pass "frame 2 captured (${F2} bytes)" || fail "frame 2 too small/missing (${F2} bytes)" # 4) CPU cost over 10s (ticks are 100/s; 2 cores = max 200%) if [ -n "${FFPID:-}" ] && [ -d /proc/$FFPID ]; then t1=$(awk '{print $14+$15}' /proc/$FFPID/stat) sleep 10 t2=$(awk '{print $14+$15}' /proc/$FFPID/stat) CPU=$(( (t2 - t1) / 10 )) echo "[19-uat] encoder CPU: ${CPU}% of one core (slice ceiling 200%)" [ "$CPU" -lt 195 ] && pass "CPU within 2-core fence (${CPU}%)" || fail "CPU at/over fence (${CPU}%)" else fail "ffmpeg gone before CPU sample" fi # 5) self-heal: kill ffmpeg, wrapper must respawn it kill "$FFPID" 2>/dev/null FFPID2="" for i in $(seq 1 9); do sleep 5 FFPID2=$(pgrep -P "$WPID" -x ffmpeg | head -1) [ -n "$FFPID2" ] && [ "$FFPID2" != "$FFPID" ] && break done [ -n "${FFPID2:-}" ] && [ "$FFPID2" != "$FFPID" ] \ && pass "self-heal: ffmpeg respawned (pid $FFPID -> $FFPID2)" \ || fail "ffmpeg did not respawn" META2="" for i in $(seq 1 6); do META2=$(timeout 25 ffprobe -v error -show_entries stream=codec_name -of csv=p=0 "$URL" 2>/dev/null | head -1) [ "${META2:-}" = "h264" ] && break sleep 3 done [ "${META2:-}" = "h264" ] && pass "stream serves again after kill" || fail "no stream after respawn: '${META2}'" echo echo "[19-uat] wrapper log tail:" tail -6 "$here/logs/19-rtmp-uat.stream.log" | sed 's/^/ /' echo [ "$FAILURES" = 0 ] && echo "[19-uat] OVERALL: PASS" || echo "[19-uat] OVERALL: FAIL ($FAILURES failures)" exit "$FAILURES"