fix(lofi): replace YT IFrame API with direct iframe embed
The YT Player API silently fails to autoplay in hidden iframes. Replaced with a direct <iframe> embed with allow='autoplay; encrypted-media'. - On 'Start Lo-Fi': creates iframe with autoplay=1 - Station change: remounts iframe with new videoId - Volume: postMessage API to YT iframe (best effort) - Much simpler, no external script dependency
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
interface Playlist {
|
interface Playlist {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,90 +17,29 @@ export default function MusicPlayer() {
|
|||||||
const [activeId, setActiveId] = useState<string>('lofi-girl');
|
const [activeId, setActiveId] = useState<string>('lofi-girl');
|
||||||
const [volume, setVolume] = useState(0.3);
|
const [volume, setVolume] = useState(0.3);
|
||||||
const [started, setStarted] = useState(false);
|
const [started, setStarted] = useState(false);
|
||||||
const [playing, setPlaying] = useState(false);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const playerRef = useRef<any>(null);
|
|
||||||
const volumeRef = useRef(volume);
|
|
||||||
const activeIdRef = useRef(activeId);
|
|
||||||
const readyResolveRef = useRef<(() => void) | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => { volumeRef.current = volume; }, [volume]);
|
const active = LOFI_PLAYLISTS.find((p) => p.id === activeId) ?? LOFI_PLAYLISTS[0];
|
||||||
useEffect(() => { activeIdRef.current = activeId; }, [activeId]);
|
|
||||||
|
|
||||||
// Load YouTube IFrame API and create player
|
// Sync volume to iframe via YT postMessage API
|
||||||
const initPlayer = useCallback(() => {
|
useEffect(() => {
|
||||||
const active = LOFI_PLAYLISTS.find((p) => p.id === activeIdRef.current);
|
if (!started || !iframeRef.current) return;
|
||||||
if (!active || playerRef.current) return;
|
const target = Math.round(volume * 100);
|
||||||
|
// YouTube IFrame postMessage API for volume control
|
||||||
const createYT = () => {
|
iframeRef.current.contentWindow?.postMessage(
|
||||||
playerRef.current = new (window as any).YT.Player('kira-youtube-player', {
|
JSON.stringify({ event: 'command', func: 'setVolume', args: [target] }),
|
||||||
height: '1',
|
'*'
|
||||||
width: '1',
|
);
|
||||||
videoId: active.videoId,
|
}, [volume, started]);
|
||||||
playerVars: {
|
|
||||||
autoplay: 1,
|
|
||||||
controls: 0,
|
|
||||||
loop: 1,
|
|
||||||
playlist: active.videoId,
|
|
||||||
enablejsapi: 1,
|
|
||||||
modestbranding: 1,
|
|
||||||
fs: 0,
|
|
||||||
rel: 0,
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
onReady: (e: any) => {
|
|
||||||
e.target.setVolume(Math.round(volumeRef.current * 100));
|
|
||||||
e.target.playVideo();
|
|
||||||
setPlaying(true);
|
|
||||||
},
|
|
||||||
onStateChange: (e: any) => {
|
|
||||||
// YT.PlayerState.PLAYING = 1, PAUSED = 2, ENDED = 0
|
|
||||||
setPlaying(e.data === 1);
|
|
||||||
},
|
|
||||||
onError: (e: any) => {
|
|
||||||
console.warn('[MusicPlayer] YouTube error:', e.data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((window as any).YT?.Player) {
|
|
||||||
createYT();
|
|
||||||
} else {
|
|
||||||
// Load the script
|
|
||||||
const tag = document.createElement('script');
|
|
||||||
tag.src = 'https://www.youtube.com/iframe_api';
|
|
||||||
document.head.appendChild(tag);
|
|
||||||
(window as any).onYouTubeIframeAPIReady = createYT;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
setStarted(true);
|
setStarted(true);
|
||||||
initPlayer();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeStation = (id: string) => {
|
const changeStation = (id: string) => {
|
||||||
setActiveId(id);
|
setActiveId(id);
|
||||||
if (playerRef.current && (window as any).YT) {
|
|
||||||
const active = LOFI_PLAYLISTS.find((p) => p.id === id);
|
|
||||||
if (active) {
|
|
||||||
try {
|
|
||||||
playerRef.current.loadVideoById(active.videoId);
|
|
||||||
playerRef.current.setVolume(Math.round(volume * 100));
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Volume sync
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
if (playerRef.current?.setVolume) {
|
|
||||||
playerRef.current.setVolume(Math.round(volume * 100));
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}, [volume]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -125,7 +64,7 @@ export default function MusicPlayer() {
|
|||||||
{LOFI_PLAYLISTS.map((p) => (
|
{LOFI_PLAYLISTS.map((p) => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => started ? changeStation(p.id) : setActiveId(p.id)}
|
onClick={() => changeStation(p.id)}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition-all ${
|
||||||
activeId === p.id && started
|
activeId === p.id && started
|
||||||
? 'bg-kira-lav text-white shadow-md'
|
? 'bg-kira-lav text-white shadow-md'
|
||||||
@@ -138,8 +77,18 @@ export default function MusicPlayer() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* YouTube player: must have real dimensions for the API to work */}
|
{/* Hidden YouTube iframe — direct embed, no API */}
|
||||||
<div id="kira-youtube-player" style={{ width: 1, height: 1, opacity: 0, position: 'absolute', pointerEvents: 'none' }} />
|
{started && (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={`https://www.youtube.com/embed/${active.videoId}?autoplay=1&loop=1&playlist=${active.videoId}&controls=0&modestbranding=1&fs=0&rel=0&enablejsapi=1`}
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
allow="autoplay; encrypted-media"
|
||||||
|
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none' }}
|
||||||
|
title="lofi player"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="relative rounded-xl overflow-hidden bg-white/30" style={{ height: 60 }}>
|
<div className="relative rounded-xl overflow-hidden bg-white/30" style={{ height: 60 }}>
|
||||||
{!started ? (
|
{!started ? (
|
||||||
@@ -152,8 +101,8 @@ export default function MusicPlayer() {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-kira-plum/40 text-xs gap-2">
|
<div className="w-full h-full flex items-center justify-center text-kira-plum/40 text-xs gap-2">
|
||||||
<span className={`w-2 h-2 rounded-full animate-pulse inline-block ${playing ? 'bg-kira-mint' : 'bg-kira-plum/30'}`} />
|
<span className="w-2 h-2 rounded-full bg-kira-mint animate-pulse inline-block" />
|
||||||
<span>playing {LOFI_PLAYLISTS.find((p) => p.id === activeId)?.name}</span>
|
<span>playing {active.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user