@werk1/w1-system-videoblock
Version:
Universal video player supporting YouTube, Vimeo, HLS, DASH with coordination and GSAP integration for W1 System
849 lines (829 loc) • 120 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('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 (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Play", role: "img", children: jsxRuntime.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 (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Pause", role: "img", children: [jsxRuntime.jsx("rect", { x: "6", y: "4", width: "4", height: "16", fill: color, rx: "1" }), jsxRuntime.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 (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Muted", role: "img", children: [jsxRuntime.jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsxRuntime.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 (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Low volume", role: "img", children: [jsxRuntime.jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsxRuntime.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 (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "High volume", role: "img", children: [jsxRuntime.jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z", fill: color }), jsxRuntime.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 }), jsxRuntime.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 (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Exit fullscreen", role: "img", children: jsxRuntime.jsx("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z", fill: color }) }));
}
else {
// Enter fullscreen icon
return (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Enter fullscreen", role: "img", children: jsxRuntime.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 (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Exit picture-in-picture", role: "img", children: [jsxRuntime.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 }), jsxRuntime.jsx("path", { d: "M16 12h-5v4h5v-4z", fill: "none", stroke: color, strokeWidth: "1" })] }));
}
else {
// Enter picture-in-picture icon
return (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className: className, "aria-label": "Enter picture-in-picture", role: "img", children: [jsxRuntime.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 }), jsxRuntime.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] = React.useState(false);
const [isVisible, setIsVisible] = React.useState(initiallyVisible);
const [isDragging, setIsDragging] = React.useState(false);
const hideTimeoutRef = React.useRef(null);
const progressBarRef = React.useRef(null);
const controlsRef = React.useRef(null);
// Set client flag
React.useEffect(() => {
setIsClient(true);
}, []);
// Auto-hide logic (only on client)
React.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 = React.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 = React.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 = React.useCallback((e) => {
if (!isDragging)
return;
handleProgressClick(e);
}, [isDragging, handleProgressClick]);
// Keyboard shortcuts (only on client and when enabled)
React.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 = React.useMemo(() => {
return playerState.duration ? (playerState.currentTime / playerState.duration) * 100 : 0;
}, [playerState.currentTime, playerState.duration]);
const bufferedPercentage = React.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 = React.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 = React.useCallback(() => {
if (showOnInteraction) {
showControls();
}
}, [showOnInteraction, showControls]);
// Expose methods to parent
React.useImperativeHandle(ref, () => ({
showControls
}), [showControls]);
// Check for Picture-in-Picture support and video readiness on client only
const [isPictureInPictureSupported, setIsPictureInPictureSupported] = React.useState(false);
const [isPictureInPictureReady, setIsPictureInPictureReady] = React.useState(false);
React.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 (jsxRuntime.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: [jsxRuntime.jsx("button", { className: styles$2.button, onClick: () => {
if (playerState.isPlaying) {
playerMethods?.pause();
}
else {
playerMethods?.play();
}
}, "aria-label": playerState.isPlaying ? 'Pause' : 'Play', children: playerState.isPlaying ? (jsxRuntime.jsx(PauseIcon, { size: 18, color: "currentColor" })) : (jsxRuntime.jsx(PlayIcon, { size: 18, color: "currentColor" })) }), showTimeDisplay && (jsxRuntime.jsx("span", { className: styles$2.timeDisplay, children: formatTime(playerState.currentTime) })), showProgressBar && (jsxRuntime.jsxs("div", { ref: progressBarRef, className: styles$2.progressContainer, onClick: handleProgressClick, onMouseMove: handleProgressDrag, onMouseDown: () => setIsDragging(true), onMouseUp: () => setIsDragging(false), onMouseLeave: () => setIsDragging(false), children: [jsxRuntime.jsx("div", { className: `${styles$2.progressBackground} ${theme === 'light' ? styles$2.progressBackgroundLight : styles$2.progressBackgroundDark}` }), jsxRuntime.jsx("div", { className: `${styles$2.progressBuffered} ${theme === 'light' ? styles$2.progressBufferedLight : styles$2.progressBufferedDark}`, style: { width: `${bufferedPercentage}%` } }), jsxRuntime.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 && (jsxRuntime.jsx("span", { className: styles$2.timeDisplay, children: formatTime(playerState.duration) })), showVolumeControl && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { className: styles$2.button, onClick: () => playerMethods?.setMuted(!playerState.isMuted), "aria-label": playerState.isMuted ? 'Unmute' : 'Mute', children: jsxRuntime.jsx(VolumeIcon, { level: playerState.volume, isMuted: playerState.isMuted, size: 18, color: "currentColor" }) }), jsxRuntime.jsxs("div", { className: styles$2.volumeSlider, children: [jsxRuntime.jsx("div", { className: `${styles$2.volumeBackground} ${theme === 'light' ? styles$2.volumeBackgroundLight : styles$2.volumeBackgroundDark}` }), jsxRuntime.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 && (jsxRuntime.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: jsxRuntime.jsx(PictureInPictureIcon, { isPictureInPicture: playerState.isPictureInPicture, size: 18, color: !isPictureInPictureReady ? "rgba(255,255,255,0.5)" : "currentColor" }) })), showFullscreen && (jsxRuntime.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: jsxRuntime.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 = React.useRef(false);
// Use refs for callbacks to avoid dependency issues
const updateStateRef = React.useRef(updateState);
const onPictureInPictureChangeRef = React.useRef(onPictureInPictureChange);
// Update callback refs when they change
React.useEffect(() => {
updateStateRef.current = updateState;
onPictureInPictureChangeRef.current = onPictureInPictureChange;
}, [updateState, onPictureInPictureChange]);
// Set up fullscreen event listeners (exact copy from original)
React.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
React.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)
modestb