'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import Artplayer from 'artplayer'; import { detectVideoFormat, VideoFormat, VideoFile } from '@/lib/video-format-detector'; import { Bookmark, Star } from 'lucide-react'; interface ArtPlayerWrapperProps { video: VideoFile; isOpen: boolean; onClose: () => void; onProgress?: (time: number) => void; onBookmark?: (videoId: number) => void; onUnbookmark?: (videoId: number) => void; onRate?: (videoId: number, rating: number) => void; useArtPlayer: boolean; isBookmarked?: boolean; bookmarkCount?: number; avgRating?: number; showBookmarks?: boolean; showRatings?: boolean; } export default function ArtPlayerWrapper({ video, isOpen, onClose, onProgress, onBookmark, onUnbookmark, onRate, useArtPlayer, isBookmarked = false, bookmarkCount = 0, avgRating = 0, showBookmarks = false, showRatings = false }: ArtPlayerWrapperProps) { const containerRef = useRef(null); const playerRef = useRef(null); const [format, setFormat] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); const [showControls, setShowControls] = useState(true); const [localIsBookmarked, setLocalIsBookmarked] = useState(isBookmarked); const [localBookmarkCount, setLocalBookmarkCount] = useState(bookmarkCount); const [localAvgRating, setLocalAvgRating] = useState(avgRating); // Update local state when props change useEffect(() => { setLocalIsBookmarked(isBookmarked); setLocalBookmarkCount(bookmarkCount); setLocalAvgRating(avgRating); }, [isBookmarked, bookmarkCount, avgRating]); // Initialize ArtPlayer useEffect(() => { if (!useArtPlayer || !isOpen || !containerRef.current) return; setIsLoading(true); setError(null); try { const detectedFormat = detectVideoFormat(video); setFormat(detectedFormat); const player = new Artplayer({ container: containerRef.current, url: detectedFormat.url, type: detectedFormat.type === 'hls' ? 'm3u8' : 'mp4', // Core playback settings autoplay: false, muted: false, volume: volume, // UI controls fullscreen: true, fullscreenWeb: true, pip: true, playbackRate: true, aspectRatio: true, screenshot: true, hotkey: true, // Display settings theme: '#3b82f6', // Blue theme // Quality control (for HLS) quality: detectedFormat.qualities || [], // Subtitle support subtitle: { url: '', // Will be populated if subtitles are available type: 'vtt', style: { color: '#fff', fontSize: '20px', textShadow: '0 0 2px rgba(0,0,0,0.8)' } }, // Settings settings: [ { html: 'Quality', icon: '⚙️', selector: detectedFormat.qualities || [], onSelect: function(item: any) { console.log('Quality selected:', item); } } ], // Custom layer for bookmark and rating controls layers: [ { html: `
${localBookmarkCount}
★ ${localAvgRating.toFixed(1)}
`, style: { position: 'absolute', top: '16px', right: '16px', zIndex: '10' } as any, click: function(this: any, component: any, event: Event) { const target = event.target as HTMLElement; if (target.closest('.artplayer-bookmark-control')) { handleBookmarkToggle(); } else if (target.closest('.artplayer-rating-control')) { handleRatingClick(); } } } ] }); // Event listeners player.on('ready', () => { console.log('ArtPlayer ready'); setIsLoading(false); }); player.on('play', () => { setIsPlaying(true); }); player.on('pause', () => { setIsPlaying(false); }); player.on('timeupdate', () => { const currentTime = player.currentTime; setCurrentTime(currentTime); if (onProgress) { onProgress(currentTime); } }); player.on('video:loadedmetadata', () => { setDuration(player.duration); }); player.on('volumechange', () => { setVolume(player.volume); setIsMuted(player.muted); }); player.on('error', (error) => { console.error('ArtPlayer error:', error); setError(`Failed to load video: ${error.message || 'Unknown error'}`); setIsLoading(false); // Fallback to current player if ArtPlayer fails if (format?.supportLevel === 'native') { console.log('ArtPlayer failed for native format, falling back to current player'); // This will trigger the parent component to switch to current player } }); // Custom event listeners player.on('controls:show', () => { setShowControls(true); }); player.on('controls:hidden', () => { setShowControls(false); }); playerRef.current = player; return () => { if (playerRef.current) { playerRef.current.destroy(); playerRef.current = null; } }; } catch (error) { console.error('Failed to initialize ArtPlayer:', error); setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`); setIsLoading(false); } }, [useArtPlayer, isOpen, video, onProgress, volume, format?.supportLevel, localIsBookmarked, localBookmarkCount, localAvgRating]); // Handle bookmark toggle const handleBookmarkToggle = useCallback(async () => { if (!video.id) return; try { if (localIsBookmarked) { if (onUnbookmark) { onUnbookmark(video.id); } setLocalIsBookmarked(false); setLocalBookmarkCount(prev => Math.max(0, prev - 1)); } else { if (onBookmark) { onBookmark(video.id); } setLocalIsBookmarked(true); setLocalBookmarkCount(prev => prev + 1); } } catch (error) { console.error('Error toggling bookmark:', error); } }, [video.id, localIsBookmarked, onBookmark, onUnbookmark]); // Handle rating click const handleRatingClick = useCallback(() => { if (!video.id || !onRate) return; const currentRating = Math.round(localAvgRating); const newRating = currentRating >= 5 ? 1 : currentRating + 1; onRate(video.id, newRating); setLocalAvgRating(newRating); }, [video.id, localAvgRating, onRate]); // Format time display const formatTime = (time: number) => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; // Format file size const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Handle keyboard shortcuts useEffect(() => { if (!isOpen || !playerRef.current) return; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'Escape': e.preventDefault(); onClose(); break; case ' ': e.preventDefault(); if (isPlaying) { playerRef.current?.pause(); } else { playerRef.current?.play(); } break; case 'ArrowLeft': e.preventDefault(); playerRef.current!.currentTime = Math.max(0, playerRef.current!.currentTime - 10); break; case 'ArrowRight': e.preventDefault(); playerRef.current!.currentTime = Math.min(playerRef.current!.duration || 0, playerRef.current!.currentTime + 10); break; case 'f': case 'F': e.preventDefault(); playerRef.current!.fullscreen = !playerRef.current!.fullscreen; break; case 'm': case 'M': e.preventDefault(); playerRef.current!.muted = !playerRef.current!.muted; break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose, isPlaying]); // Cleanup on unmount useEffect(() => { return () => { if (playerRef.current) { playerRef.current.destroy(); playerRef.current = null; } }; }, []); if (!isOpen) return null; return (
{/* Close button */} {/* Loading indicator */} {isLoading && (
Loading ArtPlayer...
)} {/* Error indicator */} {error && (
ArtPlayer Error
{error}
)} {/* Format info */} {format && (
{format.supportLevel === 'native' ? 'Native' : format.supportLevel === 'hls' ? 'HLS' : 'Fallback'}
)} {/* Video container */}
setShowControls(true)} onMouseLeave={() => setShowControls(false)} > {/* ArtPlayer will be mounted here */}
{/* Loading overlay */} {isLoading && (

Loading ArtPlayer...

)}
{/* Video info overlay */}

{video.title}

{formatFileSize(video.size)}

{duration > 0 && (

Duration: {formatTime(duration)} {format?.type === 'hls' && (HLS)}

)}
{(showBookmarks || showRatings) && (
{showBookmarks && ( )} {showRatings && (
{localAvgRating.toFixed(1)}
)}
)}
{/* Controls */}
{Math.round(volume * 100)}%
{formatTime(currentTime)} / {formatTime(duration)}
); }