From f6d7442c050a16c1cc93e87998647e31ef3ee65d Mon Sep 17 00:00:00 2001 From: dirwin31 <83434411+dirwin31@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:32:06 -0700 Subject: [PATCH] Make Low to High Smoother --- .../components/recordings/dashcam_routes.css | 16 ++++++ .../components/recordings/dashcam_routes.js | 52 +++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css index 5f0c27a36..750df7112 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css @@ -1223,6 +1223,21 @@ width: 100%; } +.dashcam-transition-frame { + background: #07080b; + height: 100%; + inset: 0; + object-fit: contain; + pointer-events: none; + position: absolute; + width: 100%; + z-index: 1; +} + +.dashcam-transition-frame[hidden] { + display: none; +} + .dashcam-player-state { align-items: center; background: rgba(7, 8, 11, 0.78); @@ -1232,6 +1247,7 @@ justify-content: center; position: absolute; text-align: center; + z-index: 2; } .dashcam-player-state.error { diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js index 7ef1048f1..353c4df43 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js @@ -301,6 +301,7 @@ async function openOverlay(route) {
+
Loading route metadata…
@@ -324,6 +325,7 @@ async function openOverlay(route) { document.body.appendChild(overlay) const video = overlay.querySelector("video") + const transitionFrame = overlay.querySelector(".dashcam-transition-frame") const playerState = overlay.querySelector(".dashcam-player-state") const segmentBar = overlay.querySelector(".dashcam-segment-bar") const segmentSelect = overlay.querySelector(".segment-select") @@ -341,6 +343,7 @@ async function openOverlay(route) { let qualityToken = 0 let upgradeController = null let upgradeTimer = null + let transitionActive = false const setPlayerMessage = (message, isError = false) => { playerState.textContent = message @@ -359,9 +362,35 @@ async function openOverlay(route) { .join("") segmentBar.hidden = !segments.length } - const swapSource = (url, { message } = {}) => { + + const finishTransition = () => { + transitionActive = false + transitionFrame.hidden = true + } + const holdCurrentFrame = () => { + if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA || !video.videoWidth || !video.videoHeight) return false + const context = transitionFrame.getContext("2d") + if (!context) return false + transitionFrame.width = video.videoWidth + transitionFrame.height = video.videoHeight + try { + context.drawImage(video, 0, 0, transitionFrame.width, transitionFrame.height) + } catch (_) { + return false + } + transitionActive = true + transitionFrame.hidden = false + setPlayerMessage("") + return true + } + const swapSource = (url, { message, seamless = false } = {}) => { const playbackTime = Number.isFinite(video.currentTime) ? video.currentTime : 0 const shouldResume = !video.paused && !video.ended + const heldFrame = seamless && holdCurrentFrame() + // Never trade a working preview for a black frame. The prepared full stream can + // be attempted again later, while the low-resolution video keeps playing. + if (seamless && !heldFrame) return false + if (heldFrame) video.addEventListener("canplay", finishTransition, { once: true }) video.addEventListener("loadedmetadata", () => { if (playbackTime > 0) { try { @@ -370,9 +399,11 @@ async function openOverlay(route) { } if (shouldResume) video.play().catch(() => {}) }, { once: true }) + if (!heldFrame) finishTransition() if (message) setPlayerMessage(message) video.src = url video.load() + return true } const cancelUpgrade = () => { @@ -393,14 +424,23 @@ async function openOverlay(route) { const fullUrl = cameraVideoUrl(segmentUrl, camera) const stillCurrent = () => token === qualityToken && showingPreview && segments[current] === segmentUrl && selectedCamera === camera && !!overlay + const swapPreparedStream = (attempt = 0) => { + if (!stillCurrent()) return + if (swapSource(fullUrl, { seamless: true })) { + showingPreview = false + return + } + if (attempt < 5) { + upgradeTimer = setTimeout(() => swapPreparedStream(attempt + 1), 100) + } + } fetch(fullUrl, { method: "HEAD", signal: controller.signal }) .then(response => { if (!stillCurrent()) return if (upgradeController === controller) upgradeController = null if (response.ok) { - showingPreview = false - swapSource(fullUrl) + swapPreparedStream() return } if (response.status === 503 && attempt < FULL_QUALITY_RETRIES) { @@ -428,6 +468,7 @@ async function openOverlay(route) { const camera = selectedCamera if (!segmentUrl || !camera) return cancelUpgrade() + finishTransition() wantsPlayback = autoplay showingPreview = preview === undefined ? supportsLowQuality(camera) : preview setPlayerMessage(message || "Loading video…") @@ -498,8 +539,11 @@ async function openOverlay(route) { }) video.addEventListener("loadeddata", () => setPlayerMessage("")) video.addEventListener("playing", () => setPlayerMessage("")) - video.addEventListener("waiting", () => setPlayerMessage("Loading video…")) + video.addEventListener("waiting", () => { + if (!transitionActive) setPlayerMessage("Loading video…") + }) video.addEventListener("error", () => { + finishTransition() // A dead preview drops through to the real stream rather than showing an error. if (showingPreview) { showingPreview = false