import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle, useMemo, } from 'react' import videojs from 'video.js' import Player from 'video.js/dist/types/player' import 'video.js/dist/video-js.css' export interface VideoPlayerRef { play: () => Promise pause: () => void } /** * Video.js-powered player with automatic format handling. Includes custom play/pause overlay and imperative controls via ref (for advanced scenarios like synchronized playback). Video.js handles codec support, adaptive streaming, and disposal lifecycle automatically. * @publicDocs */ export interface VideoPlayerProps { /** The video source URL */ src: string /** The format/MIME type of the video (default: 'video/mp4') */ format?: string /** Whether the video should be muted */ muted?: boolean /** URL for the poster image shown before playback */ poster?: string /** Whether the video should autoplay */ autoplay?: boolean /** Preload behavior: 'none', 'metadata', or 'auto' */ preload?: 'none' | 'metadata' | 'auto' /** Whether the video should loop */ loop?: boolean /** Video width in pixels */ width?: number /** Video height in pixels */ height?: number /** Custom play button component */ playButtonComponent?: React.ReactNode /** Callback when video starts playing */ onPlay?: () => void /** Callback when video is paused */ onPause?: () => void /** Callback when video ends */ onEnded?: () => void /** Callback when video player is ready */ onReady?: () => void } export const VideoPlayer: React.ForwardRefExoticComponent< VideoPlayerProps & React.RefAttributes > = forwardRef( ( { src, format = 'video/mp4', poster, muted, autoplay, preload = 'auto', loop = false, width, height, playButtonComponent, onPlay, onPause, onEnded, onReady, }, ref ) => { const videoContainerRef = useRef(null) const playerRef = useRef(null) const [isPlaying, setIsPlaying] = useState(false) // Expose imperative methods to parent components useImperativeHandle( ref, () => ({ play: async () => { if (playerRef.current) { try { await playerRef.current.play() setIsPlaying(true) } catch (error) { console.error('Error playing video:', error) } } }, pause: () => { if (playerRef.current) { playerRef.current.pause() setIsPlaying(false) } }, }), [] ) const options = useMemo( () => ({ controls: false, controlBar: { volumePanel: { inline: false, }, }, preload, loop, muted, // This makes sure that the video player does not take over the whole screen preferFullWindow: false, playsinline: true, poster, height, width, sources: [ { src, type: format, }, ], }), [muted, poster, height, width, src, format, preload, loop] ) useEffect(() => { const player = playerRef.current return () => { if (player && !player.isDisposed()) { player.dispose() playerRef.current = null } } }, [playerRef]) useEffect(() => { if (!playerRef.current) return const handlePlay = () => { onPlay?.() setIsPlaying(true) } const handlePause = () => { onPause?.() setIsPlaying(false) } const handleEnded = () => { onEnded?.() setIsPlaying(false) } playerRef.current.on('play', handlePlay) playerRef.current.on('pause', handlePause) playerRef.current.on('ended', handleEnded) return () => { if (playerRef.current) { playerRef.current.off('play', handlePlay) playerRef.current.off('pause', handlePause) playerRef.current.off('ended', handleEnded) } } }, [onEnded, onPause, onPlay]) const togglePlayPause = useCallback(() => { if (isPlaying) { playerRef.current?.pause() setIsPlaying(false) } else { playerRef.current?.play() setIsPlaying(true) } }, [isPlaying]) useEffect(() => { if (!playerRef.current) { const videoElement = document.createElement('video-js') // The Video.js player needs to be _inside_ the component element for React 18 Strict Mode. videoContainerRef.current?.appendChild(videoElement) playerRef.current = videojs(videoElement, options, () => { onReady && onReady() if (autoplay) { togglePlayPause() } }) } }, [options, videoContainerRef, autoplay, onReady, togglePlayPause]) // Update muted state when prop changes after initial render useEffect(() => { if (playerRef.current && !playerRef.current.isDisposed()) { playerRef.current.muted(muted ?? false) } }, [muted]) const containerClassName = [ width ? `w-${width}` : undefined, 'relative', ].join(' ') return (
{isPlaying ? null : (playButtonComponent ?? )}
) } ) const DefaultPlayIcon = () => (
)