'use client'; import { useEffect, useRef, useState } from 'react'; import { Icon } from '../icons'; const fmt = (s: number) => { if (!Number.isFinite(s)) return '0:00'; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return `${m}:${String(sec).padStart(2, '0')}`; }; interface VideoPlayerProps { src: string; poster?: string; /** CSS filter string applied to the video (for edited clips). */ filter?: string; autoPlay?: boolean; } /** * Custom macOS-style video player: a glass transport bar (play/pause, scrubber * with buffered + played tracks, time, volume, mute, PiP, fullscreen), a centre * play affordance, auto-hiding controls while playing, and keyboard shortcuts. * Replaces the browser's native controls for a consistent, on-brand look. */ export function VideoPlayer({ src, poster, filter, autoPlay = true }: VideoPlayerProps) { const wrapRef = useRef(null); const videoRef = useRef(null); const hideTimer = useRef | null>(null); const [playing, setPlaying] = useState(false); const [time, setTime] = useState(0); const [duration, setDuration] = useState(0); const [buffered, setBuffered] = useState(0); const [volume, setVolume] = useState(1); const [muted, setMuted] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [controlsShown, setControlsShown] = useState(true); const v = () => videoRef.current; const togglePlay = () => { const el = v(); if (!el) return; if (el.paused) void el.play(); else el.pause(); }; const seek = (t: number) => { const el = v(); if (el) el.currentTime = Math.max(0, Math.min(duration || 0, t)); }; const toggleMute = () => { const el = v(); if (!el) return; el.muted = !el.muted; setMuted(el.muted); }; const changeVolume = (val: number) => { const el = v(); if (!el) return; el.volume = val; el.muted = val === 0; setVolume(val); setMuted(val === 0); }; const toggleFullscreen = () => { const wrap = wrapRef.current; if (!wrap) return; if (document.fullscreenElement) void document.exitFullscreen(); else void wrap.requestFullscreen?.(); }; const togglePip = () => { const el = v() as HTMLVideoElement & { requestPictureInPicture?: () => Promise }; if (!el) return; if (document.pictureInPictureElement) void document.exitPictureInPicture(); else void el.requestPictureInPicture?.().catch(() => {}); }; // Auto-hide controls while playing; always show when paused / on activity. const nudge = () => { setControlsShown(true); if (hideTimer.current) clearTimeout(hideTimer.current); hideTimer.current = setTimeout(() => { if (!v()?.paused) setControlsShown(false); }, 2600); }; useEffect(() => { const onFs = () => setFullscreen(Boolean(document.fullscreenElement)); document.addEventListener('fullscreenchange', onFs); return () => { document.removeEventListener('fullscreenchange', onFs); if (hideTimer.current) clearTimeout(hideTimer.current); }; }, []); const onKeyDown = (e: React.KeyboardEvent) => { const el = v(); if (!el) return; // Keys the player owns. For these, fully stop the native event so the // Lightbox's window-level keydown listener (prev/next/Escape) doesn't ALSO // fire — React's stopPropagation alone can't stop a separate window listener. const owned = [' ', 'k', 'ArrowLeft', 'ArrowRight', 'm', 'f']; if (owned.includes(e.key)) { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation?.(); } switch (e.key) { case ' ': case 'k': togglePlay(); break; case 'ArrowLeft': seek(el.currentTime - 5); break; case 'ArrowRight': seek(el.currentTime + 5); break; case 'm': toggleMute(); break; case 'f': toggleFullscreen(); break; } nudge(); }; const pct = duration ? (time / duration) * 100 : 0; const bufPct = duration ? (buffered / duration) * 100 : 0; return (
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
); }