Timers: real-time frame-accurate LTC display
The LTC card repainted only on server anchors (~4x/s), so the frame counter jumped in ~7-frame steps. Now the client counts every frame locally between anchors and re-syncs on each one — frame-accurate and smooth, with no extra network traffic. - Server anchors carry ageMs (how old the decoded timecode is) so clients can pin the anchor to their own clock. ltc-listener reports it while running; composeLtc passes it through (listener) or derives it (reader). - Client does exact SMPTE frame<->timecode math (parseTc/tcToFrames/ framesToTc, incl. 29.97 drop-frame minute counting) and advances the display in a requestAnimationFrame loop at the LTC's own rate. - Robust when rAF is throttled (backgrounded tab): render() paints the extrapolated frame at the 4x/s anchor rate too, so the display never freezes — the rAF loop only refines it to every frame when visible. - Anchor jitter can't tick the counter backwards (small regressions clamped; genuine seeks/re-strikes pass); freshness cap stops freewheel into phantom frames when anchors stop arriving. Verified: SMPTE math against the encoder over 200k drop-frame frames plus round-trips; the extrapolation pipeline simulated at 30/25/29.97DF showing strict +1-per-frame advance and correct DF boundary crossing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dba10a30fe
commit
7e75146838
@@ -132,7 +132,7 @@ module.exports = {
|
||||
if (!ltcEnabled) {
|
||||
return { supported: false, source: '', time: '', receiving: false, status: '', fps: 0, df: false, note: 'LTC listener disabled' };
|
||||
}
|
||||
const audioVerdict = (src, tag) => ({
|
||||
const audioVerdict = (src, tag, ageMs) => ({
|
||||
supported: true,
|
||||
source: tag,
|
||||
time: src.time,
|
||||
@@ -140,16 +140,19 @@ module.exports = {
|
||||
status: src.state,
|
||||
fps: src.fps,
|
||||
df: src.df,
|
||||
// While running: how old this timecode is, so clients can pin the
|
||||
// anchor to their own clock and count frames between updates.
|
||||
...(src.state === 'running' && Number.isFinite(ageMs) ? { ageMs: Math.max(0, ageMs) } : {}),
|
||||
note: '',
|
||||
});
|
||||
// The built-in listener while it has actual data …
|
||||
if (listenerLtc && !listenerLtc.error && listenerLtc.state !== 'nosignal') {
|
||||
return audioVerdict(listenerLtc, 'listener');
|
||||
return audioVerdict(listenerLtc, 'listener', listenerLtc.ageMs);
|
||||
}
|
||||
// … else a fresh remote reader (during migrations both may run —
|
||||
// real data beats no data) …
|
||||
if (ingest && Date.now() - ingest.at < INGEST_STALE_MS) {
|
||||
return audioVerdict(ingest, 'reader');
|
||||
return audioVerdict(ingest, 'reader', Date.now() - ingest.at);
|
||||
}
|
||||
// … else a healthy-but-silent listener is believed over the indirect
|
||||
// sources: no audio means the LTC really stopped.
|
||||
|
||||
Reference in New Issue
Block a user