UNPKG

@werk1/w1-system-videoblock

Version:

Universal video player supporting YouTube, Vimeo, HLS, DASH with coordination and GSAP integration for W1 System

858 lines (839 loc) 119 kB
import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import React, { useState, useRef, useEffect, useCallback, useMemo, useImperativeHandle } from 'react'; /** * Modern SVG play icon component for W1VideoBlock controls * * @component * @param {Object} props - Component props * @param {string} [props.color='currentColor'] - Color of the icon * @param {number} [props.size=20] - Size of the icon in pixels * @param {string} [props.className=''] - Additional CSS class name * @returns {JSX.Element} Rendered play icon */ const PlayIcon = ({ color = 'currentColor', size = 20, className = '' }) => { return (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Play", role: "img", children: jsx("path", { d: "M8 5.14v13.72L19.14 12L8 5.14z", fill: color, stroke: "none" }) })); }; /** * Modern SVG pause icon component for W1VideoBlock controls * * @component * @param {Object} props - Component props * @param {string} [props.color='currentColor'] - Color of the icon * @param {number} [props.size=20] - Size of the icon in pixels * @param {string} [props.className=''] - Additional CSS class name * @returns {JSX.Element} Rendered pause icon */ const PauseIcon = ({ color = 'currentColor', size = 20, className = '' }) => { return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Pause", role: "img", children: [jsx("rect", { x: "6", y: "4", width: "4", height: "16", fill: color, rx: "1" }), jsx("rect", { x: "14", y: "4", width: "4", height: "16", fill: color, rx: "1" })] })); }; /** * Modern SVG volume icon component for W1VideoBlock controls * Displays different icons based on volume level and mute state * * @component * @param {Object} props - Component props * @param {number} props.level - Volume level (0-1) * @param {boolean} [props.isMuted=false] - Whether volume is muted * @param {string} [props.color='currentColor'] - Color of the icon * @param {number} [props.size=20] - Size of the icon in pixels * @param {string} [props.className=''] - Additional CSS class name * @returns {JSX.Element} Rendered volume icon */ const VolumeIcon = ({ level, isMuted = false, color = 'currentColor', size = 20, className = '' }) => { // Determine which icon to show const getVolumeIcon = () => { if (isMuted || level === 0) { // Modern muted icon - speaker with clean X return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Muted", role: "img", children: [jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsx("path", { d: "M17.5 8.5l3 3m0-3l-3 3", stroke: color, strokeWidth: "2", strokeLinecap: "round" })] })); } else if (level < 0.5) { // Low volume icon return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Low volume", role: "img", children: [jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsx("path", { d: "M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z", fill: color })] })); } else { // High volume icon return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "High volume", role: "img", children: [jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsx("path", { d: "M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z", fill: color }), jsx("path", { d: "M20.5 12c0-3.53-2.04-6.58-5-8.05v2.17c2.33 1.32 3.92 3.85 3.92 6.88 0 3.03-1.59 5.56-3.92 6.88v2.17c2.96-1.47 5-4.52 5-8.05z", fill: color })] })); } }; return getVolumeIcon(); }; /** * Modern SVG fullscreen icon component for W1VideoBlock controls * Shows enter or exit fullscreen icon based on current state * * @component * @param {Object} props - Component props * @param {boolean} [props.isFullscreen=false] - Whether currently in fullscreen * @param {string} [props.color='currentColor'] - Color of the icon * @param {number} [props.size=20] - Size of the icon in pixels * @param {string} [props.className=''] - Additional CSS class name * @returns {JSX.Element} Rendered fullscreen icon */ const FullscreenIcon = ({ isFullscreen = false, color = 'currentColor', size = 20, className = '' }) => { if (isFullscreen) { // Exit fullscreen icon return (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Exit fullscreen", role: "img", children: jsx("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z", fill: color }) })); } else { // Enter fullscreen icon return (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Enter fullscreen", role: "img", children: jsx("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z", fill: color }) })); } }; /** * Modern SVG picture-in-picture icon component for W1VideoBlock controls * Shows enter or exit PiP icon based on current state * * @component * @param {Object} props - Component props * @param {boolean} [props.isPictureInPicture=false] - Whether currently in PiP mode * @param {string} [props.color='currentColor'] - Color of the icon * @param {number} [props.size=20] - Size of the icon in pixels * @param {string} [props.className=''] - Additional CSS class name * @returns {JSX.Element} Rendered picture-in-picture icon */ const PictureInPictureIcon = ({ isPictureInPicture = false, color = 'currentColor', size = 20, className = '' }) => { if (isPictureInPicture) { // Exit picture-in-picture icon return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Exit picture-in-picture", role: "img", children: [jsx("path", { d: "M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zm-10-7h9v6h-9v-6z", fill: color }), jsx("path", { d: "M16 12h-5v4h5v-4z", fill: "none", stroke: color, strokeWidth: "1" })] })); } else { // Enter picture-in-picture icon return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Enter picture-in-picture", role: "img", children: [jsx("path", { d: "M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z", fill: color }), jsx("path", { d: "M13 12h7v5h-7z", fill: color })] })); } }; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z$2 = ".videocontrols-module_controls__rEKsz{align-items:center;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:linear-gradient(180deg,transparent,rgba(0,0,0,.7));display:flex;gap:12px;left:0;padding:16px;position:absolute;right:0;transition:opacity .3s ease,transform .3s ease;-webkit-user-select:none;user-select:none;z-index:10}.videocontrols-module_controlsBottom__yNQkJ{border-radius:0;bottom:0}.videocontrols-module_controlsTop__1LOwc{background:linear-gradient(0deg,transparent,rgba(0,0,0,.7));top:0}.videocontrols-module_controlsOverlay__T8vJm{background:rgba(0,0,0,.8);border-radius:12px;bottom:auto;left:50%;padding:20px;right:auto;top:50%;transform:translate(-50%,-50%)}.videocontrols-module_controlsVisible__ee2m1{opacity:1;pointer-events:auto;transform:translateY(0)}.videocontrols-module_controlsHidden__ezYj1{opacity:0;pointer-events:none;transform:translateY(100%)}.videocontrols-module_controlsTop__1LOwc.videocontrols-module_controlsHidden__ezYj1{transform:translateY(-100%)}.videocontrols-module_controlsOverlay__T8vJm.videocontrols-module_controlsHidden__ezYj1{transform:translate(-50%,-50%) scale(.9)}.videocontrols-module_themeDark__UeWkX{background:linear-gradient(180deg,transparent,rgba(0,0,0,.8));color:#fff}.videocontrols-module_themeLight__SJicq{background:linear-gradient(180deg,transparent,hsla(0,0%,100%,.9));color:#000}.videocontrols-module_themeTransparent__ZMCB0{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:rgba(0,0,0,.3);color:#fff}.videocontrols-module_compact__Q34gt{gap:8px;padding:8px 12px}.videocontrols-module_compact__Q34gt .videocontrols-module_button__oBEgG{font-size:14px;height:32px;width:32px}.videocontrols-module_button__oBEgG{-webkit-tap-highlight-color:transparent;align-items:center;background:none;border:none;border-radius:6px;color:inherit;cursor:pointer;display:flex;font-size:16px;height:40px;justify-content:center;padding:8px;transition:background-color .2s ease,transform .1s ease;width:40px}.videocontrols-module_button__oBEgG:hover{background-color:hsla(0,0%,100%,.1)}.videocontrols-module_button__oBEgG:active{transform:scale(.95)}.videocontrols-module_buttonDisabled__7EZ4I,.videocontrols-module_button__oBEgG:disabled{cursor:not-allowed;opacity:.5;pointer-events:none}.videocontrols-module_buttonDisabled__7EZ4I:hover,.videocontrols-module_button__oBEgG:disabled:hover{background-color:transparent;transform:none}.videocontrols-module_themeLight__SJicq .videocontrols-module_button__oBEgG:hover{background-color:rgba(0,0,0,.1)}.videocontrols-module_timeDisplay__BabHR{font-family:Roboto Mono,monospace;font-size:14px;font-weight:500;min-width:45px;text-align:center;white-space:nowrap}.videocontrols-module_progressContainer__Mhyc7{-webkit-tap-highlight-color:transparent;align-items:center;cursor:pointer;display:flex;flex:1;height:20px;padding:8px 0;position:relative}.videocontrols-module_progressBackground__qgoWw{border-radius:2px;height:4px;left:0;position:absolute;right:0;top:50%;transform:translateY(-50%)}.videocontrols-module_progressBackgroundDark__ldk5f{background-color:hsla(0,0%,100%,.2)}.videocontrols-module_progressBackgroundLight__Mxv8p{background-color:rgba(0,0,0,.2)}.videocontrols-module_progressBuffered__wovs7{border-radius:2px;height:4px;left:0;position:absolute;top:50%;transform:translateY(-50%);z-index:1}.videocontrols-module_progressBufferedDark__Px9Am{background-color:hsla(0,0%,100%,.4)}.videocontrols-module_progressBufferedLight__Ehfk7{background-color:rgba(0,0,0,.3)}.videocontrols-module_progressFill__5EyE2{border-radius:2px;height:4px;left:0;position:absolute;top:50%;transform:translateY(-50%);z-index:2}.videocontrols-module_progressFillDark__N1GpX{background-color:#fff}.videocontrols-module_progressFillLight__y80pS{background-color:#000}.videocontrols-module_progressFillSmooth__c8n-Z{transition:width .1s ease}.videocontrols-module_progressFillDragging__3l8Pv{transition:none}.videocontrols-module_progressContainer__Mhyc7:hover .videocontrols-module_progressBackground__qgoWw,.videocontrols-module_progressContainer__Mhyc7:hover .videocontrols-module_progressBuffered__wovs7,.videocontrols-module_progressContainer__Mhyc7:hover .videocontrols-module_progressFill__5EyE2{height:6px}.videocontrols-module_volumeSlider__paWH8{-webkit-tap-highlight-color:transparent;align-items:center;cursor:pointer;display:flex;height:20px;position:relative;width:60px}.videocontrols-module_volumeBackground__08RUu{border-radius:1.5px;height:3px;left:0;position:absolute;right:0;top:50%;transform:translateY(-50%)}.videocontrols-module_volumeBackgroundDark__jDcgf{background-color:hsla(0,0%,100%,.2)}.videocontrols-module_volumeBackgroundLight__qgwY1{background-color:rgba(0,0,0,.2)}.videocontrols-module_volumeFill__vP0ap{border-radius:1.5px;height:3px;left:0;position:absolute;top:50%;transform:translateY(-50%);transition:width .1s ease}.videocontrols-module_volumeFillDark__x2aAH{background-color:#fff}.videocontrols-module_volumeFillLight__aKjFZ{background-color:#000}@media (max-width:768px){.videocontrols-module_controls__rEKsz{gap:8px;padding:12px}.videocontrols-module_button__oBEgG{font-size:18px;height:44px;width:44px}.videocontrols-module_timeDisplay__BabHR{font-size:12px;min-width:40px}.videocontrols-module_progressContainer__Mhyc7{height:24px}.videocontrols-module_progressBackground__qgoWw,.videocontrols-module_progressBuffered__wovs7,.videocontrols-module_progressFill__5EyE2{height:6px!important}.videocontrols-module_volumeSlider__paWH8{width:50px}}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.videocontrols-module_controls__rEKsz{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}.videocontrols-module_button__oBEgG:focus{outline:2px solid hsla(0,0%,100%,.5);outline-offset:2px}.videocontrols-module_themeLight__SJicq .videocontrols-module_button__oBEgG:focus{outline-color:rgba(0,0,0,.5)}@media (prefers-reduced-motion:reduce){.videocontrols-module_button__oBEgG,.videocontrols-module_controls__rEKsz,.videocontrols-module_progressFill__5EyE2,.videocontrols-module_volumeFill__vP0ap{transition:none}}"; var styles$2 = {"controls":"videocontrols-module_controls__rEKsz","controlsBottom":"videocontrols-module_controlsBottom__yNQkJ","controlsTop":"videocontrols-module_controlsTop__1LOwc","controlsOverlay":"videocontrols-module_controlsOverlay__T8vJm","controlsVisible":"videocontrols-module_controlsVisible__ee2m1","controlsHidden":"videocontrols-module_controlsHidden__ezYj1","themeDark":"videocontrols-module_themeDark__UeWkX","themeLight":"videocontrols-module_themeLight__SJicq","themeTransparent":"videocontrols-module_themeTransparent__ZMCB0","compact":"videocontrols-module_compact__Q34gt","button":"videocontrols-module_button__oBEgG","buttonDisabled":"videocontrols-module_buttonDisabled__7EZ4I","timeDisplay":"videocontrols-module_timeDisplay__BabHR","progressContainer":"videocontrols-module_progressContainer__Mhyc7","progressBackground":"videocontrols-module_progressBackground__qgoWw","progressBackgroundDark":"videocontrols-module_progressBackgroundDark__ldk5f","progressBackgroundLight":"videocontrols-module_progressBackgroundLight__Mxv8p","progressBuffered":"videocontrols-module_progressBuffered__wovs7","progressBufferedDark":"videocontrols-module_progressBufferedDark__Px9Am","progressBufferedLight":"videocontrols-module_progressBufferedLight__Ehfk7","progressFill":"videocontrols-module_progressFill__5EyE2","progressFillDark":"videocontrols-module_progressFillDark__N1GpX","progressFillLight":"videocontrols-module_progressFillLight__y80pS","progressFillSmooth":"videocontrols-module_progressFillSmooth__c8n-Z","progressFillDragging":"videocontrols-module_progressFillDragging__3l8Pv","volumeSlider":"videocontrols-module_volumeSlider__paWH8","volumeBackground":"videocontrols-module_volumeBackground__08RUu","volumeBackgroundDark":"videocontrols-module_volumeBackgroundDark__jDcgf","volumeBackgroundLight":"videocontrols-module_volumeBackgroundLight__qgwY1","volumeFill":"videocontrols-module_volumeFill__vP0ap","volumeFillDark":"videocontrols-module_volumeFillDark__x2aAH","volumeFillLight":"videocontrols-module_volumeFillLight__aKjFZ"}; styleInject(css_248z$2); /** * W1VideoControls - Full-featured UI controls layer for video player * * Provides comprehensive video controls with auto-hide, themes, positioning, and keyboard shortcuts */ const W1VideoControls = ({ playerState, playerMethods, className = '', showVolumeControl = true, showFullscreen = true, showPictureInPicture = false, showTimeDisplay = true, showProgressBar = true, autoHide = true, autoHideDelay = 3000, initiallyVisible = true, showOnInteraction = true, customButtons, theme = 'dark', position = 'bottom', compact = false, playbackRates: _playbackRates = undefined, skipIntervals = 10, videoBlockId: _videoBlockId = undefined, videoCoordinationStore: _videoCoordinationStore = undefined, enableKeyboardControls = false, isMobileDevice = false, ref }) => { const [isClient, setIsClient] = useState(false); const [isVisible, setIsVisible] = useState(initiallyVisible); const [isDragging, setIsDragging] = useState(false); const hideTimeoutRef = useRef(null); const progressBarRef = useRef(null); const controlsRef = useRef(null); // Set client flag useEffect(() => { setIsClient(true); }, []); // Auto-hide logic (only on client) useEffect(() => { if (!autoHide || !isClient) return; const resetHideTimeout = () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } // Only set hide timeout if controls are visible and video is playing if (isVisible && playerState.isPlaying && !isDragging) { hideTimeoutRef.current = setTimeout(() => { setIsVisible(false); }, autoHideDelay); } }; resetHideTimeout(); return () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } }; }, [playerState.isPlaying, autoHide, autoHideDelay, isDragging, isClient, isVisible]); // Format time helper const formatTime = useCallback((seconds) => { if (isNaN(seconds)) return '0:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); // Polyfill for padStart for older browsers const pad = (num, size) => { const s = num.toString(); return s.length >= size ? s : '0'.repeat(size - s.length) + s; }; if (hours > 0) { return `${hours}:${pad(minutes, 2)}:${pad(secs, 2)}`; } return `${minutes}:${pad(secs, 2)}`; }, []); // Progress bar interaction const handleProgressClick = useCallback((e) => { if (!playerMethods || !progressBarRef.current || !playerState.duration || !isClient) return; const rect = progressBarRef.current.getBoundingClientRect(); const percentage = (e.clientX - rect.left) / rect.width; const newTime = percentage * playerState.duration; playerMethods.seek(newTime); }, [playerMethods, playerState.duration, isClient]); const handleProgressDrag = useCallback((e) => { if (!isDragging) return; handleProgressClick(e); }, [isDragging, handleProgressClick]); // Keyboard shortcuts (only on client and when enabled) useEffect(() => { if (!playerMethods || !isClient || !enableKeyboardControls) return; const handleKeyPress = (e) => { // Only handle if controls container is focused or contains focus const controlsElement = controlsRef.current; if (!controlsElement || !document.activeElement || !controlsElement.contains(document.activeElement)) { return; } switch (e.key) { case ' ': case 'k': e.preventDefault(); e.stopPropagation(); if (playerState.isPlaying) { playerMethods.pause(); } else { playerMethods.play().catch(() => { // Silently handle play failures }); } break; case 'ArrowLeft': e.preventDefault(); e.stopPropagation(); playerMethods.seek(Math.max(0, playerState.currentTime - skipIntervals)); break; case 'ArrowRight': e.preventDefault(); e.stopPropagation(); playerMethods.seek(Math.min(playerState.duration, playerState.currentTime + skipIntervals)); break; case 'ArrowUp': e.preventDefault(); e.stopPropagation(); playerMethods.setVolume(Math.min(1, playerState.volume + 0.1)); break; case 'ArrowDown': e.preventDefault(); e.stopPropagation(); playerMethods.setVolume(Math.max(0, playerState.volume - 0.1)); break; case 'm': case 'M': e.preventDefault(); e.stopPropagation(); playerMethods.setMuted(!playerState.isMuted); break; case 'f': case 'F': e.preventDefault(); e.stopPropagation(); if (isMobileDevice) { playerMethods.toggleMobileFullscreen?.(); } else { playerMethods.toggleFullscreen(); } break; case 'p': case 'P': e.preventDefault(); e.stopPropagation(); if (playerMethods.togglePictureInPicture) { playerMethods.togglePictureInPicture(); } break; } }; // Only listen when controls are enabled document.addEventListener('keydown', handleKeyPress); return () => document.removeEventListener('keydown', handleKeyPress); }, [playerMethods, playerState, isClient, skipIntervals, enableKeyboardControls, isMobileDevice]); // Memoize progress calculations BEFORE early return to maintain hook order const progressPercentage = useMemo(() => { return playerState.duration ? (playerState.currentTime / playerState.duration) * 100 : 0; }, [playerState.currentTime, playerState.duration]); const bufferedPercentage = useMemo(() => { return playerState.buffered && playerState.buffered.length > 0 && playerState.duration ? (playerState.buffered.end(playerState.buffered.length - 1) / playerState.duration) * 100 : 0; }, [playerState.buffered, playerState.duration]); // Function to show controls (can be called from parent) const showControls = useCallback(() => { setIsVisible(true); if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } // Always set hide timeout when showing controls, regardless of play state // This gives user time to interact with controls if (autoHide) { hideTimeoutRef.current = setTimeout(() => { setIsVisible(false); }, autoHideDelay); } }, [autoHide, autoHideDelay]); // Show controls on interaction const handleInteraction = useCallback(() => { if (showOnInteraction) { showControls(); } }, [showOnInteraction, showControls]); // Expose methods to parent useImperativeHandle(ref, () => ({ showControls }), [showControls]); // Check for Picture-in-Picture support and video readiness on client only const [isPictureInPictureSupported, setIsPictureInPictureSupported] = useState(false); const [isPictureInPictureReady, setIsPictureInPictureReady] = useState(false); useEffect(() => { if (isClient) { setIsPictureInPictureSupported(document.pictureInPictureEnabled || false); // Check if video is ready for PiP (has metadata and valid duration) const isVideoReady = playerState.duration > 0 && !isNaN(playerState.duration) && !playerState.isLoading; setIsPictureInPictureReady(isVideoReady); } }, [isClient, playerState.duration, playerState.isLoading]); // Build CSS classes const controlsClasses = [ styles$2.controls, styles$2[`controls${position.charAt(0).toUpperCase() + position.slice(1)}`], // controlsBottom, controlsTop, etc. styles$2[`theme${theme.charAt(0).toUpperCase() + theme.slice(1)}`], // themeDark, themeLight, etc. isVisible ? styles$2.controlsVisible : styles$2.controlsHidden, compact ? styles$2.compact : '', className ].filter(Boolean).join(' '); return (jsxs("div", { ref: controlsRef, className: controlsClasses, onMouseMove: handleInteraction, onMouseEnter: handleInteraction, onClick: handleInteraction, ...(enableKeyboardControls ? { tabIndex: 0, style: { outline: 'none' }, 'aria-label': 'Video controls (click to enable keyboard shortcuts)' } : {}), children: [jsx("button", { className: styles$2.button, onClick: () => { if (playerState.isPlaying) { playerMethods?.pause(); } else { playerMethods?.play(); } }, "aria-label": playerState.isPlaying ? 'Pause' : 'Play', children: playerState.isPlaying ? (jsx(PauseIcon, { size: 18, color: "currentColor" })) : (jsx(PlayIcon, { size: 18, color: "currentColor" })) }), showTimeDisplay && (jsx("span", { className: styles$2.timeDisplay, children: formatTime(playerState.currentTime) })), showProgressBar && (jsxs("div", { ref: progressBarRef, className: styles$2.progressContainer, onClick: handleProgressClick, onMouseMove: handleProgressDrag, onMouseDown: () => setIsDragging(true), onMouseUp: () => setIsDragging(false), onMouseLeave: () => setIsDragging(false), children: [jsx("div", { className: `${styles$2.progressBackground} ${theme === 'light' ? styles$2.progressBackgroundLight : styles$2.progressBackgroundDark}` }), jsx("div", { className: `${styles$2.progressBuffered} ${theme === 'light' ? styles$2.progressBufferedLight : styles$2.progressBufferedDark}`, style: { width: `${bufferedPercentage}%` } }), jsx("div", { className: `${styles$2.progressFill} ${theme === 'light' ? styles$2.progressFillLight : styles$2.progressFillDark} ${isDragging ? styles$2.progressFillDragging : styles$2.progressFillSmooth}`, style: { width: `${progressPercentage}%` } })] })), showTimeDisplay && (jsx("span", { className: styles$2.timeDisplay, children: formatTime(playerState.duration) })), showVolumeControl && (jsxs(Fragment, { children: [jsx("button", { className: styles$2.button, onClick: () => playerMethods?.setMuted(!playerState.isMuted), "aria-label": playerState.isMuted ? 'Unmute' : 'Mute', children: jsx(VolumeIcon, { level: playerState.volume, isMuted: playerState.isMuted, size: 18, color: "currentColor" }) }), jsxs("div", { className: styles$2.volumeSlider, children: [jsx("div", { className: `${styles$2.volumeBackground} ${theme === 'light' ? styles$2.volumeBackgroundLight : styles$2.volumeBackgroundDark}` }), jsx("div", { className: `${styles$2.volumeFill} ${theme === 'light' ? styles$2.volumeFillLight : styles$2.volumeFillDark}`, style: { width: `${playerState.isMuted ? 0 : playerState.volume * 100}%` } })] })] })), customButtons, showPictureInPicture && isPictureInPictureSupported && (jsx("button", { className: `${styles$2.button} ${!isPictureInPictureReady ? styles$2.buttonDisabled : ''}`, onClick: () => { if (isPictureInPictureReady && playerMethods?.togglePictureInPicture) { playerMethods.togglePictureInPicture(); } }, disabled: !isPictureInPictureReady, "aria-label": !isPictureInPictureReady ? 'Picture-in-Picture (Video loading...)' : playerState.isPictureInPicture ? 'Exit Picture-in-Picture' : 'Picture-in-Picture', title: !isPictureInPictureReady ? 'Picture-in-Picture will be available when video loads' : undefined, children: jsx(PictureInPictureIcon, { isPictureInPicture: playerState.isPictureInPicture, size: 18, color: !isPictureInPictureReady ? "rgba(255,255,255,0.5)" : "currentColor" }) })), showFullscreen && (jsx("button", { className: styles$2.button, onClick: () => { // Use mobile fullscreen for iOS Safari to enable webkitEnterFullscreen if (isMobileDevice) { playerMethods?.toggleMobileFullscreen?.(); } else { playerMethods?.toggleFullscreen(); } }, "aria-label": playerState.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen', children: jsx(FullscreenIcon, { isFullscreen: playerState.isFullscreen, size: 18, color: "currentColor" }) }))] })); }; W1VideoControls.displayName = 'W1VideoControls'; var css_248z$1 = ".videoplayer-module_videoPlayerWrapper__m0lbB{align-items:center;background-color:#000;display:flex;height:100%;justify-content:center;overflow:hidden;position:relative;width:100%}.videoplayer-module_videoElement__p-VaC{display:block;height:100%;margin:0 auto;width:100%}.videoplayer-module_cover__m-Uhl{object-fit:cover}.videoplayer-module_contain__pOLIZ{object-fit:contain}.videoplayer-module_externalPlayerContainer__laudH{align-items:center!important;display:flex!important;height:100%;justify-content:center!important;left:0;position:absolute;top:0;width:100%;z-index:2}.videoplayer-module_externalPlayerContainer__laudH iframe,.videoplayer-module_externalPlayerContainer__laudH>div{height:100%!important;left:50%!important;margin:0!important;min-height:100%!important;min-width:100%!important;padding:0!important;position:absolute!important;top:50%!important;transform:translate(-50%,-50%)!important;width:100%!important}.videoplayer-module_externalPlayerContainer__laudH>div>iframe{height:100%!important;left:0!important;margin:0!important;padding:0!important;position:absolute!important;top:0!important;width:100%!important}.videoplayer-module_externalPlayerContainer__laudH [style*=padding-bottom],.videoplayer-module_externalPlayerContainer__laudH [style*=padding-top]{padding:0!important}.videoplayer-module_externalPlayerContainer__laudH div[style*=\"position: relative\"],.videoplayer-module_externalPlayerContainer__laudH div[style*=\"position:relative\"],.videoplayer-module_externalPlayerContainer__laudH>div[style]{height:100%!important;left:50%!important;position:absolute!important;top:50%!important;transform:translate(-50%,-50%)!important;width:100%!important}.videoplayer-module_externalPlayerContainer__laudH [data-vimeo-id],.videoplayer-module_externalPlayerContainer__laudH div[style*=vimeo],.videoplayer-module_externalPlayerContainer__laudH iframe,.videoplayer-module_externalPlayerContainer__laudH iframe[src*=vimeo],.videoplayer-module_externalPlayerContainer__laudH>div>div[style],.videoplayer-module_externalPlayerContainer__laudH>div>iframe,.videoplayer-module_externalPlayerContainer__laudH>div[style]{height:100%!important;left:50%!important;margin:0!important;padding:0!important;position:absolute!important;top:50%!important;transform:translate(-50%,-50%)!important;width:100%!important}.videoplayer-module_externalPlayerContainer__laudH iframe[src*=\"player.vimeo.com\"],.videoplayer-module_externalPlayerContainer__laudH iframe[src*=vimeo]{height:100%!important;left:50%!important;position:absolute!important;top:50%!important;transform:translate(-50%,-50%)!important;width:100%!important}.videoplayer-module_externalPlayerContainer__laudH>div:has(iframe[src*=vimeo]){height:100%!important;left:50%!important;position:absolute!important;top:50%!important;transform:translate(-50%,-50%)!important;width:100%!important}.videoplayer-module_videoElementHidden__nDQys{display:none!important}.videoplayer-module_loading__l52cL{opacity:.5}.videoplayer-module_error__0Oee6{background:rgba(255,0,0,.1)}.videoplayer-module_fullscreen__y3A77{background:#000;height:100vh!important;left:0!important;position:fixed!important;top:0!important;width:100vw!important;z-index:999999!important}@media (max-width:768px){.videoplayer-module_videoPlayerWrapper__m0lbB{-webkit-tap-highlight-color:transparent}}.videoplayer-module_videoElement__p-VaC{backface-visibility:hidden;will-change:transform}"; var styles$1 = {"videoPlayerWrapper":"videoplayer-module_videoPlayerWrapper__m0lbB","videoElement":"videoplayer-module_videoElement__p-VaC","cover":"videoplayer-module_cover__m-Uhl","contain":"videoplayer-module_contain__pOLIZ","externalPlayerContainer":"videoplayer-module_externalPlayerContainer__laudH","videoElementHidden":"videoplayer-module_videoElementHidden__nDQys","loading":"videoplayer-module_loading__l52cL","error":"videoplayer-module_error__0Oee6","fullscreen":"videoplayer-module_fullscreen__y3A77"}; styleInject(css_248z$1); const useFullscreenAndPiP = ({ videoRef, _wrapperRef, isClient, updateState, onPictureInPictureChange }) => { const isFullscreenRef = useRef(false); // Use refs for callbacks to avoid dependency issues const updateStateRef = useRef(updateState); const onPictureInPictureChangeRef = useRef(onPictureInPictureChange); // Update callback refs when they change useEffect(() => { updateStateRef.current = updateState; onPictureInPictureChangeRef.current = onPictureInPictureChange; }, [updateState, onPictureInPictureChange]); // Set up fullscreen event listeners (exact copy from original) useEffect(() => { if (!isClient) return; // Capture current video element for cleanup const currentVideo = videoRef.current; const setFullscreenState = (newState) => { isFullscreenRef.current = newState; updateStateRef.current({ isFullscreen: newState }); }; const handleFullscreenChange = () => { const doc = document; const isFullscreen = !!(doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement || doc.webkitCurrentFullScreenElement); setFullscreenState(isFullscreen); }; const handleVideoFullscreenChange = () => { const video = videoRef.current; if (!video) return; const isVideoFullscreen = !!(video.webkitDisplayingFullscreen || video.displayingFullscreen); if (isVideoFullscreen !== isFullscreenRef.current) { setFullscreenState(isVideoFullscreen); } }; // Standard fullscreen events document.addEventListener("fullscreenchange", handleFullscreenChange); document.addEventListener("webkitfullscreenchange", handleFullscreenChange); document.addEventListener("mozfullscreenchange", handleFullscreenChange); document.addEventListener("MSFullscreenChange", handleFullscreenChange); // iOS Safari fullscreen events if (currentVideo) { currentVideo.addEventListener("webkitbeginfullscreen", handleVideoFullscreenChange); currentVideo.addEventListener("webkitendfullscreen", handleVideoFullscreenChange); currentVideo.addEventListener("webkitfullscreenchange", handleVideoFullscreenChange); } // Check initial fullscreen state handleFullscreenChange(); return () => { document.removeEventListener("fullscreenchange", handleFullscreenChange); document.removeEventListener("webkitfullscreenchange", handleFullscreenChange); document.removeEventListener("mozfullscreenchange", handleFullscreenChange); document.removeEventListener("MSFullscreenChange", handleFullscreenChange); // Use captured video element for cleanup if (currentVideo) { currentVideo.removeEventListener("webkitbeginfullscreen", handleVideoFullscreenChange); currentVideo.removeEventListener("webkitendfullscreen", handleVideoFullscreenChange); currentVideo.removeEventListener("webkitfullscreenchange", handleVideoFullscreenChange); } }; }, [isClient, videoRef]); // Removed callback dependencies // Set up picture-in-picture event listeners - FIXED: Listen on video element, not document useEffect(() => { if (!isClient) return; // Capture current video element for cleanup const currentVideo = videoRef.current; if (!currentVideo) return; const handlePictureInPictureEnter = () => { updateStateRef.current({ isPictureInPicture: true }); onPictureInPictureChangeRef.current?.(true); }; const handlePictureInPictureLeave = () => { updateStateRef.current({ isPictureInPicture: false }); onPictureInPictureChangeRef.current?.(false); }; // Listen on the video element itself, not document currentVideo.addEventListener("enterpictureinpicture", handlePictureInPictureEnter); currentVideo.addEventListener("leavepictureinpicture", handlePictureInPictureLeave); // Check initial PiP state const doc = document; const currentPiPElement = doc.pictureInPictureElement || doc.webkitPictureInPictureElement; const isPiP = currentPiPElement === currentVideo; updateStateRef.current({ isPictureInPicture: isPiP }); onPictureInPictureChangeRef.current?.(isPiP); return () => { currentVideo.removeEventListener("enterpictureinpicture", handlePictureInPictureEnter); currentVideo.removeEventListener("leavepictureinpicture", handlePictureInPictureLeave); }; }, [isClient, videoRef]); // Removed callback dependencies return { isFullscreenRef, }; }; /** * Video Format Detection Utilities * Automatically detects video format based on URL patterns and file extensions */ /** * Detects video format from URL */ function detectVideoFormat(src) { if (!src) return "progressive"; const url = src.toLowerCase(); // YouTube detection if (url.includes("youtube.com/watch") || url.includes("youtu.be/") || url.includes("youtube.com/embed/")) { return "youtube"; } // Vimeo detection if (url.includes("vimeo.com/") || url.includes("player.vimeo.com/")) { return "vimeo"; } // HLS detection if (url.includes(".m3u8")) { return "hls"; } // DASH detection if (url.includes(".mpd")) { return "dash"; } // Default to progressive (MP4, WebM, etc.) return "progressive"; } /** * Extracts YouTube video ID from various YouTube URL formats */ function extractYouTubeId(url) { const patterns = [/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, /youtube\.com\/v\/([^&\n?#]+)/]; for (const pattern of patterns) { const match = url.match(pattern); if (match && match[1]) { return match[1]; } } return null; } /** * Extracts Vimeo video ID from various Vimeo URL formats */ function extractVimeoId(url) { const patterns = [/vimeo\.com\/(?:.*\/)?(\d+)/, /player\.vimeo\.com\/video\/(\d+)/]; for (const pattern of patterns) { const match = url.match(pattern); if (match && match[1]) { return match[1]; } } return null; } /** * DASH Platform Integration * Handles Dash.js library loading and DASH stream management */ /** * Loads the Dash.js library */ async function loadDashLibrary() { return new Promise((resolve) => { // Check if already loaded if (window.dashjs) { resolve(true); return; } // Check if script is already loading if (document.querySelector('script[src*="dash.js"]')) { // Script is loading, wait for it const checkLoaded = () => { if (window.dashjs) { resolve(true); } else { setTimeout(checkLoaded, 100); } }; checkLoaded(); return; } // Load the script const script = document.createElement("script"); script.src = "https://cdn.dashjs.org/latest/dash.all.min.js"; script.async = true; script.onload = () => { if (window.dashjs) { resolve(true); } else { resolve(false); } }; script.onerror = () => { resolve(false); }; document.head.appendChild(script); }); } /** * Sets up DASH playback */ function setupDashPlayback(video, src) { try { if (!window.dashjs || !window.dashjs.MediaPlayer) { console.error("Dash.js not loaded"); return null; } const player = window.dashjs.MediaPlayer(); player.initialize(video, src, true); // Handle errors player.on("error", (event) => { console.error("DASH error:", event); }); return player; } catch (error) { console.error("DASH setup error:", error); return null; } } /** * HLS Platform Integration * Handles HLS.js library loading and HLS stream management */ /** * Checks if HLS is natively supported by the browser */ function isHLSNativelySupported() { const video = document.createElement("video"); return video.canPlayType("application/vnd.apple.mpegurl") !== ""; } /** * Loads the HLS.js library */ async function loadHLSLibrary() { return new Promise((resolve) => { // Check if already loaded if (window.Hls && window.Hls.isSupported()) { resolve(true); return; } // Check if script is already loading if (document.querySelector('script[src*="hls.js"]')) { // Script is loading, wait for it const checkLoaded = () => { if (window.Hls && window.Hls.isSupported()) { resolve(true); } else { setTimeout(checkLoaded, 100); } }; checkLoaded(); return; } // Load the script const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.async = true; script.onload = () => { if (window.Hls && window.Hls.isSupported()) { resolve(true); } else { resolve(false); } }; script.onerror = () => { resolve(false); }; document.head.appendChild(script); }); } /** * Sets up HLS playback */ function setupHLSPlayback(video, src) { try { if (!window.Hls || !window.Hls.isSupported()) { console.error("HLS.js not supported"); return null; } const hls = new window.Hls(); // Load the source hls.loadSource(src); hls.attachMedia(video); // Handle errors hls.on(window.Hls.Events.ERROR, (event, data) => { if (data.fatal) { switch (data.type) { case window.Hls?.ErrorTypes.NETWORK_ERROR: console.error("HLS network error, trying to recover..."); hls.startLoad(); break; case window.Hls?.ErrorTypes.MEDIA_ERROR: console.error("HLS media error, trying to recover..."); hls.recoverMediaError(); break; default: console.error("HLS fatal error, cannot recover:", data); hls.destroy(); break; } } }); return hls; } catch (error) { console.error("HLS setup error:", error); return null; } } /** * Vimeo Platform Integration * Handles Vimeo Player API loading and player management */ /** * Loads the Vimeo Player API */ async function loadVimeoAPI() { return new Promise((resolve) => { // Check if already loaded if (window.Vimeo && window.Vimeo.Player) { resolve(true); return; } // Check if script is already loading if (document.querySelector('script[src*="player.vimeo.com/api/player.js"]')) { // Script is loading, wait for it const checkLoaded = () => { if (window.Vimeo && window.Vimeo.Player) { resolve(true); } else { setTimeout(checkLoaded, 100); } }; checkLoaded(); return; } // Load the script const script = document.createElement("script"); script.src = "https://player.vimeo.com/api/player.js"; script.async = true; script.onload = () => { resolve(true); }; script.onerror = () => { resolve(false); }; document.head.appendChild(script); }); } /** * Sets up Vimeo player */ function setupVimeoPlayback(container, videoId, options = {}) { try { if (!window.Vimeo || !window.Vimeo.Player) { console.error("Vimeo API not loaded"); return null; } // Clear container container.innerHTML = ""; // Create player div const playerDiv = document.createElement("div"); container.appendChild(playerDiv); const player = new window.Vimeo.Player(playerDiv, { id: videoId, width: container.offsetWidth || 640, height: container.offsetHeight || 360, autoplay: options.autoplay || false, controls: options.controls !== false, loop: options.loop || false, muted: options.muted || false, playsinline: true, responsive: true, dnt: true, // Do not track byline: false, portrait: false, title: false, }); // Set up event listeners if (options.onReady) { player.on("loaded", options.onReady); } if (options.onPlay) { player.on("play", options.onPlay); } if (options.onPause) { player.on("pause", options.onPause); } if (options.onEnded) { player.on("ended", options.onEnded); } if (options.onError) { player.on("error", options.onError); } return player; } catch (error) { console.error("Vimeo player setup error:", error); return null; } } /** * YouTube Platform Integration * Handles YouTube IFrame API loading and player management */ /** * Loads the YouTube IFrame API */ async function loadYouTubeAPI() { return new Promise((resolve) => { // Check if already loaded if (window.YT && window.YT.Player) { resolve(true); return; } // Check if script is already loading if (document.querySelector('script[src*="youtube.com/iframe_api"]')) { // Script is loading, wait for it const checkLoaded = () => { if (window.YT && window.YT.Player) { resolve(true); } else { setTimeout(checkLoaded, 100); } }; checkLoaded(); return; } // Load the script const script = document.createElement("script"); script.src = "https://www.youtube.com/iframe_api"; script.async = true; window.onYouTubeIframeAPIReady = () => { resolve(true); }; script.onerror = () => { resolve(false); }; document.head.appendChild(script); }); } /** * Sets up YouTube player */ function setupYouTubePlayback(container, videoId, options = {}) { try { if (!window.YT || !window.YT.Player) { console.error("YouTube API not loaded"); return null; } // Clear container container.innerHTML = ""; // Create player div const playerDiv = document.createElement("div"); playerDiv.id = `youtube-player-${Date.now()}`; container.appendChild(playerDiv); const player = new window.YT.Player(playerDiv, { videoId, width: "100%", height: "100%", playerVars: { autoplay: options.autoplay ? 1 : 0, controls: options.controls ? 1 : 0, loop: options.loop ? 1 : 0, mute: options.muted ? 1 : 0, rel: 0, // Show only same-channel videos (best we can do - YouTube removed full disable in 2018) modestbranding: 1, // Remove most YouTube branding playsinline: 1, // Play inline on mobile fs: 1, // Allow fullscreen cc_load_policy: 0, // Disable closed captions by default iv_load_policy: 3, // Disable video annotations disablekb: 0, // Keep keyboard controls enabled showinfo: 0, // Hide video info (deprecated but some browsers still respect it) }, events: { onReady: options.onReady, onStateChange: options.onStateChange, onEr