spotify-clone/frontend-vite/src/components/VideoPlayer.tsx
Khoa Vo 023fee470a feat: center video player and improve lyrics functionality with Vietnamese support
- Center video player in full-screen mode (like album art)
- Add Plyr video player with proper sync and cleanup
- Improve lyrics functionality with multiple API sources (lyrics.ovh, LRCLIB, SimpMusic, ZingMP3)
- Add Vietnamese language detection for lyrics
- Add helpful links to ZingMP3/NhacCuaTui when lyrics not found
- Mobile UI improvements (gaps, settings panel removal, centered cover)
- Add origin parameter to YouTube embed for better security
- Fix TypeScript errors and improve code quality
2026-03-21 20:55:53 +07:00

161 lines
No EOL
4.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import Plyr from 'plyr';
import 'plyr/dist/plyr.css';
interface VideoPlayerProps {
videoId: string;
isPlaying: boolean;
onTimeUpdate?: (time: number) => void;
onPlay?: () => void;
onPause?: () => void;
onEnded?: () => void;
className?: string;
}
export default function VideoPlayer({
videoId,
isPlaying,
onTimeUpdate,
onPlay,
onPause,
onEnded,
className = ''
}: VideoPlayerProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const playerRef = useRef<Plyr | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [isReady, setIsReady] = useState(false);
const isParentControlledRef = useRef<boolean>(false);
// Initialize Plyr
useEffect(() => {
if (!containerRef.current || !videoId) return;
// Clean up previous instance
if (playerRef.current) {
playerRef.current.destroy();
playerRef.current = null;
setIsReady(false);
}
// Clear container
containerRef.current.innerHTML = '';
// Create wrapper div for Plyr
const wrapper = document.createElement('div');
wrapper.className = 'plyr__video-embed';
containerRef.current.appendChild(wrapper);
// Create iframe
const iframe = document.createElement('iframe');
const origin = encodeURIComponent(window.location.origin);
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=0&modestbranding=1&rel=0&showinfo=0&origin=${origin}`;
iframe.allow = 'autoplay; fullscreen';
iframe.allowFullscreen = true;
iframe.setAttribute('allowtransparency', 'true');
wrapper.appendChild(iframe);
iframeRef.current = iframe;
// Initialize Plyr
const player = new Plyr(wrapper, {
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen'],
youtube: {
noCookie: false,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
modestbranding: 1,
},
ratio: '16:9',
fullscreen: {
enabled: true,
fallback: true,
iosNative: true,
},
clickToPlay: true,
hideControls: true,
resetOnEnd: false,
});
playerRef.current = player;
// Event handlers
player.on('ready', () => {
setIsReady(true);
if (isPlaying) {
isParentControlledRef.current = true;
player.play();
// Reset flag after a short delay to allow the play event to be processed
setTimeout(() => {
isParentControlledRef.current = false;
}, 100);
}
});
player.on('play', () => {
// Only call onPlay if this event was triggered by user interaction (not by parent)
if (!isParentControlledRef.current) {
onPlay?.();
}
});
player.on('pause', () => {
// Only call onPause if this event was triggered by user interaction (not by parent)
if (!isParentControlledRef.current) {
onPause?.();
}
});
player.on('timeupdate', () => {
if (onTimeUpdate && player.currentTime) {
onTimeUpdate(player.currentTime);
}
});
player.on('ended', () => {
onEnded?.();
});
// Clean up on unmount or videoId change
return () => {
if (playerRef.current) {
playerRef.current.destroy();
playerRef.current = null;
}
// Clear container
if (containerRef.current) {
containerRef.current.innerHTML = '';
}
setIsReady(false);
};
}, [videoId]);
// Handle play/pause from parent
useEffect(() => {
if (!playerRef.current || !isReady) return;
isParentControlledRef.current = true;
if (isPlaying) {
playerRef.current.play();
} else {
playerRef.current.pause();
}
// Reset flag after a short delay to allow the play/pause event to be processed
setTimeout(() => {
isParentControlledRef.current = false;
}, 100);
}, [isPlaying, isReady]);
return (
<div className={`w-full h-full ${className}`}>
<div ref={containerRef} className="w-full h-full plyr__video-embed" />
{!isReady && (
<div className="absolute inset-0 flex items-center justify-center bg-black">
<div className="w-12 h-12 border-4 border-white/30 border-t-[#1DB954] rounded-full animate-spin" />
</div>
)}
</div>
);
}