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
108 lines
4.2 KiB
Bash
Executable File
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"
|