import React, { useRef, useEffect, useState } from "react"; import { formatTime, formatDetailedTime } from "@/lib/timeUtils"; import logger from "../lib/logger"; import "../styles/VideoPlayer.css"; interface VideoPlayerProps { videoRef: React.RefObject; currentTime: number; duration: number; isPlaying: boolean; isMuted?: boolean; onPlayPause: () => void; onSeek: (time: number) => void; onToggleMute?: () => void; } const VideoPlayer: React.FC = ({ videoRef, currentTime, duration, isPlaying, isMuted = false, onPlayPause, onSeek, onToggleMute }) => { const progressRef = useRef(null); const [isIOS, setIsIOS] = useState(false); const [hasInitialized, setHasInitialized] = useState(false); const [lastPosition, setLastPosition] = useState(null); const [isDraggingProgress, setIsDraggingProgress] = useState(false); const isDraggingProgressRef = useRef(false); const [tooltipPosition, setTooltipPosition] = useState({ x: 0 }); const [tooltipTime, setTooltipTime] = useState(0); const sampleVideoUrl = (typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) || "/videos/sample-video-10m.mp4"; // Detect iOS device useEffect(() => { const checkIOS = () => { const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream; }; setIsIOS(checkIOS()); // Check if video was previously initialized if (typeof window !== "undefined") { const wasInitialized = localStorage.getItem("video_initialized") === "true"; setHasInitialized(wasInitialized); } }, []); // Update initialized state when video plays useEffect(() => { if (isPlaying && !hasInitialized) { setHasInitialized(true); if (typeof window !== "undefined") { localStorage.setItem("video_initialized", "true"); } } }, [isPlaying, hasInitialized]); // Add iOS-specific attributes to prevent fullscreen playback useEffect(() => { const video = videoRef.current; if (!video) return; // These attributes need to be set directly on the DOM element // for iOS Safari to respect inline playback video.setAttribute("playsinline", "true"); video.setAttribute("webkit-playsinline", "true"); video.setAttribute("x-webkit-airplay", "allow"); // Store the last known good position for iOS const handleTimeUpdate = () => { if (!isDraggingProgressRef.current) { setLastPosition(video.currentTime); if (typeof window !== "undefined") { window.lastSeekedPosition = video.currentTime; } } }; // Handle iOS-specific play/pause state const handlePlay = () => { logger.debug("Video play event fired"); if (isIOS) { setHasInitialized(true); localStorage.setItem("video_initialized", "true"); } }; const handlePause = () => { logger.debug("Video pause event fired"); }; video.addEventListener("timeupdate", handleTimeUpdate); video.addEventListener("play", handlePlay); video.addEventListener("pause", handlePause); return () => { video.removeEventListener("timeupdate", handleTimeUpdate); video.removeEventListener("play", handlePlay); video.removeEventListener("pause", handlePause); }; }, [videoRef, isIOS, isDraggingProgressRef]); // Save current time to lastPosition when it changes (from external seeking) useEffect(() => { setLastPosition(currentTime); }, [currentTime]); // Jump 10 seconds forward const handleForward = () => { const newTime = Math.min(currentTime + 10, duration); onSeek(newTime); setLastPosition(newTime); }; // Jump 10 seconds backward const handleBackward = () => { const newTime = Math.max(currentTime - 10, 0); onSeek(newTime); setLastPosition(newTime); }; // Calculate progress percentage const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0; // Handle start of progress bar dragging const handleProgressDragStart = (e: React.MouseEvent) => { e.preventDefault(); setIsDraggingProgress(true); isDraggingProgressRef.current = true; // Get initial position handleProgressDrag(e); // Set up document-level event listeners for mouse movement and release const handleMouseMove = (moveEvent: MouseEvent) => { if (isDraggingProgressRef.current) { handleProgressDrag(moveEvent); } }; const handleMouseUp = () => { setIsDraggingProgress(false); isDraggingProgressRef.current = false; document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; // Handle progress dragging for both mouse and touch events const handleProgressDrag = (e: MouseEvent | React.MouseEvent) => { if (!progressRef.current) return; const rect = progressRef.current.getBoundingClientRect(); const clickPosition = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const seekTime = duration * clickPosition; // Update tooltip position and time setTooltipPosition({ x: e.clientX }); setTooltipTime(seekTime); // Store position locally for iOS Safari - critical for timeline seeking setLastPosition(seekTime); // Also store globally for integration with other components if (typeof window !== "undefined") { (window as any).lastSeekedPosition = seekTime; } onSeek(seekTime); }; // Handle touch events for progress bar const handleProgressTouchStart = (e: React.TouchEvent) => { if (!progressRef.current || !e.touches[0]) return; e.preventDefault(); setIsDraggingProgress(true); isDraggingProgressRef.current = true; // Get initial position using touch handleProgressTouchMove(e); // Set up document-level event listeners for touch movement and release const handleTouchMove = (moveEvent: TouchEvent) => { if (isDraggingProgressRef.current) { handleProgressTouchMove(moveEvent); } }; const handleTouchEnd = () => { setIsDraggingProgress(false); isDraggingProgressRef.current = false; document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener("touchend", handleTouchEnd); document.removeEventListener("touchcancel", handleTouchEnd); }; document.addEventListener("touchmove", handleTouchMove, { passive: false }); document.addEventListener("touchend", handleTouchEnd); document.addEventListener("touchcancel", handleTouchEnd); }; // Handle touch dragging on progress bar const handleProgressTouchMove = (e: TouchEvent | React.TouchEvent) => { if (!progressRef.current) return; // Get the touch coordinates const touch = "touches" in e ? e.touches[0] : null; if (!touch) return; e.preventDefault(); // Prevent scrolling while dragging const rect = progressRef.current.getBoundingClientRect(); const touchPosition = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); const seekTime = duration * touchPosition; // Update tooltip position and time setTooltipPosition({ x: touch.clientX }); setTooltipTime(seekTime); // Store position for iOS Safari setLastPosition(seekTime); // Also store globally for integration with other components if (typeof window !== "undefined") { (window as any).lastSeekedPosition = seekTime; } onSeek(seekTime); }; // Handle click on progress bar (for non-drag interactions) const handleProgressClick = (e: React.MouseEvent) => { // If we're already dragging, don't handle the click if (isDraggingProgress) return; if (progressRef.current) { const rect = progressRef.current.getBoundingClientRect(); const clickPosition = (e.clientX - rect.left) / rect.width; const seekTime = duration * clickPosition; // Store position locally for iOS Safari - critical for timeline seeking setLastPosition(seekTime); // Also store globally for integration with other components if (typeof window !== "undefined") { (window as any).lastSeekedPosition = seekTime; } onSeek(seekTime); } }; // Handle toggling fullscreen const handleFullscreen = () => { if (videoRef.current) { if (document.fullscreenElement) { document.exitFullscreen(); } else { videoRef.current.requestFullscreen(); } } }; // Handle click on video to play/pause const handleVideoClick = () => { const video = videoRef.current; if (!video) return; // If the video is paused, we want to play it if (video.paused) { // For iOS Safari: Before playing, explicitly seek to the remembered position if (isIOS && lastPosition !== null && lastPosition > 0) { logger.debug("iOS: Explicitly setting position before play:", lastPosition); // First, seek to the position video.currentTime = lastPosition; // Use a small timeout to ensure seeking is complete before play setTimeout(() => { if (videoRef.current) { // Try to play with proper promise handling videoRef.current .play() .then(() => { logger.debug( "iOS: Play started successfully at position:", videoRef.current?.currentTime ); onPlayPause(); // Update parent state after successful play }) .catch((err) => { console.error("iOS: Error playing video:", err); }); } }, 50); } else { // Normal play (non-iOS or no remembered position) video .play() .then(() => { logger.debug("Normal: Play started successfully"); onPlayPause(); // Update parent state after successful play }) .catch((err) => { console.error("Error playing video:", err); }); } } else { // If playing, pause and update state video.pause(); onPlayPause(); } }; return (
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */} {isIOS && !hasInitialized && !isPlaying && (
Tap Play to initialize video controls
)} {/* Play/Pause Indicator (shows based on current state) */}
{/* Video Controls Overlay */}
{/* Time and Duration */}
{formatTime(currentTime)} / {formatTime(duration)}
{/* Progress Bar with enhanced dragging */}
{/* Floating time tooltip when dragging */} {isDraggingProgress && (
{formatDetailedTime(tooltipTime)}
)}
{/* Controls - Mute and Fullscreen buttons */}
{/* Mute/Unmute Button */} {onToggleMute && ( )} {/* Fullscreen Button */}
); }; export default VideoPlayer;