From d830c9a386642233c18cf978eb603c0efca1917d Mon Sep 17 00:00:00 2001 From: eehkay Date: Sat, 18 Jul 2026 12:00:09 -0700 Subject: [PATCH 1/2] fix: play r2 direct uploads in the compare versions view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compare page predates the r2 upload provider: r2 versions fell through to a URL-safety check that throws on app-relative upload URLs, so their panels rendered nothing and registered no player — the shared controls drove an empty list and nothing played. - add an R2Panel mapping a plain video element over the app upload route to the shared adapter, using the same playback-url resolution as the main video page - make play/pause state detection work without the YouTube API loaded (numeric fallback), so bunny/r2-only comparisons can pause - re-sync panels that drift more than 350ms from the source player once per second so playback stays aligned, not just starts aligned Verified against the running app: both versions play in lockstep (0.000s measured drift), pause together, and seek together. Co-Authored-By: Claude Fable 5 --- .../compare/compare-versions-page-client.tsx | 201 +++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/compare-versions-page-client.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/compare-versions-page-client.tsx index 201685b..1edec62 100644 --- a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/compare-versions-page-client.tsx +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/compare-versions-page-client.tsx @@ -100,6 +100,21 @@ const isSafeUrl = (url: string) => { } }; +// Mirrors the playback-url resolution the main video page uses for direct +// R2 uploads: media streams through the app's upload route. +function resolveR2PlaybackUrl(version: Version): string { + if (version.originalUrl.startsWith('/api/upload/video/')) { + return version.originalUrl; + } + if (version.originalUrl.startsWith('videos/')) { + return `/api/upload/video/${version.originalUrl.slice('videos/'.length)}`; + } + if (version.videoId.startsWith('videos/')) { + return `/api/upload/video/${version.videoId.slice('videos/'.length)}`; + } + return version.originalUrl; +} + export default function CompareVersionsPageClient({ projectId, videoId, @@ -142,6 +157,7 @@ export default function CompareVersionsPageClient({ const currentTimeRef = useRef(0); const durationRef = useRef(0); const lastCommitRef = useRef(0); + const lastSyncRef = useRef(0); // Direct DOM refs for progress bar / playhead / timecode — updated in the RAF loop const progressBarRef = useRef(null); @@ -241,7 +257,11 @@ export default function CompareVersionsPageClient({ try { const t = sourcePlayer.getCurrentTime(); const d = sourcePlayer.getDuration(); - const playing = sourcePlayer.getPlayerState() === window.YT?.PlayerState?.PLAYING; + // PLAYING is 1 in the YouTube API; the numeric fallback keeps + // state detection working when the YT script never loads + // (bunny/r2-only comparisons, ad blockers). + const playing = + sourcePlayer.getPlayerState() === (window.YT?.PlayerState?.PLAYING ?? 1); // Update refs immediately — zero React overhead if (t !== undefined) currentTimeRef.current = t; @@ -257,6 +277,22 @@ export default function CompareVersionsPageClient({ } } + // Re-sync followers that drift from the source player — providers + // buffer at different speeds and drift past ~350ms reads as + // out-of-sync playback. + if (playing && t !== undefined && timestamp - lastSyncRef.current >= 1000) { + lastSyncRef.current = timestamp; + for (let i = 1; i < players.length; i += 1) { + try { + if (Math.abs(players[i].getCurrentTime() - t) > 0.35) { + players[i].seekTo(t, true); + } + } catch { + // Player not ready + } + } + } + // Throttle React state commits to ~4 updates/sec if (timestamp - lastCommitRef.current >= 250) { lastCommitRef.current = timestamp; @@ -296,7 +332,7 @@ export default function CompareVersionsPageClient({ try { const firstPlayer = players[0]; const state = firstPlayer.getPlayerState(); - const playing = state === window.YT?.PlayerState?.PLAYING; + const playing = state === (window.YT?.PlayerState?.PLAYING ?? 1); if (playing) { players.forEach((p) => { @@ -728,6 +764,13 @@ export default function CompareVersionsPageClient({ onRegister={registerPlayer} onUnregister={unregisterPlayer} /> + ) : version.providerId === 'r2' ? ( + ) : isSafeUrl(version.originalUrl) ? (