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..682b0b9 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 @@ -29,6 +29,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; +import { isPlayableVideoUrl, resolveR2PlaybackUrl } from '@/lib/video-upload-validation'; import { cn } from '@/lib/utils'; interface Version { @@ -91,6 +92,11 @@ function formatTime(seconds: number): string { return `${mins}:${secs.toString().padStart(2, '0')}`; } +// Panels drifting past this from the source player read as out-of-sync playback. +const MAX_PANEL_DRIFT_SECONDS = 0.35; +// Minimum gap between two corrective seeks of the same panel. +const RESYNC_COOLDOWN_MS = 4000; + const isSafeUrl = (url: string) => { try { const parsed = new URL(url); @@ -142,6 +148,8 @@ export default function CompareVersionsPageClient({ const currentTimeRef = useRef(0); const durationRef = useRef(0); const lastCommitRef = useRef(0); + const lastSyncRef = useRef(0); + const resyncCooldownRef = useRef(new WeakMap()); // Direct DOM refs for progress bar / playhead / timecode — updated in the RAF loop const progressBarRef = useRef(null); @@ -241,7 +249,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 +269,28 @@ 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. The per-player cooldown keeps a follower + // that simply cannot keep up (slow network, HLS rebuffering) from + // being seeked every second, which would stutter rather than correct. + if (playing && t !== undefined && timestamp - lastSyncRef.current >= 1000) { + lastSyncRef.current = timestamp; + const cooldowns = resyncCooldownRef.current; + for (let i = 1; i < players.length; i += 1) { + const follower = players[i]; + if (timestamp - (cooldowns.get(follower) ?? 0) < RESYNC_COOLDOWN_MS) continue; + try { + if (Math.abs(follower.getCurrentTime() - t) > MAX_PANEL_DRIFT_SECONDS) { + cooldowns.set(follower, timestamp); + follower.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 +330,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 +762,13 @@ export default function CompareVersionsPageClient({ onRegister={registerPlayer} onUnregister={unregisterPlayer} /> + ) : version.providerId === 'r2' ? ( + ) : isSafeUrl(version.originalUrl) ? (