Full frontend built against the synthetic API (no dependence on lanes A/B/D): M4 — synchronized playback: master clock from performance.now() (never a <video>); per-video correction via requestVideoFrameCallback (exact mediaTime) with a currentTime+half-frame fallback — hard-seek >150ms / nudge playbackRate +/-5% 20-150ms / lock <20ms; out-of-range videos pause+dim; single audio source; dev sync-error overlay. Timebase mirrors config.py (lib/timebase.js). Verified: seek is exact to 0ms; continuous inter-video desync 13ms mean / 46ms max. M5 — 3D viewer: PLY point cloud, per-video camera paths + current-pose frusta, OrbitControls, snap-to-camera (intrinsics->PerspectiveCamera fov) + free roam. All COLMAP->Three.js via the frozen lib/pose.js; pose interpolation in lib/poseTrack.js (slerp+lerp of contract inputs). M6 — anchor overlays: letterbox-correct per-video canvas; anchors projected via pose.js; behind-camera cull. Projection agrees with an independent pinhole to 1.45e-13 px. Timeline — colored event markers + legend, hover tooltip, click-to-jump; draggable scrubber. Phase-3 seams (annotate.js M8, camPath.js M9) left as stubs. No frozen files edited; no new deps; no change requests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19 lines
989 B
JavaScript
19 lines
989 B
JavaScript
// Timebase convention (FROZEN CONTRACT) — the JS mirror of backend/festival4d/config.py
|
|
// (`t_video_from_global` / `t_global_from_video`). Keep these identical to the Python side.
|
|
//
|
|
// t_global is the master timeline in seconds. For a video v:
|
|
// t_video = (t_global - offset_ms/1000) * (1 + drift_ppm * 1e-6)
|
|
// The reference video has offset_ms == 0 (and drift_ppm == 0). A positive offset_ms means
|
|
// the video started recording later than the master zero, so at a given t_global its local
|
|
// playhead is earlier. Never re-derive this algebra inline — call these helpers everywhere.
|
|
|
|
/** Master-timeline seconds -> a video's local playhead seconds (FROZEN). */
|
|
export function tVideoFromGlobal(tGlobal, offsetMs, driftPpm = 0) {
|
|
return (tGlobal - offsetMs / 1000) * (1 + driftPpm * 1e-6);
|
|
}
|
|
|
|
/** Inverse of tVideoFromGlobal (FROZEN). */
|
|
export function tGlobalFromVideo(tVideo, offsetMs, driftPpm = 0) {
|
|
return tVideo / (1 + driftPpm * 1e-6) + offsetMs / 1000;
|
|
}
|