@@ -55,6 +55,7 @@ import { usePlayerSheetGestures } from "@/hooks/usePlayerSheetGestures";
5555import { usePlayerTapGestures } from "@/hooks/usePlayerTapGestures" ;
5656import { useStretchPlayer } from "@/hooks/useStretchPlayer" ;
5757import { useSyncedVideo } from "@/hooks/useSyncedVideo" ;
58+ import { useSyncedYouTubeEmbed } from "@/hooks/useSyncedYouTubeEmbed" ;
5859import { HistoryItem , LyricsSettings , Media } from "@/interfaces" ;
5960import { youtubeErrorTitle } from "@/lib/apiError" ;
6061import { MAX_HISTORY_ITEMS } from "@/lib/constants" ;
@@ -68,8 +69,9 @@ import {
6869} from "@/lib/playerPrefs" ;
6970import { mergeTrackMetadata , peekSearchMeta } from "@/lib/searchMeta" ;
7071import { appTheme } from "@/lib/theme" ;
72+ import { isMarkedAudioTrackVideo } from "@/lib/trackFlags" ;
7173import { getModeFromRate , getVideoState , saveVideoState } from "@/lib/videoState" ;
72- import { getFormattedTime , getYouTubeId , isSupportedURL } from "@/utils" ;
74+ import { getFormattedTime , getYouTubeId , isSupportedURL , isYoutubeURL } from "@/utils" ;
7375import {
7476 createDynamicTheme ,
7577 getOriginalPlatformUrl ,
@@ -765,34 +767,69 @@ export function Player({
765767 savePlaybackPrefs ( { advancedStretch : enabled } ) ;
766768 } , [ ] ) ;
767769
770+ const youtubeVideoId = ( ( ) => {
771+ if ( ! media ?. sourceUrl || ! isYoutubeURL ( media . sourceUrl ) ) return undefined ;
772+ if ( media . isAudioTrackVideo ) return undefined ;
773+ const id = getYouTubeId ( media . sourceUrl ) ;
774+ if ( ! id || isMarkedAudioTrackVideo ( id ) ) return undefined ;
775+ return id ;
776+ } ) ( ) ;
777+
778+ /** Local file video (blob / same-origin) — not a proxied YouTube stream. */
779+ const localVideoUrl =
780+ media ?. videoUrl && ! media . videoUrl . startsWith ( "/api/stream/" )
781+ ? media . videoUrl
782+ : undefined ;
783+
768784 const handleToggleShowVideo = useCallback ( ( ) => {
769- if ( ! media ?. videoUrl || media . isAudioTrackVideo ) {
785+ const canShow = Boolean ( youtubeVideoId || localVideoUrl ) ;
786+ if ( ! canShow ) {
770787 showToast ( "Video not available for this track" ) ;
771788 return ;
772789 }
773790 const next = ! showVideo ;
774791 setShowVideoState ( next ) ;
775792 setShowVideo ( next ) ;
776- } , [ media , showVideo , showToast ] ) ;
793+ } , [ youtubeVideoId , localVideoUrl , showVideo , showToast ] ) ;
777794
778- /** Real motion video available (not YouTube Music ATV static art ). */
779- const hasVideoStream = Boolean ( media ?. videoUrl && ! media ?. isAudioTrackVideo ) ;
795+ /** YouTube embed or local file video (not ATV / not proxied YT stream ). */
796+ const hasVideoStream = Boolean ( youtubeVideoId || localVideoUrl ) ;
780797 /** User wants video and this track can show it. */
781798 const showingVideo = Boolean ( showVideo && hasVideoStream ) ;
799+ const showingYouTubeEmbed = Boolean ( showingVideo && youtubeVideoId ) ;
800+ const showingLocalVideo = Boolean ( showingVideo && localVideoUrl && ! youtubeVideoId ) ;
782801
783- const { videoRef, isVideoReady, videoEl } = useSyncedVideo ( {
784- src : hasVideoStream ? media ?. videoUrl : undefined ,
785- active : showingVideo ,
802+ const {
803+ videoRef,
804+ isVideoReady : localVideoReady ,
805+ videoEl,
806+ } = useSyncedVideo ( {
807+ src : localVideoUrl ,
808+ active : showingLocalVideo ,
786809 isPlaying,
787810 currentTime,
788811 rate,
789812 } ) ;
813+ const { containerRef : embedContainerRef , isVideoReady : embedReady } =
814+ useSyncedYouTubeEmbed ( {
815+ // Only load the iframe when Show video is on — saves YT CDN bandwidth.
816+ videoId : showingYouTubeEmbed ? youtubeVideoId : undefined ,
817+ active : showingYouTubeEmbed ,
818+ isPlaying,
819+ currentTime,
820+ rate,
821+ } ) ;
822+ const isVideoReady = showingYouTubeEmbed
823+ ? embedReady
824+ : showingLocalVideo
825+ ? localVideoReady
826+ : false ;
790827 const showVideoCover = ! showingVideo || ! isVideoReady ;
791828 const washCanvasRef = useRef < HTMLCanvasElement > ( null ) ;
792829
793- // Paint a low-res moving wash from the on-screen video frames
830+ // Paint a low-res moving wash from local < video> frames (iframe pixels aren't readable).
794831 useEffect ( ( ) => {
795- if ( ! showingVideo || isMini || ! videoEl || ! washCanvasRef . current ) return ;
832+ if ( ! showingLocalVideo || isMini || ! videoEl || ! washCanvasRef . current ) return ;
796833 const canvas = washCanvasRef . current ;
797834 const ctx = canvas . getContext ( "2d" , { alpha : false } ) ;
798835 if ( ! ctx ) return ;
@@ -818,7 +855,7 @@ export function Player({
818855 } ;
819856 raf = requestAnimationFrame ( draw ) ;
820857 return ( ) => cancelAnimationFrame ( raf ) ;
821- } , [ showingVideo , videoEl , isMini ] ) ;
858+ } , [ showingLocalVideo , videoEl , isMini ] ) ;
822859
823860 const handleReset = useCallback ( ( ) => {
824861 setRate ( 1 ) ;
@@ -1482,8 +1519,8 @@ export function Player({
14821519 touchAction : isMobile ? "none" : undefined ,
14831520 } }
14841521 >
1485- { /* Blurred wash — live video frames when showing video, else cover art */ }
1486- { showingVideo && media . videoUrl ? (
1522+ { /* Blurred wash — local video frames when available; YouTube embed uses cover */ }
1523+ { showingLocalVideo ? (
14871524 < >
14881525 < canvas
14891526 ref = { washCanvasRef }
@@ -1737,15 +1774,36 @@ export function Player({
17371774 style = { { display : "none" } }
17381775 preload = "metadata"
17391776 />
1740- { /* Keep video mounted whenever a stream exists — hide instead of unmount. */ }
1741- { hasVideoStream && media . videoUrl ? (
1777+ { /* YouTube: muted embed (no video proxy). Local files: <video>. */ }
1778+ { showingYouTubeEmbed && youtubeVideoId ? (
1779+ < div
1780+ ref = { embedContainerRef }
1781+ key = { youtubeVideoId }
1782+ // overflow + pointer-events help crop YT hover chrome (title/share/logo)
1783+ style = { {
1784+ position : "absolute" ,
1785+ inset : 0 ,
1786+ width : "100%" ,
1787+ height : "100%" ,
1788+ borderRadius : theme . radius . md ,
1789+ overflow : "hidden" ,
1790+ pointerEvents : "none" ,
1791+ background : "rgba(0,0,0,0.35)" ,
1792+ opacity : isVideoReady ? 1 : 0 ,
1793+ filter : stretchState === "loading" ? "blur(8px)" : "none" ,
1794+ transition : "opacity 0.2s ease-out, filter 0.3s ease-out" ,
1795+ isolation : "isolate" ,
1796+ } }
1797+ />
1798+ ) : null }
1799+ { localVideoUrl ? (
17421800 < video
17431801 ref = { videoRef }
1744- key = { media . videoUrl }
1745- src = { media . videoUrl }
1802+ key = { localVideoUrl }
1803+ src = { localVideoUrl }
17461804 muted
17471805 playsInline
1748- preload = { showingVideo ? "auto" : "metadata" }
1806+ preload = { showingLocalVideo ? "auto" : "metadata" }
17491807 poster = { coverUrl || undefined }
17501808 style = { {
17511809 position : "absolute" ,
@@ -1757,7 +1815,7 @@ export function Player({
17571815 userSelect : "none" ,
17581816 pointerEvents : "none" ,
17591817 background : "rgba(0,0,0,0.35)" ,
1760- opacity : showingVideo && isVideoReady ? 1 : 0 ,
1818+ opacity : showingLocalVideo && isVideoReady ? 1 : 0 ,
17611819 filter : stretchState === "loading" ? "blur(8px)" : "none" ,
17621820 transition : "opacity 0.2s ease-out, filter 0.3s ease-out" ,
17631821 } }
@@ -2083,13 +2141,13 @@ export function Player({
20832141 ) }
20842142 < Menu . Divider />
20852143 < Menu . Label > Actions</ Menu . Label >
2086- { ! media ?. isAudioTrackVideo && (
2144+ { hasVideoStream && (
20872145 < Menu . Item
20882146 leftSection = { < IconVideo size = { 14 } /> }
20892147 onClick = { handleToggleShowVideo }
2090- disabled = { ! media ?. videoUrl && ! showVideo }
2148+ disabled = { ! hasVideoStream && ! showVideo }
20912149 rightSection = {
2092- showVideo && media ?. videoUrl ? (
2150+ showVideo && hasVideoStream ? (
20932151 < Text size = "xs" c = "dimmed" >
20942152 On
20952153 </ Text >
0 commit comments