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 {
|
||||
id: string;
|
||||
@@ -17,90 +17,29 @@ export default function MusicPlayer() {
|
||||
const [activeId, setActiveId] = useState<string>('lofi-girl');
|
||||
const [volume, setVolume] = useState(0.3);
|
||||
const [started, setStarted] = useState(false);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const playerRef = useRef<any>(null);
|
||||
const volumeRef = useRef(volume);
|
||||
const activeIdRef = useRef(activeId);
|
||||
const readyResolveRef = useRef<(() => void) | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useEffect(() => { volumeRef.current = volume; }, [volume]);
|
||||
useEffect(() => { activeIdRef.current = activeId; }, [activeId]);
|
||||
const active = LOFI_PLAYLISTS.find((p) => p.id === activeId) ?? LOFI_PLAYLISTS[0];
|
||||
|
||||
// Load YouTube IFrame API and create player
|
||||
const initPlayer = useCallback(() => {
|
||||
const active = LOFI_PLAYLISTS.find((p) => p.id === activeIdRef.current);
|
||||
if (!active || playerRef.current) return;
|
||||
|
||||
const createYT = () => {
|
||||
playerRef.current = new (window as any).YT.Player('kira-youtube-player', {
|
||||
height: '1',
|
||||
width: '1',
|
||||
videoId: active.videoId,
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
// Sync volume to iframe via YT postMessage API
|
||||
useEffect(() => {
|
||||
if (!started || !iframeRef.current) return;
|
||||
const target = Math.round(volume * 100);
|
||||
// YouTube IFrame postMessage API for volume control
|
||||
iframeRef.current.contentWindow?.postMessage(
|
||||
JSON.stringify({ event: 'command', func: 'setVolume', args: [target] }),
|
||||
'*'
|
||||
);
|
||||
}, [volume, started]);
|
||||
|
||||
const handlePlay = () => {
|
||||
setStarted(true);
|
||||
initPlayer();
|
||||
};
|
||||
|
||||
const changeStation = (id: string) => {
|
||||
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 (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
@@ -125,7 +64,7 @@ export default function MusicPlayer() {
|
||||
{LOFI_PLAYLISTS.map((p) => (
|
||||
<button
|
||||
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 ${
|
||||
activeId === p.id && started
|
||||
? 'bg-kira-lav text-white shadow-md'
|
||||
@@ -138,8 +77,18 @@ export default function MusicPlayer() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* YouTube player: must have real dimensions for the API to work */}
|
||||
<div id="kira-youtube-player" style={{ width: 1, height: 1, opacity: 0, position: 'absolute', pointerEvents: 'none' }} />
|
||||
{/* Hidden YouTube iframe — direct embed, no API */}
|
||||
{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 }}>
|
||||
{!started ? (
|
||||
@@ -152,8 +101,8 @@ export default function MusicPlayer() {
|
||||
</button>
|
||||
) : (
|
||||
<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>playing {LOFI_PLAYLISTS.find((p) => p.id === activeId)?.name}</span>
|
||||
<span className="w-2 h-2 rounded-full bg-kira-mint animate-pulse inline-block" />
|
||||
<span>playing {active.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user