Fix VCR scrubber: freeze timeline reference during replay

THE root cause: timeline coordinate system uses Date.now() as right
edge, making it a sliding window. Playhead position = (packetTs -
(now - scope)) / scope — but 'now' advances every frame, sliding
all positions left continuously. Any scrub position drifts.

Fix: VCR.frozenNow captures Date.now() when leaving LIVE mode.
All timeline calculations use frozenNow instead of Date.now() during
REPLAY/PAUSED. Timeline stops sliding. Playhead stays put.
Cleared on return to LIVE.
This commit is contained in:
you
2026-03-19 07:10:40 +00:00
parent 73402e9b0b
commit b07c5b0b86
+12 -19
View File
@@ -71,6 +71,8 @@
function vcrSetMode(mode) {
VCR.mode = mode;
if (mode !== 'LIVE' && !VCR.frozenNow) VCR.frozenNow = Date.now();
if (mode === 'LIVE') VCR.frozenNow = null;
updateVCRUI();
}
@@ -291,7 +293,7 @@
ctx.clearRect(0, 0, cw, ch);
const now = Date.now();
const now = VCR.frozenNow || Date.now();
const scopeMs = VCR.timelineScope;
const startTs = now - scopeMs;
@@ -333,34 +335,26 @@
}
function updateTimelinePlayhead() {
if (VCR.dragging) return; // don't fight the user's drag
if (VCR.dragging) return;
const playheadEl = document.getElementById('vcrPlayhead');
if (!playheadEl) return;
const canvas = document.getElementById('vcrTimeline');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const cw = canvas.offsetWidth;
const ch = canvas.offsetHeight;
const now = Date.now();
const now = VCR.frozenNow || Date.now();
const scopeMs = VCR.timelineScope;
const startTs = now - scopeMs;
// Redraw sparkline (cheap, avoids double-buffer complexity)
// Just draw playhead line on top — clear only the line area
let x;
if (VCR.mode === 'LIVE') {
x = cw; // rightmost = now
} else if (VCR.dragPct != null && (VCR.mode === 'REPLAY' || VCR.mode === 'PAUSED')) {
// After scrub: hold at drag position until replay tick updates it
x = VCR.dragPct * cw;
x = cw;
} else if (VCR.playhead >= 0 && VCR.playhead < VCR.buffer.length) {
const playTs = VCR.buffer[VCR.playhead].ts;
x = ((playTs - startTs) / scopeMs) * cw;
} else {
x = cw;
}
const playheadEl = document.getElementById('vcrPlayhead');
if (playheadEl) {
playheadEl.style.left = Math.max(0, Math.min(cw - 2, x)) + 'px';
}
playheadEl.style.left = Math.max(0, Math.min(cw - 2, x)) + 'px';
}
function handleTimelineClick(e) {
@@ -590,10 +584,9 @@
}
function scrubCommit() {
const now = Date.now();
const targetTs = now - VCR.timelineScope + VCR.dragPct * VCR.timelineScope;
VCR.frozenNow = Date.now(); // freeze timeline at scrub moment
const targetTs = VCR.frozenNow - VCR.timelineScope + VCR.dragPct * VCR.timelineScope;
// Always fetch from DB for the target time window and replay from there
stopReplay();
vcrSetMode('REPLAY');
const fetchFrom = new Date(targetTs - 30000).toISOString(); // 30s before target
@@ -618,7 +611,7 @@
VCR.playhead = closest;
// Only replay ~50 packets from scrub point, not entire buffer to end
VCR.scrubEnd = Math.min(closest + 50, VCR.buffer.length);
VCR.dragPct = null; // let replay tick drive playhead now
// dragPct no longer drives playhead; frozenNow + buffer timestamps do
startReplay();
})
.catch(() => {});