Browse Source
Added main.js and cleaned up the index.html for better readablility. Now main.js becomes the main orchestrator linking and wiring everything in the javascript files.
refactor/modularize
Added main.js and cleaned up the index.html for better readablility. Now main.js becomes the main orchestrator linking and wiring everything in the javascript files.
refactor/modularize
2 changed files with 553 additions and 360 deletions
-
361steps/index.html
-
552steps/src/main.js
@ -0,0 +1,552 @@ |
|||||
|
// ===========================================================================================================
|
||||
|
// REFACTOR PLAN: This monolithic script will be broken down into
|
||||
|
// the following modules in the '/src' directory:
|
||||
|
//
|
||||
|
// - constants.js: Shared constants (VIDEO_FPS, RADAR_X_MAX)
|
||||
|
// - utils.js: Pure helper functions (findRadarFrameIndexForTime)
|
||||
|
// - state.js: Central application state management
|
||||
|
// - dom.js: DOM element references and UI updaters
|
||||
|
// - modal.js: Modal dialog logic
|
||||
|
// - theme.js: Dark/Light mode theme switcher
|
||||
|
// - db.js: IndexedDB caching logic
|
||||
|
// - fileParsers.js: JSON and CAN log parsing logic
|
||||
|
// - p5/radarSketch.js: The main p5.js radar visualization
|
||||
|
// - p5/speedGraph.js: The p5.js speed graph visualization
|
||||
|
// - sync.js: Playback and synchronization loop
|
||||
|
// - main.js: The main application entry point that wires everything
|
||||
|
// ===========================================================================================================
|
||||
|
|
||||
|
// import animation loop from './src/sync.js';
|
||||
|
|
||||
|
import { animationLoop } from "./sync.js"; |
||||
|
// import radar sketch from './src/p5/radarSketch.js';
|
||||
|
import { radarSketch } from "./p5/radarSketch.js"; |
||||
|
// import speed graph sketch from './src/p5/speedGraphSketch.js';
|
||||
|
import { speedGraphSketch } from "./p5/speedGraphSketch.js"; |
||||
|
// import JSON parser, can log procesor from './src/fileParsers.js';
|
||||
|
import { processCanLog, parseVisualizationJson } from "./fileParsers.js"; |
||||
|
// import constants from './constants.js';
|
||||
|
import { |
||||
|
MAX_TRAJECTORY_LENGTH, |
||||
|
VIDEO_FPS, |
||||
|
RADAR_X_MIN, |
||||
|
RADAR_X_MAX, |
||||
|
RADAR_Y_MIN, |
||||
|
RADAR_Y_MAX, |
||||
|
} from "./constants.js"; |
||||
|
// import utils and helpers from './src/utils.js';
|
||||
|
import { |
||||
|
findRadarFrameIndexForTime, |
||||
|
findLastCanIndexBefore, |
||||
|
extractTimestampInfo, |
||||
|
parseTimestamp, |
||||
|
} from "./utils.js"; |
||||
|
// import state machine from './src/state.js';
|
||||
|
import { appState } from "./state.js"; |
||||
|
// import DOM elements and UI updaters from './src/dom.js';
|
||||
|
import { |
||||
|
//---DOM Elements---//
|
||||
|
canvasContainer, |
||||
|
canvasPlaceholder, |
||||
|
videoPlayer, |
||||
|
videoPlaceholder, |
||||
|
loadJsonBtn, |
||||
|
loadVideoBtn, |
||||
|
loadCanBtn, |
||||
|
jsonFileInput, |
||||
|
videoFileInput, |
||||
|
canFileInput, |
||||
|
playPauseBtn, |
||||
|
stopBtn, |
||||
|
timelineSlider, |
||||
|
frameCounter, |
||||
|
offsetInput, |
||||
|
speedSlider, |
||||
|
speedDisplay, |
||||
|
featureToggles, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleTracks, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleDebugOverlay, |
||||
|
egoSpeedDisplay, |
||||
|
canSpeedDisplay, |
||||
|
debugOverlay, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
applySnrBtn, |
||||
|
autoOffsetIndicator, |
||||
|
clearCacheBtn, |
||||
|
speedGraphContainer, |
||||
|
speedGraphPlaceholder, |
||||
|
toggleCloseUp, |
||||
|
//---UI Updaters---//
|
||||
|
updateFrame, |
||||
|
resetVisualization, |
||||
|
updateCanDisplay, |
||||
|
updateDebugOverlay, |
||||
|
} from "./dom.js"; |
||||
|
// import modal dialog logic from './src/modal.js';
|
||||
|
import { showModal } from "./modal.js"; |
||||
|
// import initialize theme from './src/theme.js';
|
||||
|
import { initializeTheme } from "./theme.js"; |
||||
|
// import caching logic from './src/db.js';
|
||||
|
import { initDB, saveFileToDB, loadFileFromDB } from "./db.js"; |
||||
|
|
||||
|
function setupVideoPlayer(fileURL) { |
||||
|
videoPlayer.src = fileURL; |
||||
|
videoPlayer.classList.remove("hidden"); |
||||
|
videoPlaceholder.classList.add("hidden"); |
||||
|
videoPlayer.playbackRate = parseFloat(speedSlider.value); |
||||
|
} |
||||
|
loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); |
||||
|
loadVideoBtn.addEventListener("click", () => videoFileInput.click()); |
||||
|
loadCanBtn.addEventListener("click", () => canFileInput.click()); |
||||
|
clearCacheBtn.addEventListener("click", async () => { |
||||
|
const confirmed = await showModal("Clear all cached data and reload?", true); |
||||
|
if (confirmed) { |
||||
|
indexedDB.deleteDatabase("visualizerDB"); |
||||
|
localStorage.clear(); |
||||
|
window.location.reload(); |
||||
|
} |
||||
|
}); |
||||
|
jsonFileInput.addEventListener("change", (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (!file) return; |
||||
|
appState.jsonFilename = file.name; |
||||
|
localStorage.setItem("jsonFilename", appState.jsonFilename); |
||||
|
calculateAndSetOffset(); // This function now correctly sets appState variables
|
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const jsonString = e.target.result; |
||||
|
saveFileToDB("json", jsonString); |
||||
|
|
||||
|
// 1. Give the raw ingredients to our new JSON "chef"
|
||||
|
const result = parseVisualizationJson( |
||||
|
jsonString, |
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
|
||||
|
// 2. Check the result
|
||||
|
if (result.error) { |
||||
|
showModal(result.error); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 3. Update the application's central state with the prepared data
|
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
|
||||
|
// 4. Now, the "waiter" updates the UI
|
||||
|
snrMinInput.value = appState.globalMinSnr.toFixed(1); |
||||
|
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); |
||||
|
resetVisualization(); // This UI function is in dom.js
|
||||
|
canvasPlaceholder.style.display = "none"; |
||||
|
featureToggles.classList.remove("hidden"); |
||||
|
|
||||
|
if (!appState.p5_instance) { |
||||
|
appState.p5_instance = new p5(radarSketch); |
||||
|
} |
||||
|
|
||||
|
if (appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} else { |
||||
|
// Redraw p5 instance with new data
|
||||
|
appState.p5_instance.drawSnrLegendToBuffer( |
||||
|
appState.globalMinSnr, |
||||
|
appState.globalMaxSnr |
||||
|
); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
}); |
||||
|
videoFileInput.addEventListener("change", (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (!file) return; |
||||
|
appState.videoFilename = file.name; |
||||
|
localStorage.setItem("videoFilename", appState.videoFilename); |
||||
|
saveFileToDB("video", file); |
||||
|
|
||||
|
// This is the key moment: we now have a video start date.
|
||||
|
calculateAndSetOffset(); |
||||
|
|
||||
|
// Now, check if we have pending data that needs this date.
|
||||
|
if (appState.rawCanLogText) { |
||||
|
const result = processCanLog( |
||||
|
appState.rawCanLogText, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
if (!result.error) { |
||||
|
appState.canData = result.data; |
||||
|
appState.rawCanLogText = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// NEW: Re-process vizData if it was loaded before the video.
|
||||
|
if (appState.vizData) { |
||||
|
console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps."); |
||||
|
appState.vizData.radarFrames.forEach((frame) => { |
||||
|
frame.timestampMs = |
||||
|
appState.radarStartTimeMs + |
||||
|
frame.timestamp - |
||||
|
appState.videoStartDate.getTime(); |
||||
|
}); |
||||
|
resetVisualization(); // Reset UI to reflect new timestamps
|
||||
|
} |
||||
|
|
||||
|
const fileURL = URL.createObjectURL(file); |
||||
|
setupVideoPlayer(fileURL); |
||||
|
|
||||
|
// When the video is ready, update the speed graph
|
||||
|
videoPlayer.onloadedmetadata = () => { |
||||
|
if (appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
canFileInput.addEventListener("change", (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (!file) return; |
||||
|
appState.canLogFilename = file.name; |
||||
|
localStorage.setItem("canLogFilename", appState.canLogFilename); |
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const logContent = e.target.result; |
||||
|
saveFileToDB("canLogText", logContent); |
||||
|
|
||||
|
// 1. Give the raw ingredients to the chef (our parser)
|
||||
|
const result = processCanLog(logContent, appState.videoStartDate); |
||||
|
|
||||
|
// 2. Check what the chef gave back
|
||||
|
if (result.error) { |
||||
|
// If there was an error, show it and save the raw text for later.
|
||||
|
showModal(result.error); |
||||
|
appState.rawCanLogText = result.rawCanLogText; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 3. If successful, update the application's central state
|
||||
|
appState.canData = result.data; |
||||
|
appState.rawCanLogText = null; |
||||
|
|
||||
|
// 4. Now, the waiter updates the UI based on the new state
|
||||
|
if (appState.canData.length > 0 || appState.vizData) { |
||||
|
speedGraphPlaceholder.classList.add("hidden"); |
||||
|
if (!appState.speedGraphInstance) { |
||||
|
// We need to pass the speedGraphSketch function definition here
|
||||
|
appState.speedGraphInstance = new p5(speedGraphSketch); |
||||
|
} |
||||
|
if (videoPlayer.duration) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
} else { |
||||
|
showModal(`No CAN messages with ID 0x30F found.`); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
}); |
||||
|
offsetInput.addEventListener("input", () => { |
||||
|
autoOffsetIndicator.classList.add("hidden"); |
||||
|
localStorage.setItem("visualizerOffset", offsetInput.value); |
||||
|
}); |
||||
|
applySnrBtn.addEventListener("click", () => { |
||||
|
const newMin = parseFloat(snrMinInput.value), |
||||
|
newMax = parseFloat(snrMaxInput.value); |
||||
|
if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) { |
||||
|
showModal("Invalid SNR range."); |
||||
|
return; |
||||
|
} |
||||
|
appState.globalMinSnr = newMin; |
||||
|
appState.globalMaxSnr = newMax; |
||||
|
toggleFrameNorm.checked = false; |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.drawSnrLegendToBuffer( |
||||
|
appState.globalMinSnr, |
||||
|
appState.globalMaxSnr |
||||
|
); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
}); |
||||
|
playPauseBtn.addEventListener("click", () => { |
||||
|
if (!appState.vizData && !videoPlayer.src) return; |
||||
|
appState.isPlaying = !appState.isPlaying; |
||||
|
playPauseBtn.textContent = appState.isPlaying ? "Pause" : "Play"; |
||||
|
if (appState.isPlaying) { |
||||
|
if (videoPlayer.src && videoPlayer.readyState > 1) { |
||||
|
appState.masterClockStart = performance.now(); |
||||
|
appState.mediaTimeStart = videoPlayer.currentTime; |
||||
|
appState.lastSyncTime = appState.masterClockStart; |
||||
|
videoPlayer.play(); |
||||
|
} |
||||
|
requestAnimationFrame(animationLoop); |
||||
|
} else { |
||||
|
if (videoPlayer.src) videoPlayer.pause(); |
||||
|
} |
||||
|
}); |
||||
|
stopBtn.addEventListener("click", () => { |
||||
|
videoPlayer.pause(); |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
if (appState.vizData) { |
||||
|
updateFrame(0, true); |
||||
|
} else if (videoPlayer.src) { |
||||
|
videoPlayer.currentTime = 0; |
||||
|
} |
||||
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
||||
|
}); |
||||
|
timelineSlider.addEventListener("input", (event) => { |
||||
|
if (!appState.vizData) return; |
||||
|
if (appState.isPlaying) { |
||||
|
videoPlayer.pause(); |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
} |
||||
|
const frame = parseInt(event.target.value, 10); |
||||
|
updateFrame(frame, true); |
||||
|
appState.mediaTimeStart = videoPlayer.currentTime; |
||||
|
appState.masterClockStart = performance.now(); |
||||
|
}); |
||||
|
speedSlider.addEventListener("input", (event) => { |
||||
|
const speed = parseFloat(event.target.value); |
||||
|
videoPlayer.playbackRate = speed; |
||||
|
speedDisplay.textContent = `${speed.toFixed(1)}x`; |
||||
|
}); |
||||
|
|
||||
|
// ADD THE NEW TOGGLE TO THE ARRAY
|
||||
|
const colorToggles = [ |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
]; |
||||
|
colorToggles.forEach((t) => { |
||||
|
t.addEventListener("change", (e) => { |
||||
|
if (e.target.checked) { |
||||
|
colorToggles.forEach((o) => { |
||||
|
if (o !== e.target) o.checked = false; |
||||
|
}); |
||||
|
} |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
[ |
||||
|
toggleVelocity, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleTracks, |
||||
|
toggleDebugOverlay, |
||||
|
].forEach((t) => { |
||||
|
t.addEventListener("change", () => { |
||||
|
if (appState.p5_instance) { |
||||
|
if (t === toggleFrameNorm && !toggleFrameNorm.checked) |
||||
|
appState.p5_instance.drawSnrLegendToBuffer( |
||||
|
appState.globalMinSnr, |
||||
|
appState.globalMaxSnr |
||||
|
); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
if (t === toggleDebugOverlay) updateDebugOverlay(videoPlayer.currentTime); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
toggleCloseUp.addEventListener("change", () => { |
||||
|
appState.isCloseUpMode = toggleCloseUp.checked; |
||||
|
if (appState.p5_instance) { |
||||
|
if (appState.isCloseUpMode) { |
||||
|
if (appState.isPlaying) { |
||||
|
playPauseBtn.click(); |
||||
|
} |
||||
|
appState.p5_instance.loop(); |
||||
|
} else { |
||||
|
appState.p5_instance.noLoop(); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
videoPlayer.addEventListener("ended", () => { |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
}); |
||||
|
document.addEventListener("keydown", (event) => { |
||||
|
if ( |
||||
|
!appState.vizData || |
||||
|
["ArrowRight", "ArrowLeft"].indexOf(event.key) === -1 |
||||
|
) |
||||
|
return; |
||||
|
event.preventDefault(); |
||||
|
if (appState.isPlaying) { |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
videoPlayer.pause(); |
||||
|
} |
||||
|
let newFrame = appState.currentFrame; |
||||
|
if (event.key === "ArrowRight") |
||||
|
newFrame = Math.min( |
||||
|
appState.vizData.radarFrames.length - 1, |
||||
|
appState.currentFrame + 1 |
||||
|
); |
||||
|
else if (event.key === "ArrowLeft") |
||||
|
newFrame = Math.max(0, appState.currentFrame - 1); |
||||
|
if (newFrame !== appState.currentFrame) { |
||||
|
updateFrame(newFrame, true); |
||||
|
appState.mediaTimeStart = videoPlayer.currentTime; |
||||
|
appState.masterClockStart = performance.now(); |
||||
|
} |
||||
|
}); |
||||
|
function calculateAndSetOffset() { |
||||
|
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); |
||||
|
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); |
||||
|
if (videoTimestampInfo) { |
||||
|
appState.videoStartDate = parseTimestamp( |
||||
|
videoTimestampInfo.timestampStr, |
||||
|
videoTimestampInfo.format |
||||
|
); |
||||
|
if (appState.videoStartDate) |
||||
|
console.log( |
||||
|
`Video start date set to: ${appState.videoStartDate.toISOString()}` |
||||
|
); |
||||
|
} |
||||
|
if (jsonTimestampInfo) { |
||||
|
const jsonDate = parseTimestamp( |
||||
|
jsonTimestampInfo.timestampStr, |
||||
|
jsonTimestampInfo.format |
||||
|
); |
||||
|
if (jsonDate) { |
||||
|
appState.radarStartTimeMs = jsonDate.getTime(); |
||||
|
console.log(`Radar start date set to: ${jsonDate.toISOString()}`); |
||||
|
if (appState.videoStartDate) { |
||||
|
const offset = |
||||
|
appState.radarStartTimeMs - appState.videoStartDate.getTime(); |
||||
|
offsetInput.value = offset; |
||||
|
localStorage.setItem("visualizerOffset", offset); |
||||
|
autoOffsetIndicator.classList.remove("hidden"); |
||||
|
console.log(`Auto-calculated offset: ${offset} ms`); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Application Initialization ---
|
||||
|
document.addEventListener("DOMContentLoaded", () => { |
||||
|
initializeTheme(); |
||||
|
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); |
||||
|
|
||||
|
initDB(() => { |
||||
|
console.log("DEBUG: Database initialized."); |
||||
|
const savedOffset = localStorage.getItem("visualizerOffset"); |
||||
|
if (savedOffset !== null) { |
||||
|
offsetInput.value = savedOffset; |
||||
|
} |
||||
|
appState.videoFilename = localStorage.getItem("videoFilename"); |
||||
|
appState.jsonFilename = localStorage.getItem("jsonFilename"); |
||||
|
appState.canLogFilename = localStorage.getItem("canLogFilename"); |
||||
|
|
||||
|
// This is important: it sets videoStartDate if a video filename is cached
|
||||
|
calculateAndSetOffset(); |
||||
|
|
||||
|
const videoPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("video", resolve) |
||||
|
); |
||||
|
const jsonPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("json", resolve) |
||||
|
); |
||||
|
const canLogPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("canLogText", resolve) |
||||
|
); |
||||
|
|
||||
|
Promise.all([videoPromise, jsonPromise, canLogPromise]) |
||||
|
.then(([videoBlob, jsonString, canLogText]) => { |
||||
|
console.log("DEBUG: All data fetched from IndexedDB."); |
||||
|
|
||||
|
const processAllData = () => { |
||||
|
console.log("DEBUG: Processing all loaded data."); |
||||
|
|
||||
|
// 1. Process JSON (only if we have a video date)
|
||||
|
if (jsonString && appState.videoStartDate) { |
||||
|
const result = parseVisualizationJson( |
||||
|
jsonString, |
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
if (!result.error) { |
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
snrMinInput.value = appState.globalMinSnr.toFixed(1); |
||||
|
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); |
||||
|
} else { |
||||
|
showModal(result.error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Process CAN log (only if we have a video date)
|
||||
|
if (canLogText && appState.videoStartDate) { |
||||
|
const result = processCanLog(canLogText, appState.videoStartDate); |
||||
|
if (!result.error) { |
||||
|
appState.canData = result.data; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 3. Update all UI elements now that data is processed
|
||||
|
if (appState.vizData) { |
||||
|
resetVisualization(); |
||||
|
canvasPlaceholder.style.display = "none"; |
||||
|
featureToggles.classList.remove("hidden"); |
||||
|
if (!appState.p5_instance) { |
||||
|
appState.p5_instance = new p5(radarSketch); |
||||
|
} |
||||
|
} |
||||
|
if (appState.canData.length > 0 || appState.vizData) { |
||||
|
speedGraphPlaceholder.classList.add("hidden"); |
||||
|
if (!appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance = new p5(speedGraphSketch); |
||||
|
} |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// This is the main controller
|
||||
|
// --- THIS IS THE CORRECTED CODE ---
|
||||
|
if (videoBlob) { |
||||
|
const fileURL = URL.createObjectURL(videoBlob); |
||||
|
setupVideoPlayer(fileURL); |
||||
|
// This ensures we ONLY process data once the video's duration is known.
|
||||
|
videoPlayer.onloadedmetadata = processAllData; |
||||
|
} else { |
||||
|
// If there's no video, we can go ahead and process the other data.
|
||||
|
processAllData(); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error("DEBUG: Error during Promise.all data loading:", error); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue