Fox 2026-06-05: "the lips are not synced with video for listener" and
"when i am at the bottom and new transcriptions flow in, keep
scrolling as they arrive unless ive scrolled up."
Lip-sync flap diagnosis (per fxhp-phone telemetry):
lip-sync pub=25cc delay=2.87s
lip-sync pub=25cc delay=6.87s
lip-sync pub=25cc delay=2.87s (every ~683ms)
lip-sync pub=25cc delay=6.87s
Two values exactly 4s apart (4s = worklet target). One source is
LIVE (worklet buffer near target → 6.87s = 2.87 native + 4 worklet),
the other is a STALE worklet whose source died (buffer drained to
0 → 2.87s = 2.87 native + 0 worklet). Both post bufferedSeconds
every 683ms. With LIP_SYNC_HISTORY = 5 odd-length, alternating
samples produce alternating medians: [A,B,A,B,A] → A;
[B,A,B,A,B] → B. Video target whipsaws ±4s per tick.
Three fixes:
1. LIP_SYNC_HISTORY = 10 (was 5). Even-length window.
2. medianOf returns the AVERAGE of the two middle values for
even-length arrays. Alternating samples now produce a stable
median = (A+B)/2 — at least the value doesn't whip.
3. Stale-source filter in refreshLipSyncForUuid: if
node.bufferedSeconds < 0.5 AND nativeJbufSec > 1.0, skip this
refresh entirely. A worklet whose buffer is drained while the
native audio receiver still reports a healthy jitter buffer is
from an OLD subscription that's no longer carrying audio. Its
"0" reading would pull the median toward the dead value.
Only the LIVE worklet's refreshes update the history; the median
stabilizes on the actual audio total delay.
Autoscroll fix in appendTranscriptLine:
The previous logic computed "near bottom" AFTER appending the line.
scrollHeight grew the instant the line was in the DOM, so
(scrollTop + clientHeight) became < (scrollHeight - 40) for every
append — autoscroll never fired. Now we check BEFORE appending and
widen the tolerance to 120px (covers touch-scroll inertia residue).