Files
ultix/19-rtmp-uat.sh
mrcharles 777767db54 24x7 RTMP desktop stream, client side + UAT PASS (#798)
Whole-screen KDE capture -> x264 -> RTMP, fenced to the rt slice.
Loopback mediamtx receiver; Cloudron cutover = one creds edit.
Detail: https://projects.knownelement.com/issues/798#note-4519

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-09-05 05:01:23 -05:00

108 lines
4.2 KiB
Bash
Executable File

#!/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"