Browse Source

refactor(core): Enhance robustness of file loading and UI state management

This commit introduces a series of significant improvements to the file processing pipeline and UI update logic, resolving several critical bugs related to state management, UI freezes, and component rendering. The primary goal was to decouple UI components from strict file dependencies and ensure the application remains responsive and predictable under various file loading scenarios.

### Key Changes and Fixes:

1.  **Graceful Handling of File Swapping (Fixes UI Freeze):**
    -   Resolved a critical bug where loading a new JSON file while a video was already present would cause the application to freeze at "Parsing JSON (100%)".
    -   The root cause was an incomplete state reset. The fix involves calling `resetVisualization()` at the beginning of `finalizeSetup()`, which ensures the playback timeline and animation loops are reset to a clean slate before redrawing with new data.
    -   This change allows users to seamlessly load and compare different JSON logs against the same video without unloading the video or freezing the application.

2.  **Decoupled and Robust Speed Graph:**
    -   The speed graph is no longer dependent on a video file to render. It will now draw correctly as long as valid JSON data is available.
    -   The graph's X-axis is now consistently scaled based on the JSON data's internal duration (`finalDuration = jsonDuration`). This makes the graph's behavior predictable and ensures all data within the JSON file is always visible.
    -   Corrected the data source for plotting. The graph now uses the `frame.timestamp` property, which is the reliable relative time from the start of the log. This fixes a bug where the graph would appear to start late (e.g., at 220s) because it was incorrectly using `timestampMs`, a property modified by the video synchronization offset.

3.  **Object URL Lifecycle Management (Memory Leak Fix):**
    -   Implemented proper lifecycle management for video object URLs created with `URL.createObjectURL()`.
    -   Before creating a new video URL, the application now checks for and revokes any existing `appState.videoObjectUrl` to prevent memory leaks from accumulating unused blob URLs.

4.  **Verified Synchronization Failsafe:**
    -   Confirmed that the failsafe logic in `calculateAndSetOffset` is working as intended. If the automatically calculated offset between a video and JSON file exceeds a 30-second threshold, it correctly defaults to `0`, preventing extreme and incorrect synchronization when mismatched files are loaded.
refactor/sync-centralize
RUSHIL AMBARISH KADU 6 months ago
parent
commit
ae9b8dc62b
  1. 45
      steps/src/fileLoader.js
  2. 31
      steps/src/p5/speedGraphSketch.js

45
steps/src/fileLoader.js

