diff --git a/steps/src/debug.js b/steps/src/debug.js index b7cf431..4d9aa3c 100644 --- a/steps/src/debug.js +++ b/steps/src/debug.js @@ -5,10 +5,10 @@ export const debugFlags = { // Logs from videoFrameCallback and animationLoop in sync.js - sync: true, + sync: false, // Logs from the main p5.js draw() functions (e.g., radarSketch.js) - drawing: true, + drawing: false, // Logs related to file loading, parsing, and caching fileLoading: false, diff --git a/steps/src/main.js b/steps/src/main.js index ff1a792..25eccf0 100644 --- a/steps/src/main.js +++ b/steps/src/main.js @@ -51,6 +51,7 @@ import { findRadarFrameIndexForTime, extractTimestampInfo, parseTimestamp, + precomputeRadarVideoSync, throttle, formatTime, } from "./utils.js"; @@ -216,6 +217,7 @@ async function processFilePipeline() { appState.vizData = result.data; appState.globalMinSnr = result.minSnr; appState.globalMaxSnr = result.maxSnr; + precomputeRadarVideoSync(appState.vizData, appState.offset); } // 3. Handle Video Loading SECOND, with two-stage initialization @@ -317,16 +319,6 @@ function finalizeSetup(_parsedJsonData) { canvasPlaceholder.style.display = "none"; featureToggles.classList.remove("hidden"); - // This is the critical step. We loop through the radar data ONCE to create - // a relative timestamp in seconds for every frame. This simplifies all - // future synchronization math. - if (appState.vizData) { - appState.vizData.radarFrames.forEach((frame) => { - // frame.timestamp is the relative time in ms from the radar's start. - // We convert it to seconds for easier comparison with video.mediaTime. - frame.relativeTimeSec = frame.timestamp / 1000; - }); - } // Create the p5 instances if (!appState.p5_instance) { @@ -852,36 +844,51 @@ document.addEventListener("keydown", (event) => { }); -function calculateAndSetOffset() { +function calculateAndSetOffset() { const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); + + let videoDate = null; if (videoTimestampInfo) { - appState.videoStartDate = parseTimestamp( + videoDate = parseTimestamp( videoTimestampInfo.timestampStr, videoTimestampInfo.format ); - if (appState.videoStartDate) { - } + appState.videoStartDate = videoDate; // Store for potential future use } + let jsonDate = null; if (jsonTimestampInfo) { - const jsonDate = parseTimestamp( + jsonDate = parseTimestamp( jsonTimestampInfo.timestampStr, jsonTimestampInfo.format ); - if (jsonDate) { - appState.radarStartTimeMs = jsonDate.getTime(); - console.log(`Radar start date set to: ${jsonDate.toISOString()}`); - if (appState.videoStartDate) { - appState.offset = - appState.radarStartTimeMs - appState.videoStartDate.getTime(); - offsetInput.value = appState.offset; - localStorage.setItem("visualizerOffset", appState.offset); - autoOffsetIndicator.classList.remove("hidden"); - console.log(`Auto-calculated offset: ${appState.offset} ms`); - } + } + + let calculatedOffset = 0; + if (jsonDate && videoDate) { + appState.radarStartTimeMs = jsonDate.getTime(); + const offset = jsonDate.getTime() - videoDate.getTime(); + + // Logic Rule: If offset is invalid or too large, default to 0. + if (isNaN(offset) || Math.abs(offset) > 30000) { + console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 0.`); + calculatedOffset = 0; + } else { + calculatedOffset = offset; + autoOffsetIndicator.classList.remove("hidden"); + console.log(`Auto-calculated offset: ${calculatedOffset} ms`); } } + + appState.offset = calculatedOffset; + offsetInput.value = appState.offset; + localStorage.setItem("visualizerOffset", appState.offset); + + // Trigger Baking: This is the point where we apply the offset to the data. + if (appState.vizData) { + precomputeRadarVideoSync(appState.vizData, appState.offset); + } } offsetInput.addEventListener("keydown", (event) => { // Check if the key pressed was 'Enter' @@ -893,6 +900,7 @@ offsetInput.addEventListener("keydown", (event) => { const newOffset = parseFloat(offsetInput.value) || 0; appState.offset = newOffset; localStorage.setItem("visualizerOffset", newOffset); + if (appState.vizData) precomputeRadarVideoSync(appState.vizData, appState.offset); console.log(`Manual offset entered: ${appState.offset}ms`); // Force a resync of the video to the current frame diff --git a/steps/src/sync.js b/steps/src/sync.js index d80589c..e5b932f 100644 --- a/steps/src/sync.js +++ b/steps/src/sync.js @@ -12,7 +12,7 @@ import { egoSpeedDisplay, canSpeedDisplay, } from "./dom.js"; -import { findRadarFrameIndexForTime } from "./utils.js"; +import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js"; import { throttledUpdateExplorer } from "./dataExplorer.js"; import { debugFlags } from "./debug.js"; @@ -52,6 +52,9 @@ export function forceResyncWithOffset() { appState.offset = newOffset; // Update the central state localStorage.setItem("visualizerOffset", newOffset); // Persist it + // Re-Bake: Overwrite the pre-calculated sync times with the new offset. + precomputeRadarVideoSync(appState.vizData, appState.offset); + console.log(`Forcing resync with new offset: ${appState.offset}ms`); // If the video is playing, pause it to allow for precise frame tuning. @@ -115,11 +118,9 @@ export function updateFrame(frame, forceVideoSeek = false) { frameData ) { // Convert frame's relative time to the video's timeline - const targetRadarTimeSec = frameData.relativeTimeSec; - const targetVideoTimeSec = targetRadarTimeSec + (appState.offset / 1000); - + const targetVideoTimeSec = frameData.videoSyncedTime; - if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) { + if (targetVideoTimeSec >= 0 && videoPlayer.duration && targetVideoTimeSec <= videoPlayer.duration) { // Ensure target time is within video duration if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) { // Check for significant drift @@ -168,14 +169,13 @@ export function videoFrameCallback(now, metadata) { return; } - // 1. Get video time and calculate the target time on the radar's timeline. - const videoNowSec = metadata.mediaTime; - const targetRadarTimeSec = videoNowSec - (appState.offset / 1000); + // 1. Get the video's current time directly from the callback metadata. + const videoCurrentTime = metadata.mediaTime; // 2. Find the corresponding radar frame index. - const frameIndex = findRadarFrameIndexForTime(targetRadarTimeSec, appState.vizData); + const frameIndex = findRadarFrameIndexForTime(videoCurrentTime, appState.vizData); - // 3. Update the application state. This is the ONLY state this function changes. + // 3. Update the application state if the frame has changed. if (frameIndex !== appState.currentFrame) { appState.currentFrame = frameIndex; // This is the ONLY state this function should change. All UI updates are in animationLoop. @@ -264,7 +264,7 @@ function handleTimelineWheel(event) { // 4. Calculate the new frame index. const direction = Math.sign(event.deltaY); // FIX: Invert the direction. Scrolling down (positive deltaY) should advance the frame. - let newFrame = appState.currentFrame + direction * seekAmount; + let newFrame = appState.currentFrame - direction * seekAmount; // 5. Clamp the new frame to the valid range. const totalFrames = appState.vizData.radarFrames.length - 1; diff --git a/steps/src/utils.js b/steps/src/utils.js index b12f7be..be1345e 100644 --- a/steps/src/utils.js +++ b/steps/src/utils.js @@ -1,27 +1,35 @@ +/** + * Performs a binary search on the radar frames to find the frame index + * closest to the target video time. + * + * @param {number} targetTimeSec - The target time in seconds (from video.currentTime). + * @param {object} vizData - The visualization data containing radarFrames. + * @returns {number} The index of the closest radar frame. + */ export function findRadarFrameIndexForTime(targetTimeSec, vizData) { if (!vizData || vizData.radarFrames.length === 0) return -1; // Initialize low, high, and answer variables for binary search // 'ans' will store the index of the closest frame found so far // 'low' and 'high' define the search range let low = 0, - high = vizData.radarFrames.length - 1, - ans = 0; + high = vizData.radarFrames.length - 1; + // Perform binary search to find the radar frame whose timestamp is closest to, but not exceeding, the target time while (low <= high) { let mid = Math.floor((low + high) / 2); - // If the current frame's timestamp is less than or equal to the target time, - // it's a potential answer, and we try to find a more recent one in the right half. - if (vizData.radarFrames[mid].relativeTimeSec <= targetTimeSec) { - ans = mid; + const frameTime = vizData.radarFrames[mid].videoSyncedTime; + + if (frameTime < targetTimeSec) { low = mid + 1; - } else { - // If the current frame's timestamp is greater than the target time, - // we need to look in the left half. + } else if (frameTime > targetTimeSec) { high = mid - 1; + } else { + // Exact match found + return mid; } } - // Return the index of the found radar frame. - return ans; + // No exact match, return the closest index (clamped to bounds) + return Math.max(0, Math.min(high, vizData.radarFrames.length - 1)); } @@ -144,3 +152,16 @@ export function formatUTCTime(date) { const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0'); return `${hours}:${minutes}:${seconds}.${milliseconds}`; } + +/** + * Pre-calculates the video-synchronized timestamp for each radar frame. + * This "bakes" the offset into the data, simplifying future sync calculations. + * + * @param {object} vizData - The visualization data containing radarFrames. + * @param {number} offsetMs - The time offset between radar and video in milliseconds. + */ +export function precomputeRadarVideoSync(vizData, offsetMs) { + vizData.radarFrames.forEach((frame) => { + frame.videoSyncedTime = (frame.timestamp + offsetMs) / 1000; + }); +}