From 284730a2d116a2d7cb76694c9efafbab62603353 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 1 Sep 2026 20:41:24 -0400 Subject: [PATCH] sync-probe: survive signals, document the edge-stall flaw + baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untrapped TERM skipped the EXIT trap and left the channel dark once; now signals exit through the trap. The YouTube-side capture wedges on a stale HLS manifest after the swap's ingest gap — the rig-side measurement (same inputs, same encoder, no YouTube) is the reliable A/V number: 2026-09-01 baseline −65ms audio-early, corrected with AV_OFFSET=0.065 in both station envs. Claude-Session: https://claude.ai/code/session_01EaVptJ3ZhyeQ9ZZZNcp55T --- toolchain/tv/sync-probe.sh | 90 ++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 42 deletions(-) mode change 100644 => 100755 toolchain/tv/sync-probe.sh diff --git a/toolchain/tv/sync-probe.sh b/toolchain/tv/sync-probe.sh old mode 100644 new mode 100755 index fe35e2a2ff..69da1c4f93 --- a/toolchain/tv/sync-probe.sh +++ b/toolchain/tv/sync-probe.sh @@ -1,55 +1,64 @@ -#!/bin/sh +#!/bin/bash # sync-probe.sh — end-to-end AV-sync + latency measurement for a TV station. +# Runs on a residential machine (YouTube bot-checks datacenter IPs out of +# HLS access, so jasellite can't pull its own stream); the rig host airs +# the card, this side records the real viewer feed and reads the numbers. # -# Airs the sync test card (sync-test.html: 1kHz beep + white flash on the -# even-UTC-second grid, double-flash on :00/:30) through the REAL pipeline -# (same rig, same encoder, same stream key), records the actual YouTube -# HLS a viewer would receive, and reads the numbers back out: +# toolchain/tv/sync-probe.sh [rig-host] [station] [watch-url] +# defaults: jasellite wgtv the Whistlegraph TV broadcast # -# AV offset — median(beep onset − flash onset) via silencedetect and -# blackdetect. Positive = audio LATE (behind picture). -# glass delay — wallclock between a double-flash airing and it arriving -# at the HLS live edge (unambiguous modulo 30s). -# -# sync-probe.sh # e.g. ~/wgtv https://www.youtube.com/watch?v=… -# -# The station unit + its watchdog timer are stopped for the ~90s test and -# restarted after, so the broadcast shows a brief test card — honest TV. +# What happens: the station unit + watchdog timer pause, the same rig +# pipeline airs sync-test.html (1kHz beep + white flash on the even-UTC- +# second grid, double flash on :00/:30), yt-dlp records ~45s of the live +# HLS here, the station is restored (EXIT trap — even on failure), and +# blackdetect/silencedetect yield: +# AV offset — median(beep − flash); positive = audio LATE. +# glass delay — double-flash wallclock vs arrival, unambiguous mod 30s. set -eu -STATION_DIR="${1:?usage: sync-probe.sh }" -WATCH_URL="${2:?usage: sync-probe.sh }" -UNIT=$(basename "$STATION_DIR") -CAP="$STATION_DIR/sync-capture.mp4" +HOST="${1:-jasellite}" +UNIT="${2:-wgtv}" +WATCH="${3:-https://www.youtube.com/watch?v=PhzZS0OEW64}" DUR=45 +CAP=$(mktemp -t sync-cap).mp4 -echo "▸ probing $UNIT → $WATCH_URL" -systemctl --user stop "$UNIT-watchdog.timer" "$UNIT" 2>/dev/null || true -sleep 2 +restore() { + echo "▸ restoring $UNIT on $HOST" + # Bracketed patterns so pkill can't match this ssh's own command line. + ssh "$HOST" "sh -c 'pkill -f \"user-data-dir=/home/jas/.tv-rig-chrom[e] \" 2>/dev/null; pkill -f \"Xvfb :9[9]\" 2>/dev/null; pkill -f -- \"-i :9[9] \" 2>/dev/null; sleep 1; systemctl --user start $UNIT $UNIT-watchdog.timer'" || true +} +# EXIT alone doesn't fire on an untrapped signal — a killed probe once left +# the channel dark. Trap the signals and exit through them. +trap restore EXIT +trap 'exit 129' INT TERM -# Test-card env: the station's own settings, page swapped for the card. -ENVT=$(mktemp) -grep -v "^URL=" "$STATION_DIR/$UNIT.env" > "$ENVT" -echo "URL=file://$HOME/wgtv/sync-test.html" >> "$ENVT" -echo "UNMUTE_CLICK=1" >> "$ENVT" +# ⚠️ Known flaw in the YouTube-side capture: the ingest gap during the +# station swap stalls the public HLS edge for several minutes, so the +# recording can wedge at a stale manifest. The rig-side measurement below +# is the reliable A/V number (same inputs, same encoder, no YouTube): +# +# ssh 'ffmpeg -thread_queue_size 1024 -use_wallclock_as_timestamps 1 \ +# -f x11grab -framerate 30 -video_size -i :99 \ +# -thread_queue_size 1024 -use_wallclock_as_timestamps 1 \ +# -f pulse -fragment_size 1024 -i tvsink.monitor \ +# -af aresample=async=1:first_pts=0 -fps_mode cfr -t 40 \ +# -c:v libx264 -preset veryfast -c:a aac -y /tmp/rig-sync.mp4' +# …then blackdetect/silencedetect as below. 2026-09-01 baseline measured +# −65ms (audio early) → AV_OFFSET=0.065 now set in both station envs. -sh "$STATION_DIR/rig.sh" "$ENVT" > "$STATION_DIR/sync-rig.log" 2>&1 & -RIG_PID=$! -echo "▸ test card rig up (pid $RIG_PID), waiting for ingest…" +echo "▸ pausing $UNIT and airing the test card…" +ssh "$HOST" "sh -c 'systemctl --user stop $UNIT-watchdog.timer $UNIT; sleep 2; + grep -v \"^URL=\" $UNIT/$UNIT.env > /tmp/sync.env; + echo URL=file:///home/jas/wgtv/sync-test.html >> /tmp/sync.env; + echo UNMUTE_CLICK=1 >> /tmp/sync.env; + nohup sh $UNIT/rig.sh /tmp/sync.env > $UNIT/sync-rig.log 2>&1 & echo card-rig-up'" sleep 30 -HLS=$("$HOME/bin/yt-dlp" -g "$WATCH_URL" 2>/dev/null | head -1) -[ -n "$HLS" ] || { echo "✗ no HLS url"; kill "$RIG_PID" 2>/dev/null; exit 1; } -T0=$(date +%s.%N) echo "▸ recording ${DUR}s of the live edge…" +HLS=$(yt-dlp -g "$WATCH" | head -1) +T0=$(python3 -c 'import time; print(time.time())') ffmpeg -v error -i "$HLS" -t "$DUR" -c copy -y "$CAP" -kill "$RIG_PID" 2>/dev/null || true -pkill -f "user-data-dir=$HOME/.tv-rig-chrome" 2>/dev/null || true -pkill -f "Xvfb :99" 2>/dev/null || true -sleep 2 -systemctl --user start "$UNIT" "$UNIT-watchdog.timer" -echo "▸ station restored; analyzing…" - +echo "▸ analyzing…" FL=$(ffmpeg -i "$CAP" -vf blackdetect=d=0.2:pix_th=0.10 -an -f null - 2>&1 | grep -o "black_end:[0-9.]*" | cut -d: -f2) BP=$(ffmpeg -i "$CAP" -af silencedetect=n=-25dB:d=0.3 -vn -f null - 2>&1 | @@ -62,15 +71,12 @@ bp = [float(x) for x in os.environ["BEEPS"].split()] t0 = float(os.environ["T0"]) if not fl or not bp: print(f"✗ not enough events (flashes={len(fl)}, beeps={len(bp)})"); raise SystemExit(1) -# AV offset: each beep pairs with its nearest flash. offs = [] for b in bp: f = min(fl, key=lambda f: abs(f - b)) if abs(f - b) < 0.6: offs.append(b - f) offs.sort() med = offs[len(offs)//2] if offs else None -# Glass-to-glass: find a double flash (two onsets ~0.4s apart) → its first -# onset sat on a :00/:30 boundary when it aired. delay = None for a, b in zip(fl, fl[1:]): if 0.25 < b - a < 0.55: -- 2.51.2