@ -180,6 +180,12 @@ function loadVideo(file, isRetry = false) {
let metadataLoaded = false; let metadataLoaded = false;
let loadTimeout; let loadTimeout;
// Before creating a new URL, revoke the old one if it exists.
if (!isRetry && appState.videoObjectUrl) {
URL.revokeObjectURL(appState.videoObjectUrl);
appState.videoObjectUrl = null;
}
const fileURL = isRetry ? videoPlayer.src : URL.createObjectURL(file); const fileURL = isRetry ? videoPlayer.src : URL.createObjectURL(file);
const cleanup = () => { const cleanup = () => {
@ -233,6 +239,7 @@ function loadVideo(file, isRetry = false) {
// Revoke URL to free memory if we're giving up on it // Revoke URL to free memory if we're giving up on it
if (videoPlayer.src.startsWith('blob:')) { if (videoPlayer.src.startsWith('blob:')) {
URL.revokeObjectURL(videoPlayer.src); URL.revokeObjectURL(videoPlayer.src);
appState.videoObjectUrl = null; // Clear from state
} }
videoPlayer.src = ""; videoPlayer.src = "";
videoPlayer.classList.add("hidden"); videoPlayer.classList.add("hidden");
@ -267,6 +274,9 @@ function loadVideo(file, isRetry = false) {
} }
function finalizeSetup() { function finalizeSetup() {
// CRITICAL FIX: Always reset the visualization state before redrawing.
// This pauses the video and resets the timeline, ensuring a clean slate for the new data.
resetVisualization();
// 1. Manage Placeholders & Visibility // 1. Manage Placeholders & Visibility
// If we have data (vizData), we show the canvas container. // If we have data (vizData), we show the canvas container.
if (appState.vizData) { if (appState.vizData) {
@ -304,13 +314,31 @@ function finalizeSetup() {
appState.speedGraphInstance = new p5(speedGraphSketch); appState.speedGraphInstance = new p5(speedGraphSketch);
} }
// Important: Reset the visualization timeline to 0
resetVisualization();
// Update speed graph with new data + video duration // Update speed graph with new data + video duration
// Note: videoPlayer.duration might be NaN if video isn't loaded.
const duration = appState.videoMissing ? 0 : (videoPlayer.duration || 0);
appState.speedGraphInstance.setData(appState.vizData, duration);
// Determine the most appropriate duration for the graph's X-axis.
let finalDuration = 0;
let jsonDuration = 0;
// 1. Calculate duration from the JSON data itself as a reliable baseline.
if (appState.vizData.radarFrames && appState.vizData.radarFrames.length > 0) {
const lastFrame = appState.vizData.radarFrames[appState.vizData.radarFrames.length - 1];
jsonDuration = lastFrame.timestamp / 1000.0;
}
// 2. Get video duration, normalizing invalid values.
let videoDuration = appState.videoMissing ? 0 : (videoPlayer.duration || 0);
if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) {
videoDuration = 0;
}
// 3. Set the graph's duration. Prioritize JSON duration, but clip it
// to the video's duration if a video is present and shorter.
finalDuration = jsonDuration;
if (videoDuration > 0 && jsonDuration > videoDuration) {
finalDuration = jsonDuration;
}
appState.speedGraphInstance.setData(appState.vizData, finalDuration);
appState.speedGraphInstance.redraw(); appState.speedGraphInstance.redraw();
} }
@ -332,6 +360,11 @@ function setupVideoPlayer(fileURL) {
videoPlayer.classList.remove("hidden"); videoPlayer.classList.remove("hidden");
videoPlaceholder.classList.add("hidden"); videoPlaceholder.classList.add("hidden");
videoPlayer.playbackRate = parseFloat(speedSlider.value); videoPlayer.playbackRate = parseFloat(speedSlider.value);
// Store the new object URL if it's a blob
if (fileURL.startsWith("blob:")) {
appState.videoObjectUrl = fileURL;
}
} }
function calculateAndSetOffset() { function calculateAndSetOffset() {

31
steps/src/p5/speedGraphSketch.js

@ -28,7 +28,7 @@ export const speedGraphSketch = function (p) {
b.fill(textColor); b.fill(textColor);
b.textSize(10); b.textSize(10);
for (let s = minSpeed; s <= maxSpeed; s += 10) {
for (let s = minSpeed; s <= maxSpeed; s += 5) {
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.text(s, pad.left - 8, y); b.text(s, pad.left - 8, y);
if (s === 0) { if (s === 0) {
@ -43,7 +43,7 @@ export const speedGraphSketch = function (p) {
} }
b.fill(textColor); b.fill(textColor);
b.text("km/h", pad.left - 8, pad.top - 8);
b.text("km/h", pad.left - 8, pad.top - 12);
b.textAlign(b.CENTER, b.TOP); b.textAlign(b.CENTER, b.TOP);
b.noStroke(); b.noStroke();
b.fill(isDark ? 180 : 150); b.fill(isDark ? 180 : 150);
@ -54,6 +54,16 @@ export const speedGraphSketch = function (p) {
b.text(Math.round(t), x, b.height - pad.bottom + 5); b.text(Math.round(t), x, b.height - pad.bottom + 5);
} }
b.fill(textColor); b.fill(textColor);
// Draw vertical grid lines for time
b.strokeWeight(1);
b.stroke(isDark ? 80 : 230);
for (let t = 10; t <= videoDuration; t += 10) {
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right);
b.line(x, pad.top, x, b.height - pad.bottom);
}
b.noStroke();
b.text("Time (s)", (pad.left + (b.width - pad.right)) / 2, b.height - pad.bottom + 18); b.text("Time (s)", (pad.left + (b.width - pad.right)) / 2, b.height - pad.bottom + 18);
b.pop(); b.pop();
@ -65,7 +75,7 @@ export const speedGraphSketch = function (p) {
b.beginShape(); b.beginShape();
for (const frame of radarData.radarFrames) { for (const frame of radarData.radarFrames) {
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) continue; if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) continue;
const relTime = frame.timestampMs / 1000;
const relTime = frame.timestamp / 1000;
if (relTime >= 0 && relTime <= videoDuration) { if (relTime >= 0 && relTime <= videoDuration) {
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right);
const y = b.map(frame.canVehSpeed_kmph, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); const y = b.map(frame.canVehSpeed_kmph, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
@ -81,7 +91,7 @@ export const speedGraphSketch = function (p) {
b.drawingContext.setLineDash([5, 5]); b.drawingContext.setLineDash([5, 5]);
b.beginShape(); b.beginShape();
for (const frame of radarData.radarFrames) { for (const frame of radarData.radarFrames) {
const relTime = frame.timestampMs / 1000;
const relTime = frame.timestamp / 1000;
if (relTime >= 0 && relTime <= videoDuration) { if (relTime >= 0 && relTime <= videoDuration) {
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right);
const egoSpeedKmh = frame.egoVelocity[1] * 3.6; const egoSpeedKmh = frame.egoVelocity[1] * 3.6;
@ -154,6 +164,11 @@ export const speedGraphSketch = function (p) {
p.setData = function (radarData, duration) { p.setData = function (radarData, duration) {
if (!radarData || !radarData.radarFrames) return; if (!radarData || !radarData.radarFrames) return;
// Clear the old buffer to prevent showing stale graphs, especially if new data has no duration.
staticBuffer.clear();
p.background(document.documentElement.classList.contains("dark") ? [55, 65, 81] : 255);
videoDuration = duration; videoDuration = duration;
let speeds = []; let speeds = [];
@ -172,18 +187,18 @@ export const speedGraphSketch = function (p) {
if (maxSpeed <= 0) maxSpeed = 10; if (maxSpeed <= 0) maxSpeed = 10;
if (minSpeed >= 0) minSpeed = 0; if (minSpeed >= 0) minSpeed = 0;
if (videoDuration > 0) {
if (videoDuration >= 0) {
p.drawStaticGraphToBuffer(radarData); p.drawStaticGraphToBuffer(radarData);
} }
}; };
p.draw = function () { p.draw = function () {
if (!videoDuration || videoDuration <= 0) {
if (!staticBuffer || !videoDuration || videoDuration <= 0) {
const isDark = document.documentElement.classList.contains("dark"); const isDark = document.documentElement.classList.contains("dark");
p.background(isDark ? [55, 65, 81] : 255); p.background(isDark ? [55, 65, 81] : 255);
p.fill(isDark ? 200 : 100); p.fill(isDark ? 200 : 100);
p.textAlign(p.CENTER, p.CENTER); p.textAlign(p.CENTER, p.CENTER);
p.text("Waiting for video duration...", p.width / 2, p.height / 2);
p.text("No data to display", p.width / 2, p.height / 2);
return; return;
} }
p.image(staticBuffer, 0, 0); p.image(staticBuffer, 0, 0);
@ -203,7 +218,7 @@ export const speedGraphSketch = function (p) {
const frameData = appState.vizData.radarFrames[appState.currentFrame]; const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return; if (!frameData) return;
const currentTimeSec = frameData.timestampMs / 1000.0;
const currentTimeSec = frameData.timestamp / 1000.0;
const x = p.map(currentTimeSec, 0, videoDuration, pad.left, p.width - pad.right); const x = p.map(currentTimeSec, 0, videoDuration, pad.left, p.width - pad.right);
p.stroke(255, 0, 0, 150); p.stroke(255, 0, 0, 150);

Loading…
Cancel
Save