From 66be634b5371732911030c83e326aea319b483cd Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Mon, 24 Nov 2025 16:54:20 +0800 Subject: [PATCH] feat: Improve video playback state syncing for AirPlay and add `unoptimized` and `referrerPolicy` to image components. --- components/home/MovieCard.tsx | 2 ++ components/player/DesktopVideoPlayer.tsx | 1 + .../hooks/desktop/usePlaybackControls.ts | 3 +- .../hooks/mobile/useMobilePlaybackControls.ts | 3 +- components/player/hooks/usePlaybackPolling.ts | 31 +++++++++++++------ components/search/VideoCard.tsx | 2 ++ 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/components/home/MovieCard.tsx b/components/home/MovieCard.tsx index aac134f..ff8a13f 100644 --- a/components/home/MovieCard.tsx +++ b/components/home/MovieCard.tsx @@ -45,6 +45,8 @@ export const MovieCard = memo(function MovieCard({ movie, onMovieClick }: MovieC className="object-cover transition-transform duration-300 group-hover:scale-105 rounded-[var(--radius-2xl)]" sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw" loading="lazy" + unoptimized + referrerPolicy="no-referrer" onError={(e) => { const target = e.currentTarget as HTMLImageElement; target.style.display = 'none'; diff --git a/components/player/DesktopVideoPlayer.tsx b/components/player/DesktopVideoPlayer.tsx index 12211a1..accdc5d 100644 --- a/components/player/DesktopVideoPlayer.tsx +++ b/components/player/DesktopVideoPlayer.tsx @@ -73,6 +73,7 @@ export function DesktopVideoPlayer({ className="w-full h-full object-contain" src={src} poster={poster} + x-webkit-airplay="allow" onPlay={handlePlay} onPause={handlePause} onTimeUpdate={handleTimeUpdateEvent} diff --git a/components/player/hooks/desktop/usePlaybackControls.ts b/components/player/hooks/desktop/usePlaybackControls.ts index 9e90777..9ecd9cf 100644 --- a/components/player/hooks/desktop/usePlaybackControls.ts +++ b/components/player/hooks/desktop/usePlaybackControls.ts @@ -123,7 +123,8 @@ export function usePlaybackControls({ videoRef, isDraggingProgressRef, setCurrentTime, - setDuration + setDuration, + setIsPlaying }); return { diff --git a/components/player/hooks/mobile/useMobilePlaybackControls.ts b/components/player/hooks/mobile/useMobilePlaybackControls.ts index 9682f89..fd40a74 100644 --- a/components/player/hooks/mobile/useMobilePlaybackControls.ts +++ b/components/player/hooks/mobile/useMobilePlaybackControls.ts @@ -135,7 +135,8 @@ export function useMobilePlaybackControls({ videoRef, isDraggingProgressRef, setCurrentTime, - setDuration + setDuration, + setIsPlaying }); return { diff --git a/components/player/hooks/usePlaybackPolling.ts b/components/player/hooks/usePlaybackPolling.ts index b40fffd..d3143a6 100644 --- a/components/player/hooks/usePlaybackPolling.ts +++ b/components/player/hooks/usePlaybackPolling.ts @@ -6,6 +6,7 @@ interface UsePlaybackPollingProps { isDraggingProgressRef: React.MutableRefObject; setCurrentTime: (time: number) => void; setDuration: (duration: number) => void; + setIsPlaying: (playing: boolean) => void; } /** @@ -17,24 +18,34 @@ export function usePlaybackPolling({ videoRef, isDraggingProgressRef, setCurrentTime, - setDuration + setDuration, + setIsPlaying }: UsePlaybackPollingProps) { useEffect(() => { - if (!isPlaying || !videoRef.current) return; + if (!videoRef.current) return; const interval = setInterval(() => { - if (videoRef.current && !isDraggingProgressRef.current) { - const current = videoRef.current.currentTime; - const total = videoRef.current.duration; + if (videoRef.current) { + // Sync play/pause state (crucial for AirPlay where events might be missed) + const isVideoPaused = videoRef.current.paused; + if (isVideoPaused === isPlaying && !isDraggingProgressRef.current) { + // State mismatch detected (e.g. AirPlay paused but app thinks playing) + setIsPlaying(!isVideoPaused); + } - // Only update if significantly different to avoid jitter - setCurrentTime(current); - if (!isNaN(total) && total > 0) { - setDuration(total); + if (!isDraggingProgressRef.current && !isVideoPaused) { + const current = videoRef.current.currentTime; + const total = videoRef.current.duration; + + // Only update if significantly different to avoid jitter + setCurrentTime(current); + if (!isNaN(total) && total > 0) { + setDuration(total); + } } } }, 500); // Poll every 500ms return () => clearInterval(interval); - }, [isPlaying, videoRef, isDraggingProgressRef, setCurrentTime, setDuration]); + }, [isPlaying, videoRef, isDraggingProgressRef, setCurrentTime, setDuration, setIsPlaying]); } diff --git a/components/search/VideoCard.tsx b/components/search/VideoCard.tsx index ebf5f71..bc53fad 100644 --- a/components/search/VideoCard.tsx +++ b/components/search/VideoCard.tsx @@ -60,6 +60,8 @@ export const VideoCard = memo(({ className="object-cover rounded-[var(--radius-2xl)]" sizes="(max-width: 640px) 33vw, (max-width: 1024px) 20vw, 16vw" loading="lazy" + unoptimized + referrerPolicy="no-referrer" onError={(e) => { const target = e.currentTarget as HTMLImageElement; target.style.opacity = '0';