Browse Source
Subject: feat(sync): Add advanced debug overlay and fix timing bugs
Subject: feat(sync): Add advanced debug overlay and fix timing bugs
Body: This commit introduces a new advanced debugging overlay to help diagnose synchronization issues and fixes three core timing bugs that caused data streams to be misaligned. Advanced Debug Overlay: A new "Show Advanced Debug" toggle has been added to the UI. When enabled, it displays critical synchronization diagnostics in real-time, including: Video vs. Target Radar timestamps The calculated "Drift" in milliseconds between the two Absolute start times for video and radar recordings The currently applied offset This provides a precise tool for manually calibrating the offset and verifying sync accuracy. Synchronization Fixes: The previous implementation suffered from several race conditions and logical errors in its time management: Speed Graph Misalignment: The ego speed line on the graph was plotted using raw radar timestamps, ignoring the video offset. This has been corrected to use the offset-adjusted timestampMs, aligning it with the CAN data. Playback Drift: The main animation loop was incorrectly applying the time offset a second time during playback, causing the radar visualization to lead or lag the video. The redundant offset calculation has been removed from the animationLoop. Seeking Inaccuracy: When scrubbing the timeline, the UI would update the CAN speed using a stale videoPlayer.currentTime value due to the asynchronous nature of video seeking. The logic in updateFrame now uses the precise, calculated target time for the update, ensuring the EGO and CAN speed indicators match perfectly during seeks. These changes result in a significantly more robust and verifiable synchronization between the video and radar data feeds.refactor/modularize
5 changed files with 327 additions and 202 deletions
-
164steps/index.html
-
357steps/src/dom.js
-
4steps/src/main.js
-
2steps/src/p5/speedGraphSketch.js
-
2steps/src/sync.js
@ -1,158 +1,247 @@ |
|||||
import { appState } from './state.js'; |
|
||||
import { findLastCanIndexBefore } from './utils.js'; |
|
||||
import { VIDEO_FPS } from './constants.js'; |
|
||||
|
import { appState } from "./state.js"; |
||||
|
import { findLastCanIndexBefore } from "./utils.js"; |
||||
|
import { VIDEO_FPS } from "./constants.js"; |
||||
|
|
||||
// --- DOM Element References --- //
|
// --- DOM Element References --- //
|
||||
|
|
||||
|
|
||||
export const canvasContainer = document.getElementById('canvas-container'); |
|
||||
export const canvasPlaceholder = document.getElementById('canvas-placeholder'); |
|
||||
export const videoPlayer = document.getElementById('video-player'); |
|
||||
export const videoPlaceholder = document.getElementById('video-placeholder'); |
|
||||
export const loadJsonBtn = document.getElementById('load-json-btn'); |
|
||||
export const loadVideoBtn = document.getElementById('load-video-btn'); |
|
||||
export const loadCanBtn = document.getElementById('load-can-btn'); |
|
||||
export const jsonFileInput = document.getElementById('json-file-input'); |
|
||||
export const videoFileInput = document.getElementById('video-file-input'); |
|
||||
export const canFileInput = document.getElementById('can-file-input'); |
|
||||
export const playPauseBtn = document.getElementById('play-pause-btn'); |
|
||||
export const stopBtn = document.getElementById('stop-btn'); |
|
||||
export const timelineSlider = document.getElementById('timeline-slider'); |
|
||||
export const frameCounter = document.getElementById('frame-counter'); |
|
||||
export const offsetInput = document.getElementById('offset-input'); |
|
||||
export const speedSlider = document.getElementById('speed-slider'); |
|
||||
export const speedDisplay = document.getElementById('speed-display'); |
|
||||
export const featureToggles = document.getElementById('feature-toggles'); |
|
||||
export const toggleSnrColor = document.getElementById('toggle-snr-color'); |
|
||||
export const toggleClusterColor = document.getElementById('toggle-cluster-color'); |
|
||||
export const toggleInlierColor = document.getElementById('toggle-inlier-color'); |
|
||||
export const toggleStationaryColor = document.getElementById('toggle-stationary-color'); |
|
||||
export const toggleVelocity = document.getElementById('toggle-velocity'); |
|
||||
export const toggleTracks = document.getElementById('toggle-tracks'); |
|
||||
export const toggleEgoSpeed = document.getElementById('toggle-ego-speed'); |
|
||||
export const toggleFrameNorm = document.getElementById('toggle-frame-norm'); |
|
||||
export const toggleDebugOverlay = document.getElementById('toggle-debug-overlay'); |
|
||||
export const egoSpeedDisplay = document.getElementById('ego-speed-display'); |
|
||||
export const canSpeedDisplay = document.getElementById('can-speed-display'); |
|
||||
export const debugOverlay = document.getElementById('debug-overlay'); |
|
||||
export const snrMinInput = document.getElementById('snr-min-input'); |
|
||||
export const snrMaxInput = document.getElementById('snr-max-input'); |
|
||||
export const applySnrBtn = document.getElementById('apply-snr-btn'); |
|
||||
export const autoOffsetIndicator = document.getElementById('auto-offset-indicator'); |
|
||||
export const clearCacheBtn = document.getElementById('clear-cache-btn'); |
|
||||
export const speedGraphContainer = document.getElementById('speed-graph-container'); |
|
||||
export const speedGraphPlaceholder = document.getElementById('speed-graph-placeholder'); |
|
||||
export const modalContainer = document.getElementById('modal-container'); |
|
||||
export const modalOverlay = document.getElementById('modal-overlay'); |
|
||||
export const modalContent = document.getElementById('modal-content'); |
|
||||
export const modalText = document.getElementById('modal-text'); |
|
||||
export const modalOkBtn = document.getElementById('modal-ok-btn'); |
|
||||
export const modalCancelBtn = document.getElementById('modal-cancel-btn'); |
|
||||
export const toggleCloseUp = document.getElementById('toggle-close-up'); |
|
||||
|
export const canvasContainer = document.getElementById("canvas-container"); |
||||
|
export const canvasPlaceholder = document.getElementById("canvas-placeholder"); |
||||
|
export const videoPlayer = document.getElementById("video-player"); |
||||
|
export const videoPlaceholder = document.getElementById("video-placeholder"); |
||||
|
export const loadJsonBtn = document.getElementById("load-json-btn"); |
||||
|
export const loadVideoBtn = document.getElementById("load-video-btn"); |
||||
|
export const loadCanBtn = document.getElementById("load-can-btn"); |
||||
|
export const jsonFileInput = document.getElementById("json-file-input"); |
||||
|
export const videoFileInput = document.getElementById("video-file-input"); |
||||
|
export const canFileInput = document.getElementById("can-file-input"); |
||||
|
export const playPauseBtn = document.getElementById("play-pause-btn"); |
||||
|
export const stopBtn = document.getElementById("stop-btn"); |
||||
|
export const timelineSlider = document.getElementById("timeline-slider"); |
||||
|
export const frameCounter = document.getElementById("frame-counter"); |
||||
|
export const offsetInput = document.getElementById("offset-input"); |
||||
|
export const speedSlider = document.getElementById("speed-slider"); |
||||
|
export const speedDisplay = document.getElementById("speed-display"); |
||||
|
export const featureToggles = document.getElementById("feature-toggles"); |
||||
|
export const toggleSnrColor = document.getElementById("toggle-snr-color"); |
||||
|
export const toggleClusterColor = document.getElementById( |
||||
|
"toggle-cluster-color" |
||||
|
); |
||||
|
export const toggleInlierColor = document.getElementById("toggle-inlier-color"); |
||||
|
export const toggleStationaryColor = document.getElementById( |
||||
|
"toggle-stationary-color" |
||||
|
); |
||||
|
export const toggleVelocity = document.getElementById("toggle-velocity"); |
||||
|
export const toggleTracks = document.getElementById("toggle-tracks"); |
||||
|
export const toggleEgoSpeed = document.getElementById("toggle-ego-speed"); |
||||
|
export const toggleFrameNorm = document.getElementById("toggle-frame-norm"); |
||||
|
export const toggleDebugOverlay = document.getElementById( |
||||
|
"toggle-debug-overlay" |
||||
|
); |
||||
|
export const egoSpeedDisplay = document.getElementById("ego-speed-display"); |
||||
|
export const canSpeedDisplay = document.getElementById("can-speed-display"); |
||||
|
export const debugOverlay = document.getElementById("debug-overlay"); |
||||
|
export const toggleDebug2Overlay = document.getElementById( |
||||
|
"toggle-debug2-overlay" |
||||
|
); |
||||
|
export const snrMinInput = document.getElementById("snr-min-input"); |
||||
|
export const snrMaxInput = document.getElementById("snr-max-input"); |
||||
|
export const applySnrBtn = document.getElementById("apply-snr-btn"); |
||||
|
export const autoOffsetIndicator = document.getElementById( |
||||
|
"auto-offset-indicator" |
||||
|
); |
||||
|
export const clearCacheBtn = document.getElementById("clear-cache-btn"); |
||||
|
export const speedGraphContainer = document.getElementById( |
||||
|
"speed-graph-container" |
||||
|
); |
||||
|
export const speedGraphPlaceholder = document.getElementById( |
||||
|
"speed-graph-placeholder" |
||||
|
); |
||||
|
export const modalContainer = document.getElementById("modal-container"); |
||||
|
export const modalOverlay = document.getElementById("modal-overlay"); |
||||
|
export const modalContent = document.getElementById("modal-content"); |
||||
|
export const modalText = document.getElementById("modal-text"); |
||||
|
export const modalOkBtn = document.getElementById("modal-ok-btn"); |
||||
|
export const modalCancelBtn = document.getElementById("modal-cancel-btn"); |
||||
|
export const toggleCloseUp = document.getElementById("toggle-close-up"); |
||||
|
|
||||
//----------------------UPDATE FRAME Function----------------------//
|
//----------------------UPDATE FRAME Function----------------------//
|
||||
|
|
||||
|
// Located in: src/dom.js
|
||||
|
|
||||
export function updateFrame(frame, forceVideoSeek) { |
export function updateFrame(frame, forceVideoSeek) { |
||||
if (!appState.vizData || frame < 0 || frame >= appState.vizData.radarFrames.length) return; |
|
||||
appState.currentFrame = frame; |
|
||||
timelineSlider.value = appState.currentFrame; |
|
||||
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${appState.vizData.radarFrames.length}`; |
|
||||
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|
||||
if (toggleEgoSpeed.checked && frameData) { |
|
||||
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1); |
|
||||
egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`; |
|
||||
egoSpeedDisplay.classList.remove('hidden'); |
|
||||
} else { |
|
||||
egoSpeedDisplay.classList.add('hidden'); |
|
||||
|
if ( |
||||
|
!appState.vizData || |
||||
|
frame < 0 || |
||||
|
frame >= appState.vizData.radarFrames.length |
||||
|
) |
||||
|
return; |
||||
|
appState.currentFrame = frame; |
||||
|
timelineSlider.value = appState.currentFrame; |
||||
|
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${ |
||||
|
appState.vizData.radarFrames.length |
||||
|
}`;
|
||||
|
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
||||
|
if (toggleEgoSpeed.checked && frameData) { |
||||
|
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1); |
||||
|
egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`; |
||||
|
egoSpeedDisplay.classList.remove("hidden"); |
||||
|
} else { |
||||
|
egoSpeedDisplay.classList.add("hidden"); |
||||
|
} |
||||
|
|
||||
|
// --- Start of fix ---
|
||||
|
let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time
|
||||
|
|
||||
|
if ( |
||||
|
forceVideoSeek && |
||||
|
videoPlayer.src && |
||||
|
videoPlayer.readyState > 1 && |
||||
|
appState.videoStartDate && |
||||
|
frameData |
||||
|
) { |
||||
|
const offsetMs = parseFloat(offsetInput.value) || 0; |
||||
|
const targetRadarTimeMs = frameData.timestampMs; |
||||
|
const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000; |
||||
|
if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) { |
||||
|
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) { |
||||
|
videoPlayer.currentTime = targetVideoTimeSec; |
||||
|
} |
||||
|
// MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime
|
||||
|
timeForUpdates = targetVideoTimeSec; |
||||
} |
} |
||||
if (forceVideoSeek && videoPlayer.src && videoPlayer.readyState > 1 && appState.videoStartDate && frameData) { |
|
||||
const offsetMs = parseFloat(offsetInput.value) || 0; |
|
||||
const targetRadarTimeMs = frameData.timestampMs; |
|
||||
const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000; |
|
||||
if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) { |
|
||||
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) { |
|
||||
videoPlayer.currentTime = targetVideoTimeSec; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
if (!appState.isPlaying) { |
|
||||
updateCanDisplay(videoPlayer.currentTime); |
|
||||
updateDebugOverlay(videoPlayer.currentTime); |
|
||||
} |
|
||||
if (appState.p5_instance) appState.p5_instance.redraw(); |
|
||||
if (appState.speedGraphInstance && !appState.isPlaying) appState.speedGraphInstance.redraw(); |
|
||||
|
} |
||||
|
|
||||
|
if (!appState.isPlaying) { |
||||
|
// MODIFIED: Use our new synchronized time variable
|
||||
|
updateCanDisplay(timeForUpdates); |
||||
|
updateDebugOverlay(timeForUpdates); |
||||
|
} |
||||
|
// --- End of fix ---
|
||||
|
|
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (appState.speedGraphInstance && !appState.isPlaying) |
||||
|
appState.speedGraphInstance.redraw(); |
||||
} |
} |
||||
|
|
||||
//----------------------RESET VISUALIZATION Function----------------------//
|
//----------------------RESET VISUALIZATION Function----------------------//
|
||||
|
|
||||
|
|
||||
export function resetVisualization() { |
export function resetVisualization() { |
||||
appState.isPlaying = false; |
|
||||
playPauseBtn.textContent = 'Play'; |
|
||||
const numFrames = appState.vizData.radarFrames.length; |
|
||||
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0; |
|
||||
updateFrame(0, true); |
|
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
const numFrames = appState.vizData.radarFrames.length; |
||||
|
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0; |
||||
|
updateFrame(0, true); |
||||
} |
} |
||||
|
|
||||
|
|
||||
//----------------------CAN DISPLAY UPDATE Function----------------------//
|
//----------------------CAN DISPLAY UPDATE Function----------------------//
|
||||
|
|
||||
|
|
||||
|
|
||||
export function updateCanDisplay(currentMediaTime) { |
export function updateCanDisplay(currentMediaTime) { |
||||
if (appState.canData.length > 0 && videoPlayer.src && appState.videoStartDate) { |
|
||||
const videoAbsoluteTimeMs = appState.videoStartDate.getTime() + (currentMediaTime * 1000); |
|
||||
const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs, appState.canData); |
|
||||
if (canIndex !== -1) { |
|
||||
const currentCanMessage = appState.canData[canIndex]; |
|
||||
canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`; |
|
||||
canSpeedDisplay.classList.remove('hidden'); |
|
||||
} |
|
||||
else { |
|
||||
canSpeedDisplay.classList.add('hidden'); |
|
||||
} |
|
||||
} |
|
||||
else { |
|
||||
canSpeedDisplay.classList.add('hidden'); |
|
||||
|
if ( |
||||
|
appState.canData.length > 0 && |
||||
|
videoPlayer.src && |
||||
|
appState.videoStartDate |
||||
|
) { |
||||
|
const videoAbsoluteTimeMs = |
||||
|
appState.videoStartDate.getTime() + currentMediaTime * 1000; |
||||
|
const canIndex = findLastCanIndexBefore( |
||||
|
videoAbsoluteTimeMs, |
||||
|
appState.canData |
||||
|
); |
||||
|
if (canIndex !== -1) { |
||||
|
const currentCanMessage = appState.canData[canIndex]; |
||||
|
canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`; |
||||
|
canSpeedDisplay.classList.remove("hidden"); |
||||
|
} else { |
||||
|
canSpeedDisplay.classList.add("hidden"); |
||||
} |
} |
||||
|
} else { |
||||
|
canSpeedDisplay.classList.add("hidden"); |
||||
|
} |
||||
} |
} |
||||
|
|
||||
|
|
||||
//----------------------DEBUG OVERLAY UPDATE Function----------------------//
|
//----------------------DEBUG OVERLAY UPDATE Function----------------------//
|
||||
|
|
||||
|
|
||||
|
|
||||
export function updateDebugOverlay(currentMediaTime) { |
export function updateDebugOverlay(currentMediaTime) { |
||||
if (!toggleDebugOverlay.checked) { |
|
||||
debugOverlay.classList.add('hidden'); |
|
||||
return; |
|
||||
} debugOverlay.classList.remove('hidden'); |
|
||||
let content = []; |
|
||||
if (appState.videoStartDate) { |
|
||||
const videoAbsoluteTimeMs = appState.videoStartDate.getTime() + (currentMediaTime * 1000); |
|
||||
content.push(`Media Time (s): ${currentMediaTime.toFixed(3)}`); |
|
||||
const videoFrame = Math.floor(currentMediaTime * VIDEO_FPS); |
|
||||
content.push(`Video Frame: ${videoFrame}`); |
|
||||
content.push(`Vid Abs Time: ${new Date(videoAbsoluteTimeMs).toISOString().split('T')[1].replace('Z', '')}`); |
|
||||
if (appState.canData.length > 0) { |
|
||||
const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs, appState.canData); |
|
||||
if (canIndex !== -1) { |
|
||||
const currentCanMessage = appState.canData[canIndex]; |
|
||||
content.push(`CAN Abs Time: ${new Date(currentCanMessage.time).toISOString().split('T')[1].replace('Z', '')}`); |
|
||||
content.push(`CAN Speed: ${currentCanMessage.speed} km/h`); |
|
||||
} |
|
||||
else { |
|
||||
content.push('CAN: No data for time'); |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
else { |
|
||||
content.push('Video not loaded...'); |
|
||||
|
|
||||
} if (appState.vizData) { |
|
||||
content.push(`Radar Frame: ${appState.currentFrame + 1}`); |
|
||||
if (appState.vizData.radarFrames[appState.currentFrame]) |
|
||||
content.push(`Radar Abs Time: ${new Date(appState.vizData.radarFrames[appState.currentFrame].timestampMs).toISOString().split('T')[1].replace('Z', '')}`); |
|
||||
} debugOverlay.innerHTML = content.join('<br>'); |
|
||||
} |
|
||||
|
// Check the state of both debug toggles
|
||||
|
const isDebug1Visible = toggleDebugOverlay.checked; |
||||
|
const isDebug2Visible = toggleDebug2Overlay.checked; |
||||
|
|
||||
|
// If neither is checked, hide the overlay and stop
|
||||
|
if (!isDebug1Visible && !isDebug2Visible) { |
||||
|
debugOverlay.classList.add("hidden"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
debugOverlay.classList.remove("hidden"); |
||||
|
let content = []; |
||||
|
|
||||
|
// --- Logic for the original debug overlay ---
|
||||
|
if (isDebug1Visible) { |
||||
|
content.push(`--- Basic Info ---`); |
||||
|
if (appState.videoStartDate) { |
||||
|
const videoAbsoluteTimeMs = |
||||
|
appState.videoStartDate.getTime() + currentMediaTime * 1000; |
||||
|
content.push(`Media Time (s): ${currentMediaTime.toFixed(3)}`); |
||||
|
content.push(`Video Frame: ${Math.floor(currentMediaTime * VIDEO_FPS)}`); |
||||
|
content.push( |
||||
|
`Vid Abs Time: ${new Date(videoAbsoluteTimeMs) |
||||
|
.toISOString() |
||||
|
.split("T")[1] |
||||
|
.replace("Z", "")}`
|
||||
|
); |
||||
|
} else { |
||||
|
content.push("Video not loaded..."); |
||||
|
} |
||||
|
if ( |
||||
|
appState.vizData && |
||||
|
appState.vizData.radarFrames[appState.currentFrame] |
||||
|
) { |
||||
|
content.push(`Radar Frame: ${appState.currentFrame + 1}`); |
||||
|
const frameTime = |
||||
|
appState.vizData.radarFrames[appState.currentFrame].timestampMs; |
||||
|
content.push( |
||||
|
`Radar Abs Time: ${new Date( |
||||
|
appState.videoStartDate.getTime() + frameTime |
||||
|
) |
||||
|
.toISOString() |
||||
|
.split("T")[1] |
||||
|
.replace("Z", "")}`
|
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Logic for the new advanced debug overlay ---
|
||||
|
if (isDebug2Visible) { |
||||
|
content.push(`--- Sync Diagnostics ---`); |
||||
|
if ( |
||||
|
appState.videoStartDate && |
||||
|
appState.vizData && |
||||
|
appState.vizData.radarFrames[appState.currentFrame] |
||||
|
) { |
||||
|
const currentRadarFrame = |
||||
|
appState.vizData.radarFrames[appState.currentFrame]; |
||||
|
const targetRadarTimeMs = currentRadarFrame.timestampMs; |
||||
|
const driftMs = currentMediaTime * 1000 - targetRadarTimeMs; |
||||
|
|
||||
|
// Style the drift value to be green if sync is good, and red if it's off
|
||||
|
const driftColor = Math.abs(driftMs) > 40 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
|
||||
|
|
||||
|
content.push(`Video Time (s): ${currentMediaTime.toFixed(3)}`); |
||||
|
content.push(`Target Radar Time (ms): ${targetRadarTimeMs.toFixed(0)}`); |
||||
|
content.push( |
||||
|
`Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>` |
||||
|
); |
||||
|
content.push( |
||||
|
`Video Start Time: ${appState.videoStartDate.toISOString()}` |
||||
|
); |
||||
|
content.push( |
||||
|
`Radar Start Time: ${new Date(appState.radarStartTimeMs).toISOString()}` |
||||
|
); |
||||
|
content.push(`Calculated Offset (ms): ${offsetInput.value}`); |
||||
|
} else { |
||||
|
content.push("Load video and radar data to see sync info."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
debugOverlay.innerHTML = content.join("<br>"); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue