@scarlett-player/embed 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hls2.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"hls2.js","sources":["../../plugins/ui/dist/index.js"],"sourcesContent":["// src/styles.ts\nvar styles = `\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n/* Chapter markers */\n.sp-progress__markers {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.sp-progress__marker {\n position: absolute;\n top: 0;\n width: 2px;\n height: 100%;\n margin-left: -1px;\n background: rgba(0, 0, 0, 0.65);\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.sp-progress__tooltip-chapter {\n display: block;\n max-width: 220px;\n overflow: hidden;\n color: rgba(255, 255, 255, 0.75);\n font-weight: 400;\n font-variant-numeric: normal;\n text-overflow: ellipsis;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n`;\n\n// src/icons.ts\nvar icons = {\n play: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>`,\n pause: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\"/></svg>`,\n replay: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z\"/></svg>`,\n volumeHigh: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\"/></svg>`,\n volumeLow: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z\"/></svg>`,\n volumeMute: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\"/></svg>`,\n fullscreen: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/></svg>`,\n exitFullscreen: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"/></svg>`,\n pip: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M19 7h-8v6h8V7zm2-4H3c-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\"/></svg>`,\n exitPip: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><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 16H3V5h18v14zM9 9h6v2H9z\"/></svg>`,\n settings: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z\"/></svg>`,\n chromecast: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z\"/></svg>`,\n chromecastConnected: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z\"/></svg>`,\n airplay: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M6 22h12l-6-6-6 6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z\"/></svg>`,\n captions: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z\"/></svg>`,\n captionsOff: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M19.5 5.5v13h-15v-13h15zM19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2z\"/></svg>`,\n checkmark: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg>`,\n chevronUp: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z\"/></svg>`,\n chevronDown: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z\"/></svg>`,\n spinner: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" class=\"sp-spin\"><path d=\"M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z\"/></svg>`,\n skipForward: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z\"/></svg>`,\n skipBack: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z\"/></svg>`,\n forward10: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z\"/><path d=\"M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z\"/></svg>`,\n replay10: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z\"/><path d=\"M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z\"/></svg>`,\n error: `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\"/></svg>`\n};\n\n// src/utils/dom.ts\nfunction createElement(tag, attrs, children) {\n const el = document.createElement(tag);\n if (attrs) {\n for (const [key, value] of Object.entries(attrs)) {\n if (key === \"className\") {\n el.className = value;\n } else {\n el.setAttribute(key, value);\n }\n }\n }\n if (children) {\n for (const child of children) {\n if (typeof child === \"string\") {\n el.appendChild(document.createTextNode(child));\n } else {\n el.appendChild(child);\n }\n }\n }\n return el;\n}\nfunction createButton(className, label, icon) {\n const btn = createElement(\"button\", {\n className: `sp-control ${className}`,\n \"aria-label\": label,\n type: \"button\"\n });\n setHTML(btn, icon);\n return btn;\n}\nfunction getVideo(container) {\n return container.querySelector(\"video\");\n}\nvar lastHTML = /* @__PURE__ */ new WeakMap();\nfunction setHTML(el, html) {\n if (lastHTML.get(el) === html) return false;\n el.innerHTML = html;\n lastHTML.set(el, html);\n return true;\n}\nfunction setAttr(el, name, value) {\n if (el.getAttribute(name) === value) return false;\n el.setAttribute(name, value);\n return true;\n}\n\n// src/utils/format.ts\nfunction formatTime(seconds) {\n if (!isFinite(seconds) || isNaN(seconds)) {\n return \"0:00\";\n }\n const absSeconds = Math.abs(seconds);\n const h = Math.floor(absSeconds / 3600);\n const m = Math.floor(absSeconds % 3600 / 60);\n const s = Math.floor(absSeconds % 60);\n const sign = seconds < 0 ? \"-\" : \"\";\n if (h > 0) {\n return `${sign}${h}:${pad(m)}:${pad(s)}`;\n }\n return `${sign}${m}:${pad(s)}`;\n}\nfunction pad(n) {\n return n < 10 ? `0${n}` : `${n}`;\n}\nfunction formatLiveTime(behindLive) {\n if (behindLive <= 0) {\n return \"LIVE\";\n }\n return `-${formatTime(behindLive)}`;\n}\n\n// src/controls/PlayButton.ts\nvar PlayButton = class {\n constructor(api) {\n this.clickHandler = () => {\n this.toggle();\n };\n this.api = api;\n this.el = createButton(\"sp-play\", \"Play\", icons.play);\n this.el.addEventListener(\"click\", this.clickHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const playing = this.api.getState(\"playing\");\n const ended = this.api.getState(\"ended\");\n let icon;\n let label;\n if (ended) {\n icon = icons.replay;\n label = \"Replay\";\n } else if (playing) {\n icon = icons.pause;\n label = \"Pause\";\n } else {\n icon = icons.play;\n label = \"Play\";\n }\n setHTML(this.el, icon);\n setAttr(this.el, \"aria-label\", label);\n }\n toggle() {\n const video = getVideo(this.api.container);\n if (!video) return;\n const ended = this.api.getState(\"ended\");\n if (ended) {\n video.currentTime = 0;\n video.play().catch(() => {\n });\n } else if (!video.paused) {\n video.pause();\n } else {\n video.play().catch(() => {\n });\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.clickHandler);\n this.el.remove();\n }\n};\n\n// src/controls/ThumbnailPreview.ts\nvar ThumbnailPreview = class {\n constructor() {\n this.config = null;\n this.loaded = false;\n this.el = createElement(\"div\", { className: \"sp-thumbnail-preview\" });\n this.img = createElement(\"div\", { className: \"sp-thumbnail-preview__img\" });\n this.el.appendChild(this.img);\n }\n getElement() {\n return this.el;\n }\n setConfig(config) {\n this.config = config;\n this.loaded = false;\n if (config) {\n this.img.style.width = `${config.width}px`;\n this.img.style.height = `${config.height}px`;\n this.el.style.width = `${config.width}px`;\n this.el.style.height = `${config.height}px`;\n const preload = new Image();\n preload.onload = () => {\n this.loaded = true;\n };\n preload.onerror = () => {\n this.config = null;\n this.loaded = false;\n };\n preload.src = config.src;\n }\n }\n /**\n * Update the thumbnail to show the frame at the given time.\n * @param time Time in seconds\n * @param percent Position as 0-1 fraction (for horizontal positioning)\n */\n show(time, percent) {\n if (!this.config || !this.loaded) {\n this.el.style.display = \"none\";\n return;\n }\n const { src, width, height, columns, interval } = this.config;\n const index = Math.floor(time / interval);\n const col = index % columns;\n const row = Math.floor(index / columns);\n this.img.style.backgroundImage = `url(${src})`;\n this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;\n this.img.style.backgroundSize = `${columns * width}px auto`;\n this.img.style.width = `${width}px`;\n this.img.style.height = `${height}px`;\n this.el.style.left = `${percent * 100}%`;\n this.el.style.display = \"\";\n }\n hide() {\n this.el.style.display = \"none\";\n }\n isConfigured() {\n return this.config !== null;\n }\n destroy() {\n this.el.remove();\n }\n};\n\n// src/controls/ProgressBar.ts\nvar ProgressBar = class {\n constructor(api) {\n this.isDragging = false;\n this.lastSeekTime = 0;\n this.seekThrottleMs = 100;\n // Throttle seeks to max 10/sec\n this.wasPlayingBeforeDrag = false;\n /** Chapter list the marker layer was last built from, to avoid rebuilding every frame. */\n this.renderedChapters = null;\n /** Duration the marker layer was last built against, since positions are a percentage of it. */\n this.renderedDuration = 0;\n this.onMouseDown = (e) => {\n e.preventDefault();\n const video = getVideo(this.api.container);\n this.wasPlayingBeforeDrag = video ? !video.paused : false;\n this.isDragging = true;\n this.el.classList.add(\"sp-progress--dragging\");\n this.lastSeekTime = 0;\n this.seek(e.clientX, true);\n };\n this.onDocMouseMove = (e) => {\n if (this.isDragging) {\n this.seek(e.clientX);\n this.updateVisualPosition(e.clientX);\n }\n };\n this.onMouseUp = (e) => {\n if (this.isDragging) {\n this.seek(e.clientX, true);\n this.isDragging = false;\n this.el.classList.remove(\"sp-progress--dragging\");\n if (this.wasPlayingBeforeDrag) {\n const video = getVideo(this.api.container);\n if (video && video.paused) {\n const resumePlayback = () => {\n video.removeEventListener(\"seeked\", resumePlayback);\n video.play().catch(() => {\n });\n };\n video.addEventListener(\"seeked\", resumePlayback);\n }\n }\n }\n };\n this.onTouchStart = (e) => {\n e.preventDefault();\n const video = getVideo(this.api.container);\n this.wasPlayingBeforeDrag = video ? !video.paused : false;\n this.isDragging = true;\n this.el.classList.add(\"sp-progress--dragging\");\n this.lastSeekTime = 0;\n this.seek(e.touches[0].clientX, true);\n };\n this.onDocTouchMove = (e) => {\n if (this.isDragging) {\n e.preventDefault();\n this.seek(e.touches[0].clientX);\n this.updateVisualPosition(e.touches[0].clientX);\n }\n };\n this.onTouchEnd = (e) => {\n if (this.isDragging) {\n const clientX = e.changedTouches?.[0]?.clientX;\n if (clientX !== void 0) {\n this.seek(clientX, true);\n }\n this.isDragging = false;\n this.el.classList.remove(\"sp-progress--dragging\");\n if (this.wasPlayingBeforeDrag) {\n const video = getVideo(this.api.container);\n if (video && video.paused) {\n const resumePlayback = () => {\n video.removeEventListener(\"seeked\", resumePlayback);\n video.play().catch(() => {\n });\n };\n video.addEventListener(\"seeked\", resumePlayback);\n }\n }\n this.tooltip.style.opacity = \"0\";\n this.thumbnailPreview.hide();\n }\n };\n this.onMouseMove = (e) => {\n this.updateTooltip(e.clientX);\n };\n this.onMouseLeave = () => {\n if (!this.isDragging) {\n this.tooltip.style.opacity = \"0\";\n this.thumbnailPreview.hide();\n }\n };\n this.onKeyDown = (e) => {\n const video = getVideo(this.api.container);\n if (!video) return;\n const step = 5;\n const live = this.api.getState(\"live\");\n const seekableRange = this.api.getState(\"seekableRange\");\n if (live && seekableRange) {\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n video.currentTime = Math.max(seekableRange.start, video.currentTime - step);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n video.currentTime = Math.min(seekableRange.end, video.currentTime + step);\n break;\n case \"Home\":\n e.preventDefault();\n video.currentTime = seekableRange.start;\n break;\n case \"End\":\n e.preventDefault();\n video.currentTime = seekableRange.end;\n break;\n }\n } else {\n const duration = this.api.getState(\"duration\") || 0;\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n video.currentTime = Math.max(0, video.currentTime - step);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n video.currentTime = Math.min(duration, video.currentTime + step);\n break;\n case \"Home\":\n e.preventDefault();\n video.currentTime = 0;\n break;\n case \"End\":\n e.preventDefault();\n video.currentTime = duration;\n break;\n }\n }\n };\n this.api = api;\n this.wrapper = createElement(\"div\", { className: \"sp-progress-wrapper\" });\n this.el = createElement(\"div\", { className: \"sp-progress\" });\n const track = createElement(\"div\", { className: \"sp-progress__track\" });\n this.buffered = createElement(\"div\", { className: \"sp-progress__buffered\" });\n this.filled = createElement(\"div\", { className: \"sp-progress__filled\" });\n this.markers = createElement(\"div\", { className: \"sp-progress__markers\" });\n this.handle = createElement(\"div\", { className: \"sp-progress__handle\" });\n this.tooltip = createElement(\"div\", { className: \"sp-progress__tooltip\" });\n this.tooltip.textContent = \"0:00\";\n this.thumbnailPreview = new ThumbnailPreview();\n track.appendChild(this.buffered);\n track.appendChild(this.filled);\n track.appendChild(this.markers);\n track.appendChild(this.handle);\n this.el.appendChild(track);\n this.el.appendChild(this.thumbnailPreview.getElement());\n this.el.appendChild(this.tooltip);\n this.wrapper.appendChild(this.el);\n this.el.setAttribute(\"role\", \"slider\");\n this.el.setAttribute(\"aria-label\", \"Seek\");\n this.el.setAttribute(\"aria-valuemin\", \"0\");\n this.el.setAttribute(\"aria-valuemax\", \"0\");\n this.el.setAttribute(\"aria-valuenow\", \"0\");\n this.el.setAttribute(\"aria-valuetext\", \"0:00\");\n this.el.setAttribute(\"tabindex\", \"0\");\n this.wrapper.addEventListener(\"mousedown\", this.onMouseDown);\n this.wrapper.addEventListener(\"mousemove\", this.onMouseMove);\n this.wrapper.addEventListener(\"mouseleave\", this.onMouseLeave);\n this.wrapper.addEventListener(\"touchstart\", this.onTouchStart, { passive: false });\n this.el.addEventListener(\"keydown\", this.onKeyDown);\n document.addEventListener(\"mousemove\", this.onDocMouseMove);\n document.addEventListener(\"mouseup\", this.onMouseUp);\n document.addEventListener(\"touchmove\", this.onDocTouchMove, { passive: false });\n document.addEventListener(\"touchend\", this.onTouchEnd);\n document.addEventListener(\"touchcancel\", this.onTouchEnd);\n }\n render() {\n return this.wrapper;\n }\n /** Show the progress bar */\n show() {\n this.wrapper.classList.add(\"sp-progress-wrapper--visible\");\n }\n /** Hide the progress bar */\n hide() {\n this.wrapper.classList.remove(\"sp-progress-wrapper--visible\");\n }\n /** Set thumbnail sprite configuration */\n setThumbnails(config) {\n this.thumbnailPreview.setConfig(config);\n }\n update() {\n const currentTime = this.api.getState(\"currentTime\") || 0;\n const duration = this.api.getState(\"duration\") || 0;\n const bufferedRanges = this.api.getState(\"buffered\");\n const live = this.api.getState(\"live\");\n const seekableRange = this.api.getState(\"seekableRange\");\n const thumbnails = this.api.getState(\"thumbnails\");\n if (thumbnails && !this.thumbnailPreview.isConfigured()) {\n this.thumbnailPreview.setConfig(thumbnails);\n }\n this.el.classList.toggle(\"sp-progress--live\", !!live);\n this.updateMarkers(duration, live, seekableRange);\n if (live && seekableRange) {\n const rangeLength = seekableRange.end - seekableRange.start;\n if (rangeLength > 0) {\n const progress = (currentTime - seekableRange.start) / rangeLength * 100;\n this.filled.style.width = `${Math.max(0, Math.min(100, progress))}%`;\n this.handle.style.left = `${Math.max(0, Math.min(100, progress))}%`;\n }\n if (bufferedRanges && bufferedRanges.length > 0) {\n const rangeLength2 = seekableRange.end - seekableRange.start;\n if (rangeLength2 > 0) {\n const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);\n const bufferedPercent = (bufferedEnd - seekableRange.start) / rangeLength2 * 100;\n this.buffered.style.width = `${Math.max(0, Math.min(100, bufferedPercent))}%`;\n }\n }\n this.el.setAttribute(\"aria-valuemax\", String(Math.floor(seekableRange.end)));\n this.el.setAttribute(\"aria-valuenow\", String(Math.floor(currentTime)));\n this.el.setAttribute(\"aria-valuetext\", `${Math.floor(seekableRange.end - currentTime)} seconds behind live`);\n } else if (duration > 0) {\n const progress = currentTime / duration * 100;\n this.filled.style.width = `${progress}%`;\n this.handle.style.left = `${progress}%`;\n if (bufferedRanges && bufferedRanges.length > 0) {\n const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);\n const bufferedPercent = bufferedEnd / duration * 100;\n this.buffered.style.width = `${bufferedPercent}%`;\n }\n this.el.setAttribute(\"aria-valuemax\", String(Math.floor(duration)));\n this.el.setAttribute(\"aria-valuenow\", String(Math.floor(currentTime)));\n this.el.setAttribute(\"aria-valuetext\", formatTime(currentTime));\n }\n }\n /**\n * Label of the chapter containing a point on the timeline.\n *\n * Mirrors the chapters plugin's own lookup: a start time belongs to its\n * chapter, an end time belongs to the next one, and a point in a gap between\n * sparse chapters belongs to neither.\n *\n * @param time - Position in seconds\n * @returns The chapter label, or null when the point is outside every chapter\n */\n chapterLabelAt(time) {\n const chapters = this.api.getState(\"chapters\") ?? [];\n for (let i = chapters.length - 1; i >= 0; i--) {\n const chapter = chapters[i];\n if (time < chapter.time) continue;\n const next = chapters[i + 1];\n const end = chapter.endTime ?? (next ? next.time : Infinity);\n return time < end ? chapter.label : null;\n }\n return null;\n }\n /**\n * Paint chapter dividers along the track.\n *\n * Reads the `chapters` state that core owns, so this works whether the list\n * came from the chapters plugin or the host set it directly, and renders\n * nothing at all when there are none.\n *\n * Rebuilds only when the list or the duration actually changed. `update()`\n * runs on every time update, and rebuilding a dozen nodes 4 times a second\n * would churn the DOM for no reason.\n *\n * @param duration - Media duration in seconds\n * @param live - Whether the media is live\n * @param seekableRange - DVR window, when the media is live\n */\n updateMarkers(duration, live, seekableRange) {\n const chapters = this.api.getState(\"chapters\") ?? [];\n const range = live ? seekableRange ? seekableRange.end - seekableRange.start : 0 : duration;\n if (chapters.length === 0 || range <= 0) {\n if (this.renderedChapters !== null) {\n this.markers.textContent = \"\";\n this.renderedChapters = null;\n this.renderedDuration = 0;\n }\n return;\n }\n if (chapters === this.renderedChapters && range === this.renderedDuration) {\n return;\n }\n const origin = live && seekableRange ? seekableRange.start : 0;\n this.markers.textContent = \"\";\n for (const chapter of chapters) {\n if (chapter.time <= origin) continue;\n const percent = (chapter.time - origin) / range * 100;\n if (percent <= 0 || percent >= 100) continue;\n const marker = createElement(\"div\", { className: \"sp-progress__marker\" });\n marker.style.left = `${percent}%`;\n marker.title = chapter.label;\n this.markers.appendChild(marker);\n }\n this.renderedChapters = chapters;\n this.renderedDuration = range;\n }\n getTimeFromPosition(clientX) {\n const rect = this.el.getBoundingClientRect();\n const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n const live = this.api.getState(\"live\");\n const seekableRange = this.api.getState(\"seekableRange\");\n if (live && seekableRange) {\n const rangeLength = seekableRange.end - seekableRange.start;\n return seekableRange.start + percent * rangeLength;\n }\n const duration = this.api.getState(\"duration\") || 0;\n return percent * duration;\n }\n updateTooltip(clientX) {\n const rect = this.el.getBoundingClientRect();\n const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n const time = this.getTimeFromPosition(clientX);\n const live = this.api.getState(\"live\");\n const seekableRange = this.api.getState(\"seekableRange\");\n if (live && seekableRange) {\n const behindLive = seekableRange.end - time;\n this.tooltip.textContent = formatLiveTime(behindLive);\n } else {\n this.tooltip.textContent = formatTime(time);\n }\n const chapterLabel = this.chapterLabelAt(time);\n if (chapterLabel) {\n const label = createElement(\"span\", { className: \"sp-progress__tooltip-chapter\" });\n label.textContent = chapterLabel;\n this.tooltip.appendChild(label);\n }\n this.tooltip.style.left = `${percent * 100}%`;\n if (this.thumbnailPreview.isConfigured()) {\n this.thumbnailPreview.show(time, percent);\n }\n }\n updateVisualPosition(clientX) {\n const rect = this.el.getBoundingClientRect();\n const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n this.filled.style.width = `${percent * 100}%`;\n this.handle.style.left = `${percent * 100}%`;\n }\n seek(clientX, force = false) {\n const video = getVideo(this.api.container);\n if (!video) return;\n const now = Date.now();\n if (!force && this.isDragging && now - this.lastSeekTime < this.seekThrottleMs) {\n return;\n }\n this.lastSeekTime = now;\n const time = this.getTimeFromPosition(clientX);\n video.currentTime = time;\n }\n destroy() {\n this.wrapper.removeEventListener(\"mousedown\", this.onMouseDown);\n this.wrapper.removeEventListener(\"mousemove\", this.onMouseMove);\n this.wrapper.removeEventListener(\"mouseleave\", this.onMouseLeave);\n this.wrapper.removeEventListener(\"touchstart\", this.onTouchStart);\n document.removeEventListener(\"mousemove\", this.onDocMouseMove);\n document.removeEventListener(\"mouseup\", this.onMouseUp);\n document.removeEventListener(\"touchmove\", this.onDocTouchMove);\n document.removeEventListener(\"touchend\", this.onTouchEnd);\n document.removeEventListener(\"touchcancel\", this.onTouchEnd);\n this.thumbnailPreview.destroy();\n this.wrapper.remove();\n }\n};\n\n// src/controls/TimeDisplay.ts\nvar TimeDisplay = class {\n constructor(api) {\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-time\" });\n this.el.setAttribute(\"aria-live\", \"off\");\n }\n render() {\n return this.el;\n }\n update() {\n const live = this.api.getState(\"live\");\n const currentTime = this.api.getState(\"currentTime\") || 0;\n const duration = this.api.getState(\"duration\") || 0;\n if (live) {\n const seekableRange = this.api.getState(\"seekableRange\");\n if (seekableRange) {\n const behindLive = seekableRange.end - currentTime;\n this.el.textContent = formatLiveTime(behindLive);\n } else {\n this.el.textContent = formatLiveTime(0);\n }\n } else {\n this.el.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;\n }\n }\n destroy() {\n this.el.remove();\n }\n};\n\n// src/controls/VolumeControl.ts\nvar VolumeControl = class {\n constructor(api) {\n this.isDragging = false;\n this.onMouseDown = (e) => {\n e.preventDefault();\n this.isDragging = true;\n this.setVolume(this.getVolumeFromPosition(e.clientX));\n };\n this.onDocMouseMove = (e) => {\n if (this.isDragging) {\n this.setVolume(this.getVolumeFromPosition(e.clientX));\n }\n };\n this.onMouseUp = () => {\n this.isDragging = false;\n };\n this.onTouchStart = (e) => {\n e.preventDefault();\n this.isDragging = true;\n this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));\n };\n this.onDocTouchMove = (e) => {\n if (this.isDragging) {\n e.preventDefault();\n this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));\n }\n };\n this.onTouchEnd = () => {\n this.isDragging = false;\n };\n this.onKeyDown = (e) => {\n const video = getVideo(this.api.container);\n if (!video) return;\n const step = 0.1;\n switch (e.key) {\n case \"ArrowUp\":\n case \"ArrowRight\":\n e.preventDefault();\n this.setVolume(video.volume + step);\n break;\n case \"ArrowDown\":\n case \"ArrowLeft\":\n e.preventDefault();\n this.setVolume(video.volume - step);\n break;\n }\n };\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-volume\" });\n this.btn = createElement(\"button\", {\n className: \"sp-control sp-volume__btn\",\n \"aria-label\": \"Mute\",\n type: \"button\"\n });\n this.btn.innerHTML = icons.volumeHigh;\n this.btn.onclick = () => this.toggleMute();\n const sliderWrap = createElement(\"div\", { className: \"sp-volume__slider-wrap\" });\n this.slider = createElement(\"div\", { className: \"sp-volume__slider\" });\n this.slider.setAttribute(\"role\", \"slider\");\n this.slider.setAttribute(\"aria-label\", \"Volume\");\n this.slider.setAttribute(\"aria-valuemin\", \"0\");\n this.slider.setAttribute(\"aria-valuemax\", \"100\");\n this.slider.setAttribute(\"tabindex\", \"0\");\n this.level = createElement(\"div\", { className: \"sp-volume__level\" });\n this.slider.appendChild(this.level);\n sliderWrap.appendChild(this.slider);\n this.el.appendChild(this.btn);\n this.el.appendChild(sliderWrap);\n this.slider.addEventListener(\"mousedown\", this.onMouseDown);\n this.slider.addEventListener(\"touchstart\", this.onTouchStart, { passive: false });\n this.slider.addEventListener(\"keydown\", this.onKeyDown);\n document.addEventListener(\"mousemove\", this.onDocMouseMove);\n document.addEventListener(\"mouseup\", this.onMouseUp);\n document.addEventListener(\"touchmove\", this.onDocTouchMove, { passive: false });\n document.addEventListener(\"touchend\", this.onTouchEnd);\n document.addEventListener(\"touchcancel\", this.onTouchEnd);\n }\n render() {\n return this.el;\n }\n update() {\n const volume = this.api.getState(\"volume\") ?? 1;\n const muted = this.api.getState(\"muted\") ?? false;\n let icon;\n let label;\n if (muted || volume === 0) {\n icon = icons.volumeMute;\n label = \"Unmute\";\n } else if (volume < 0.5) {\n icon = icons.volumeLow;\n label = \"Mute\";\n } else {\n icon = icons.volumeHigh;\n label = \"Mute\";\n }\n setHTML(this.btn, icon);\n setAttr(this.btn, \"aria-label\", label);\n const displayVolume = muted ? 0 : volume;\n const width = `${displayVolume * 100}%`;\n if (this.level.style.width !== width) {\n this.level.style.width = width;\n }\n const volumePercent = Math.round(displayVolume * 100);\n setAttr(this.slider, \"aria-valuenow\", String(volumePercent));\n setAttr(this.slider, \"aria-valuetext\", `${volumePercent}%`);\n }\n toggleMute() {\n const video = getVideo(this.api.container);\n if (!video) return;\n video.muted = !video.muted;\n }\n setVolume(percent) {\n const video = getVideo(this.api.container);\n if (!video) return;\n const vol = Math.max(0, Math.min(1, percent));\n video.volume = vol;\n if (vol > 0 && video.muted) {\n video.muted = false;\n }\n }\n getVolumeFromPosition(clientX) {\n const rect = this.slider.getBoundingClientRect();\n return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n }\n destroy() {\n this.slider.removeEventListener(\"mousedown\", this.onMouseDown);\n this.slider.removeEventListener(\"touchstart\", this.onTouchStart);\n this.slider.removeEventListener(\"keydown\", this.onKeyDown);\n document.removeEventListener(\"mousemove\", this.onDocMouseMove);\n document.removeEventListener(\"mouseup\", this.onMouseUp);\n document.removeEventListener(\"touchmove\", this.onDocTouchMove);\n document.removeEventListener(\"touchend\", this.onTouchEnd);\n document.removeEventListener(\"touchcancel\", this.onTouchEnd);\n this.el.remove();\n }\n};\n\n// src/controls/LiveIndicator.ts\nvar LiveIndicator = class {\n constructor(api) {\n this.handleClick = () => {\n this.seekToLive();\n };\n this.handleKeyDown = (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n this.seekToLive();\n }\n };\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-live\" });\n this.dot = createElement(\"div\", { className: \"sp-live__dot\" });\n this.label = document.createElement(\"span\");\n this.label.textContent = \"LIVE\";\n this.el.appendChild(this.dot);\n this.el.appendChild(this.label);\n this.el.setAttribute(\"role\", \"button\");\n this.el.setAttribute(\"aria-label\", \"Live broadcast - currently at live edge\");\n this.el.setAttribute(\"tabindex\", \"0\");\n this.el.addEventListener(\"click\", this.handleClick);\n this.el.addEventListener(\"keydown\", this.handleKeyDown);\n }\n render() {\n return this.el;\n }\n update() {\n const live = this.api.getState(\"live\");\n const liveEdge = this.api.getState(\"liveEdge\");\n this.el.style.display = live ? \"\" : \"none\";\n if (liveEdge) {\n this.el.classList.remove(\"sp-live--behind\");\n this.label.textContent = \"LIVE\";\n this.dot.setAttribute(\"aria-hidden\", \"true\");\n this.el.setAttribute(\"aria-label\", \"Live broadcast - currently at live edge\");\n } else {\n this.el.classList.add(\"sp-live--behind\");\n this.label.textContent = \"GO LIVE\";\n this.dot.setAttribute(\"aria-hidden\", \"true\");\n this.el.setAttribute(\"aria-label\", \"Live broadcast - behind live edge, click to seek to live\");\n }\n }\n seekToLive() {\n const video = getVideo(this.api.container);\n if (!video) return;\n const seekableRange = this.api.getState(\"seekableRange\");\n if (seekableRange) {\n video.currentTime = seekableRange.end;\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.handleClick);\n this.el.removeEventListener(\"keydown\", this.handleKeyDown);\n this.el.remove();\n }\n};\n\n// src/controls/QualityMenu.ts\nvar QualityMenu = class {\n constructor(api) {\n this.isOpen = false;\n this.lastQualitiesJson = \"\";\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-quality\" });\n this.btn = createButton(\"sp-quality__btn\", \"Quality\", icons.settings);\n this.btnLabel = createElement(\"span\", { className: \"sp-quality__label\" });\n this.btnLabel.textContent = \"Auto\";\n this.btn.appendChild(this.btnLabel);\n this.btn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n this.toggle();\n });\n this.menu = createElement(\"div\", { className: \"sp-quality-menu\" });\n this.menu.setAttribute(\"role\", \"menu\");\n this.menu.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n });\n this.el.appendChild(this.btn);\n this.el.appendChild(this.menu);\n this.closeHandler = (e) => {\n if (!this.el.contains(e.target)) {\n this.close();\n }\n };\n document.addEventListener(\"click\", this.closeHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const qualities = this.api.getState(\"qualities\") || [];\n const currentQuality = this.api.getState(\"currentQuality\");\n this.el.style.display = qualities.length > 0 ? \"\" : \"none\";\n this.btnLabel.textContent = currentQuality?.label || \"Auto\";\n const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));\n const currentId = currentQuality?.id || \"auto\";\n if (qualitiesJson !== this.lastQualitiesJson) {\n this.lastQualitiesJson = qualitiesJson;\n this.rebuildMenu(qualities);\n }\n this.updateActiveStates(currentId);\n }\n rebuildMenu(qualities) {\n this.menu.innerHTML = \"\";\n const autoItem = this.createMenuItem(\"Auto\", \"auto\");\n this.menu.appendChild(autoItem);\n const sorted = [...qualities].sort((a, b) => b.height - a.height);\n for (const q of sorted) {\n if (q.id === \"auto\") continue;\n const item = this.createMenuItem(q.label, q.id);\n this.menu.appendChild(item);\n }\n }\n updateActiveStates(activeId) {\n const items = this.menu.querySelectorAll(\".sp-quality-menu__item\");\n items.forEach((item) => {\n const id = item.getAttribute(\"data-quality-id\");\n const isActive = id === activeId;\n item.classList.toggle(\"sp-quality-menu__item--active\", isActive);\n });\n }\n createMenuItem(label, qualityId) {\n const item = createElement(\"div\", {\n className: \"sp-quality-menu__item\"\n });\n item.setAttribute(\"role\", \"menuitem\");\n item.setAttribute(\"data-quality-id\", qualityId);\n const labelSpan = createElement(\"span\", { className: \"sp-quality-menu__label\" });\n labelSpan.textContent = label;\n item.appendChild(labelSpan);\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n this.selectQuality(qualityId);\n });\n return item;\n }\n selectQuality(qualityId) {\n this.api.emit(\"quality:select\", {\n quality: qualityId,\n auto: qualityId === \"auto\"\n });\n this.close();\n }\n toggle() {\n this.isOpen ? this.close() : this.open();\n }\n open() {\n this.isOpen = true;\n this.menu.classList.add(\"sp-quality-menu--open\");\n this.btn.setAttribute(\"aria-expanded\", \"true\");\n }\n close() {\n this.isOpen = false;\n this.menu.classList.remove(\"sp-quality-menu--open\");\n this.btn.setAttribute(\"aria-expanded\", \"false\");\n }\n destroy() {\n document.removeEventListener(\"click\", this.closeHandler);\n this.el.remove();\n }\n};\n\n// src/controls/CastButton.ts\nfunction isChromecastSupported() {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent;\n return /Chrome/.test(ua) && !/Edge|Edg/.test(ua);\n}\nfunction isAirPlaySupported() {\n if (typeof HTMLVideoElement === \"undefined\") return false;\n return typeof HTMLVideoElement.prototype.webkitShowPlaybackTargetPicker === \"function\";\n}\nvar CastButton = class {\n constructor(api, type) {\n this.api = api;\n this.type = type;\n this.supported = type === \"chromecast\" ? isChromecastSupported() : isAirPlaySupported();\n const icon = type === \"chromecast\" ? icons.chromecast : icons.airplay;\n const label = type === \"chromecast\" ? \"Cast\" : \"AirPlay\";\n this.el = createButton(`sp-cast sp-cast--${type}`, label, icon);\n this.el.addEventListener(\"click\", () => this.handleClick());\n if (!this.supported) {\n this.el.style.display = \"none\";\n }\n }\n render() {\n return this.el;\n }\n update() {\n if (!this.supported) {\n this.el.style.display = \"none\";\n return;\n }\n if (this.type === \"chromecast\") {\n const available = this.api.getState(\"chromecastAvailable\");\n const active = this.api.getState(\"chromecastActive\");\n this.el.style.display = \"\";\n this.el.disabled = !available && !active;\n this.el.classList.toggle(\"sp-cast--active\", !!active);\n this.el.classList.toggle(\"sp-cast--unavailable\", !available && !active);\n if (active) {\n setHTML(this.el, icons.chromecastConnected);\n setAttr(this.el, \"aria-label\", \"Stop casting\");\n } else {\n setHTML(this.el, icons.chromecast);\n setAttr(this.el, \"aria-label\", available ? \"Cast\" : \"No Cast devices found\");\n }\n } else {\n const active = this.api.getState(\"airplayActive\");\n this.el.style.display = \"\";\n this.el.disabled = false;\n this.el.classList.toggle(\"sp-cast--active\", !!active);\n this.el.classList.remove(\"sp-cast--unavailable\");\n setAttr(this.el, \"aria-label\", active ? \"Stop AirPlay\" : \"AirPlay\");\n }\n }\n handleClick() {\n if (this.type === \"chromecast\") {\n this.handleChromecast();\n } else {\n this.handleAirPlay();\n }\n }\n handleChromecast() {\n const chromecast = this.api.getPlugin(\"chromecast\");\n if (!chromecast) return;\n if (chromecast.isConnected()) {\n chromecast.endSession();\n } else {\n chromecast.requestSession().catch(() => {\n });\n }\n }\n async handleAirPlay() {\n const airplayPlugin = this.api.getPlugin(\"airplay\");\n if (airplayPlugin) {\n await airplayPlugin.showPicker();\n } else {\n const video = getVideo(this.api.container);\n video?.webkitShowPlaybackTargetPicker?.();\n }\n }\n destroy() {\n this.el.remove();\n }\n};\n\n// src/controls/PipButton.ts\nvar PipButton = class {\n constructor(api) {\n this.clickHandler = () => {\n void this.toggle().catch(() => {\n });\n };\n this.api = api;\n const probe = document.createElement(\"video\");\n this.supported = \"pictureInPictureEnabled\" in document || \"webkitSetPresentationMode\" in probe;\n this.el = createButton(\"sp-pip\", \"Picture-in-Picture\", icons.pip);\n this.el.addEventListener(\"click\", this.clickHandler);\n if (!this.supported) {\n this.el.style.display = \"none\";\n } else {\n this.el.disabled = true;\n this.el.setAttribute(\"aria-disabled\", \"true\");\n }\n }\n render() {\n return this.el;\n }\n /** Whether the media element is ready to enter PiP (metadata loaded). */\n isMediaReady() {\n const video = getVideo(this.api.container);\n return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;\n }\n update() {\n if (!this.supported) return;\n const pip = !!this.api.getState(\"pip\");\n const enabled = pip || this.isMediaReady();\n this.el.disabled = !enabled;\n setAttr(this.el, \"aria-disabled\", String(!enabled));\n setHTML(this.el, pip ? icons.exitPip : icons.pip);\n setAttr(this.el, \"aria-label\", pip ? \"Exit Picture-in-Picture\" : \"Picture-in-Picture\");\n this.el.classList.toggle(\"sp-pip--active\", pip);\n }\n async toggle() {\n const video = getVideo(this.api.container);\n if (!video) {\n this.api.logger.warn(\"PiP: video element not found\");\n return;\n }\n const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === \"picture-in-picture\";\n if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {\n this.api.logger.debug(\"PiP: ignored, media not ready\", {\n readyState: video.readyState\n });\n return;\n }\n try {\n if (isInPip) {\n if (document.pictureInPictureElement) {\n await document.exitPictureInPicture();\n } else if (video.webkitSetPresentationMode) {\n video.webkitSetPresentationMode(\"inline\");\n }\n this.api.logger.debug(\"PiP: exited\");\n } else {\n if (video.requestPictureInPicture) {\n await video.requestPictureInPicture();\n } else if (video.webkitSetPresentationMode) {\n video.webkitSetPresentationMode(\"picture-in-picture\");\n }\n this.api.logger.debug(\"PiP: entered\");\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n this.api.logger.warn(\"PiP: failed\", { error: message });\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.clickHandler);\n this.el.remove();\n }\n};\n\n// src/controls/FullscreenButton.ts\nvar FullscreenButton = class {\n constructor(api) {\n this.clickHandler = () => {\n this.toggle();\n };\n this.api = api;\n this.el = createButton(\"sp-fullscreen\", \"Fullscreen\", icons.fullscreen);\n this.el.addEventListener(\"click\", this.clickHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const fullscreen = this.api.getState(\"fullscreen\");\n if (fullscreen) {\n setHTML(this.el, icons.exitFullscreen);\n setAttr(this.el, \"aria-label\", \"Exit fullscreen\");\n } else {\n setHTML(this.el, icons.fullscreen);\n setAttr(this.el, \"aria-label\", \"Fullscreen\");\n }\n }\n async toggle() {\n const container = this.api.container;\n const video = getVideo(container);\n try {\n if (document.fullscreenElement) {\n await document.exitFullscreen();\n } else if (container.requestFullscreen) {\n await container.requestFullscreen();\n } else if (video?.webkitEnterFullscreen) {\n video.webkitEnterFullscreen();\n }\n } catch {\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.clickHandler);\n this.el.remove();\n }\n};\n\n// src/controls/Spacer.ts\nvar Spacer = class {\n constructor() {\n this.el = createElement(\"div\", { className: \"sp-spacer\" });\n }\n render() {\n return this.el;\n }\n update() {\n }\n destroy() {\n this.el.remove();\n }\n};\n\n// src/controls/ErrorOverlay.ts\nfunction getUserMessage(error) {\n if (!error) return \"Something went wrong.\";\n const code = error.code;\n if (code) {\n switch (code) {\n case \"MEDIA_NETWORK_ERROR\":\n return \"Having trouble connecting. Check your internet and try again.\";\n case \"MEDIA_DECODE_ERROR\":\n return \"This video can't be played right now.\";\n case \"SOURCE_LOAD_FAILED\":\n case \"SOURCE_NOT_SUPPORTED\":\n case \"PROVIDER_NOT_FOUND\":\n return \"Unable to load video. Please try again.\";\n case \"PLAYBACK_FAILED\":\n return \"Playback stopped unexpectedly. Please try again.\";\n case \"MEDIA_APPEND_ERROR\":\n return \"Video playback was interrupted. Please try again.\";\n case \"MEDIA_BUFFER_FULL\":\n return \"Your device is low on video memory. Close other apps or tabs and try again.\";\n case \"PLAYLIST_INVALID\":\n return \"The stream is temporarily unavailable. Please try again.\";\n }\n }\n const msg = error.message?.toLowerCase() || \"\";\n if (msg.includes(\"network\") || msg.includes(\"timeout\") || msg.includes(\"fetch\") || msg.includes(\"connection\")) {\n return \"Having trouble connecting. Check your internet and try again.\";\n }\n if (msg.includes(\"manifest\")) {\n return \"Unable to load video. Please try again.\";\n }\n if (msg.includes(\"decode\") || msg.includes(\"media\") || msg.includes(\"format\") || msg.includes(\"codec\")) {\n return \"This video can't be played right now.\";\n }\n if (msg.includes(\"not found\") || msg.includes(\"404\") || msg.includes(\"source\") || msg.includes(\"not supported\")) {\n return \"Video not found.\";\n }\n return \"Something went wrong.\";\n}\nvar ErrorOverlay = class {\n constructor(api) {\n this.visible = false;\n this.lastSource = null;\n this.handleRetry = () => {\n if (this.retryBtn.disabled) return;\n this.retryBtn.disabled = true;\n this.hide();\n const source = this.api.getState(\"source\");\n const src = source?.src || this.lastSource;\n if (src) {\n this.api.emit(\"error:retry\", { src });\n }\n setTimeout(() => {\n this.retryBtn.disabled = false;\n }, 1e3);\n };\n this.handleDismiss = () => {\n this.hide();\n this.api.emit(\"error:dismiss\", void 0);\n };\n this.api = api;\n const overlay = document.createElement(\"div\");\n overlay.className = \"sp-error-overlay\";\n overlay.setAttribute(\"role\", \"alert\");\n overlay.setAttribute(\"aria-live\", \"assertive\");\n const content = document.createElement(\"div\");\n content.className = \"sp-error-overlay__content\";\n const iconEl = document.createElement(\"div\");\n iconEl.className = \"sp-error-overlay__icon\";\n iconEl.innerHTML = icons.error;\n const messageEl = document.createElement(\"p\");\n messageEl.className = \"sp-error-overlay__message\";\n messageEl.textContent = \"Something went wrong.\";\n const actions = document.createElement(\"div\");\n actions.className = \"sp-error-overlay__actions\";\n this.retryBtn = document.createElement(\"button\");\n this.retryBtn.className = \"sp-error-overlay__retry\";\n this.retryBtn.setAttribute(\"type\", \"button\");\n this.retryBtn.setAttribute(\"aria-label\", \"Try again\");\n this.retryBtn.textContent = \"Try Again\";\n this.retryBtn.addEventListener(\"click\", this.handleRetry);\n this.dismissBtn = document.createElement(\"button\");\n this.dismissBtn.className = \"sp-error-overlay__dismiss\";\n this.dismissBtn.setAttribute(\"type\", \"button\");\n this.dismissBtn.setAttribute(\"aria-label\", \"Go back\");\n this.dismissBtn.textContent = \"Go Back\";\n this.dismissBtn.addEventListener(\"click\", this.handleDismiss);\n actions.appendChild(this.retryBtn);\n actions.appendChild(this.dismissBtn);\n content.appendChild(iconEl);\n content.appendChild(messageEl);\n content.appendChild(actions);\n overlay.appendChild(content);\n this.el = overlay;\n }\n render() {\n return this.el;\n }\n /** Show the error overlay with the given error */\n show(error) {\n const message = getUserMessage(error);\n const messageEl = this.el.querySelector(\".sp-error-overlay__message\");\n if (messageEl) {\n messageEl.textContent = message;\n }\n const source = this.api.getState(\"source\");\n if (source?.src) {\n this.lastSource = source.src;\n }\n this.visible = true;\n this.retryBtn.disabled = false;\n this.el.classList.remove(\"sp-error-overlay--reconnecting\");\n this.el.classList.add(\"sp-error-overlay--visible\");\n }\n /**\n * Show the reconnecting state.\n *\n * Displayed while the provider auto-reconnects after a fatal error, so the\n * viewer sees the player working on the problem instead of a dead-end\n * error. Try Again stays available for viewers who want to force an\n * immediate attempt.\n */\n showReconnecting() {\n const messageEl = this.el.querySelector(\".sp-error-overlay__message\");\n if (messageEl) {\n messageEl.textContent = \"Connection lost. Reconnecting...\";\n }\n const source = this.api.getState(\"source\");\n if (source?.src) {\n this.lastSource = source.src;\n }\n this.visible = true;\n this.retryBtn.disabled = false;\n this.el.classList.add(\"sp-error-overlay--reconnecting\");\n this.el.classList.add(\"sp-error-overlay--visible\");\n }\n /** Hide the error overlay */\n hide() {\n this.visible = false;\n this.el.classList.remove(\"sp-error-overlay--visible\");\n this.el.classList.remove(\"sp-error-overlay--reconnecting\");\n }\n isVisible() {\n return this.visible;\n }\n update() {\n const playbackState = this.api.getState(\"playbackState\");\n if (this.visible && playbackState !== \"error\" && playbackState !== \"loading\") {\n const playing = this.api.getState(\"playing\");\n if (playing) {\n this.hide();\n }\n }\n }\n destroy() {\n this.retryBtn.removeEventListener(\"click\", this.handleRetry);\n this.dismissBtn.removeEventListener(\"click\", this.handleDismiss);\n this.el.remove();\n }\n};\n\n// src/controls/SettingsMenu.ts\nvar SPEED_OPTIONS = [\n { label: \"0.5x\", value: 0.5 },\n { label: \"0.75x\", value: 0.75 },\n { label: \"Normal\", value: 1 },\n { label: \"1.25x\", value: 1.25 },\n { label: \"1.5x\", value: 1.5 },\n { label: \"2x\", value: 2 }\n];\nvar SettingsMenu = class {\n constructor(api) {\n this.isOpen = false;\n this.currentPanel = \"main\";\n this.lastQualitiesJson = \"\";\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-settings\" });\n this.btn = createButton(\"sp-settings__btn\", \"Settings\", icons.settings);\n this.btn.setAttribute(\"aria-haspopup\", \"true\");\n this.btn.setAttribute(\"aria-expanded\", \"false\");\n this.btn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n this.toggle();\n });\n this.panel = createElement(\"div\", { className: \"sp-settings-panel\" });\n this.panel.setAttribute(\"role\", \"menu\");\n this.panel.addEventListener(\"click\", (e) => e.stopPropagation());\n this.el.appendChild(this.btn);\n this.el.appendChild(this.panel);\n this.closeHandler = (e) => {\n if (!this.el.contains(e.target)) {\n this.close();\n }\n };\n document.addEventListener(\"click\", this.closeHandler);\n this.keyHandler = (e) => {\n if (!this.isOpen) return;\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n if (this.currentPanel !== \"main\") {\n this.showPanel(\"main\");\n } else {\n this.close();\n this.btn.focus();\n }\n return;\n }\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n e.stopPropagation();\n this.navigateItems(e.key === \"ArrowDown\" ? 1 : -1);\n return;\n }\n if (e.key === \"Tab\") {\n e.preventDefault();\n e.stopPropagation();\n this.navigateItems(e.shiftKey ? -1 : 1);\n }\n };\n document.addEventListener(\"keydown\", this.keyHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const qualities = this.api.getState(\"qualities\") || [];\n const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));\n if (qualitiesJson !== this.lastQualitiesJson) {\n this.lastQualitiesJson = qualitiesJson;\n if (this.isOpen && this.currentPanel === \"quality\") {\n this.renderQualityPanel();\n }\n }\n if (this.isOpen) {\n if (this.currentPanel === \"quality\") {\n this.updateQualityActiveStates();\n } else if (this.currentPanel === \"speed\") {\n this.updateSpeedActiveStates();\n } else if (this.currentPanel === \"captions\") {\n this.updateCaptionsActiveStates();\n }\n }\n }\n toggle() {\n this.isOpen ? this.close() : this.open();\n }\n open() {\n this.isOpen = true;\n this.currentPanel = \"main\";\n this.renderMainPanel();\n this.panel.classList.add(\"sp-settings-panel--open\");\n this.btn.setAttribute(\"aria-expanded\", \"true\");\n this.focusFirstItem();\n }\n close() {\n this.isOpen = false;\n this.currentPanel = \"main\";\n this.panel.classList.remove(\"sp-settings-panel--open\");\n this.btn.setAttribute(\"aria-expanded\", \"false\");\n }\n showPanel(panel) {\n this.currentPanel = panel;\n switch (panel) {\n case \"main\":\n this.renderMainPanel();\n break;\n case \"quality\":\n this.renderQualityPanel();\n break;\n case \"speed\":\n this.renderSpeedPanel();\n break;\n case \"captions\":\n this.renderCaptionsPanel();\n break;\n }\n this.focusFirstItem();\n }\n renderMainPanel() {\n this.panel.innerHTML = \"\";\n this.panel.className = \"sp-settings-panel sp-settings-panel--open sp-settings-panel--main\";\n const qualities = this.api.getState(\"qualities\") || [];\n const currentQuality = this.api.getState(\"currentQuality\");\n const playbackRate = this.api.getState(\"playbackRate\") ?? 1;\n if (qualities.length > 0) {\n const qualityRow = this.createMainRow(\n \"Quality\",\n currentQuality?.label || \"Auto\",\n () => this.showPanel(\"quality\")\n );\n this.panel.appendChild(qualityRow);\n }\n const textTracks = this.api.getState(\"textTracks\") || [];\n if (textTracks.length > 0) {\n const currentTextTrack = this.api.getState(\"currentTextTrack\");\n const captionsLabel = currentTextTrack ? currentTextTrack.label : \"Off\";\n const captionsRow = this.createMainRow(\n \"Captions\",\n captionsLabel,\n () => this.showPanel(\"captions\")\n );\n this.panel.appendChild(captionsRow);\n }\n const speedLabel = playbackRate === 1 ? \"Normal\" : `${playbackRate}x`;\n const speedRow = this.createMainRow(\n \"Speed\",\n speedLabel,\n () => this.showPanel(\"speed\")\n );\n this.panel.appendChild(speedRow);\n }\n createMainRow(label, value, onClick2) {\n const row = createElement(\"div\", { className: \"sp-settings-panel__row\" });\n row.setAttribute(\"role\", \"menuitem\");\n row.setAttribute(\"tabindex\", \"0\");\n row.setAttribute(\"aria-haspopup\", \"true\");\n const labelEl = createElement(\"span\", { className: \"sp-settings-panel__label\" });\n labelEl.textContent = label;\n const rightSide = createElement(\"span\", { className: \"sp-settings-panel__value\" });\n rightSide.textContent = value;\n const arrow = createElement(\"span\", { className: \"sp-settings-panel__arrow\" });\n arrow.innerHTML = icons.chevronDown;\n rightSide.appendChild(arrow);\n row.appendChild(labelEl);\n row.appendChild(rightSide);\n row.addEventListener(\"click\", (e) => {\n e.preventDefault();\n onClick2();\n });\n row.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick2();\n }\n });\n return row;\n }\n renderQualityPanel() {\n this.panel.innerHTML = \"\";\n this.panel.className = \"sp-settings-panel sp-settings-panel--open sp-settings-panel--sub\";\n const header = this.createSubHeader(\"Quality\");\n this.panel.appendChild(header);\n const qualities = this.api.getState(\"qualities\") || [];\n const currentQuality = this.api.getState(\"currentQuality\");\n const activeId = currentQuality?.id || \"auto\";\n const autoItem = this.createMenuItem(\"Auto\", \"auto\", activeId === \"auto\");\n autoItem.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.selectQuality(\"auto\");\n });\n this.panel.appendChild(autoItem);\n const sorted = [...qualities].sort(\n (a, b) => b.height - a.height\n );\n for (const q of sorted) {\n if (q.id === \"auto\") continue;\n const item = this.createMenuItem(q.label, q.id, q.id === activeId);\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.selectQuality(q.id);\n });\n this.panel.appendChild(item);\n }\n }\n renderSpeedPanel() {\n this.panel.innerHTML = \"\";\n this.panel.className = \"sp-settings-panel sp-settings-panel--open sp-settings-panel--sub\";\n const header = this.createSubHeader(\"Speed\");\n this.panel.appendChild(header);\n const currentRate = this.api.getState(\"playbackRate\") ?? 1;\n for (const opt of SPEED_OPTIONS) {\n const isActive = Math.abs(currentRate - opt.value) < 0.01;\n const item = this.createMenuItem(opt.label, String(opt.value), isActive);\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.selectSpeed(opt.value);\n });\n this.panel.appendChild(item);\n }\n }\n renderCaptionsPanel() {\n this.panel.innerHTML = \"\";\n this.panel.className = \"sp-settings-panel sp-settings-panel--open sp-settings-panel--sub\";\n const header = this.createSubHeader(\"Captions\");\n this.panel.appendChild(header);\n const textTracks = this.api.getState(\"textTracks\") || [];\n const currentTextTrack = this.api.getState(\"currentTextTrack\");\n const activeId = currentTextTrack?.id || \"off\";\n const offItem = this.createMenuItem(\"Off\", \"off\", activeId === \"off\");\n offItem.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.selectCaption(null);\n });\n this.panel.appendChild(offItem);\n for (const track of textTracks) {\n const item = this.createMenuItem(track.label, track.id, track.id === activeId);\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.selectCaption(track.id);\n });\n this.panel.appendChild(item);\n }\n }\n selectCaption(trackId) {\n this.api.emit(\"track:text\", { trackId });\n this.close();\n }\n updateCaptionsActiveStates() {\n const currentTextTrack = this.api.getState(\"currentTextTrack\");\n const activeId = currentTextTrack?.id || \"off\";\n const items = this.panel.querySelectorAll(\".sp-settings-panel__item\");\n items.forEach((item) => {\n const id = item.getAttribute(\"data-id\");\n item.classList.toggle(\"sp-settings-panel__item--active\", id === activeId);\n });\n }\n createSubHeader(title) {\n const header = createElement(\"div\", { className: \"sp-settings-panel__header\" });\n header.setAttribute(\"role\", \"menuitem\");\n header.setAttribute(\"tabindex\", \"0\");\n const backArrow = createElement(\"span\", { className: \"sp-settings-panel__back\" });\n backArrow.innerHTML = icons.chevronUp;\n const label = createElement(\"span\", { className: \"sp-settings-panel__header-label\" });\n label.textContent = title;\n header.appendChild(backArrow);\n header.appendChild(label);\n header.addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.showPanel(\"main\");\n });\n header.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n this.showPanel(\"main\");\n }\n });\n return header;\n }\n createMenuItem(label, dataId, isActive) {\n const item = createElement(\"div\", {\n className: `sp-settings-panel__item${isActive ? \" sp-settings-panel__item--active\" : \"\"}`\n });\n item.setAttribute(\"role\", \"menuitem\");\n item.setAttribute(\"tabindex\", \"0\");\n item.setAttribute(\"data-id\", dataId);\n const labelEl = createElement(\"span\");\n labelEl.textContent = label;\n const check = createElement(\"span\", { className: \"sp-settings-panel__check\" });\n check.innerHTML = icons.checkmark;\n item.appendChild(labelEl);\n item.appendChild(check);\n item.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n item.click();\n }\n });\n return item;\n }\n selectQuality(qualityId) {\n this.api.emit(\"quality:select\", {\n quality: qualityId,\n auto: qualityId === \"auto\"\n });\n this.close();\n }\n selectSpeed(rate) {\n this.api.emit(\"playback:ratechange\", { rate });\n const video = this.api.container.querySelector(\"video\");\n if (video) {\n video.playbackRate = rate;\n }\n this.close();\n }\n updateQualityActiveStates() {\n const currentQuality = this.api.getState(\"currentQuality\");\n const activeId = currentQuality?.id || \"auto\";\n const items = this.panel.querySelectorAll(\".sp-settings-panel__item\");\n items.forEach((item) => {\n const id = item.getAttribute(\"data-id\");\n item.classList.toggle(\"sp-settings-panel__item--active\", id === activeId);\n });\n }\n updateSpeedActiveStates() {\n const currentRate = this.api.getState(\"playbackRate\") ?? 1;\n const items = this.panel.querySelectorAll(\".sp-settings-panel__item\");\n items.forEach((item) => {\n const id = item.getAttribute(\"data-id\");\n const value = parseFloat(id || \"1\");\n item.classList.toggle(\n \"sp-settings-panel__item--active\",\n Math.abs(currentRate - value) < 0.01\n );\n });\n }\n getFocusableItems() {\n return Array.from(\n this.panel.querySelectorAll('[role=\"menuitem\"]')\n );\n }\n focusFirstItem() {\n requestAnimationFrame(() => {\n const items = this.getFocusableItems();\n if (items.length > 0) {\n items[0].focus();\n }\n });\n }\n navigateItems(direction) {\n const items = this.getFocusableItems();\n if (items.length === 0) return;\n const active = document.activeElement;\n const currentIndex = items.indexOf(active);\n let nextIndex;\n if (currentIndex === -1) {\n nextIndex = direction === 1 ? 0 : items.length - 1;\n } else {\n nextIndex = (currentIndex + direction + items.length) % items.length;\n }\n items[nextIndex].focus();\n }\n getPanel() {\n return this.currentPanel;\n }\n isMenuOpen() {\n return this.isOpen;\n }\n destroy() {\n document.removeEventListener(\"click\", this.closeHandler);\n document.removeEventListener(\"keydown\", this.keyHandler);\n this.el.remove();\n }\n};\n\n// src/controls/SkipButton.ts\nvar DEFAULT_SKIP_SECONDS = 10;\nvar SkipButton = class {\n constructor(api, direction, seconds = DEFAULT_SKIP_SECONDS) {\n this.clickHandler = () => {\n this.skip();\n };\n this.api = api;\n this.direction = direction;\n this.seconds = seconds;\n const icon = direction === \"backward\" ? icons.replay10 : icons.forward10;\n const label = direction === \"backward\" ? `Rewind ${seconds} seconds` : `Forward ${seconds} seconds`;\n this.el = createButton(\n `sp-skip sp-skip--${direction}`,\n label,\n icon\n );\n this.el.addEventListener(\"click\", this.clickHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const live = this.api.getState(\"live\");\n const duration = this.api.getState(\"duration\") ?? 0;\n const seekableRange = this.api.getState(\"seekableRange\");\n if (live && !seekableRange) {\n this.el.style.display = \"none\";\n return;\n }\n if (live && seekableRange) {\n this.el.style.display = \"\";\n return;\n }\n if (duration === 0) {\n this.el.style.display = \"none\";\n return;\n }\n this.el.style.display = \"\";\n }\n skip() {\n const video = getVideo(this.api.container);\n if (!video) return;\n const live = this.api.getState(\"live\");\n const seekableRange = this.api.getState(\"seekableRange\");\n if (live && seekableRange) {\n if (this.direction === \"backward\") {\n video.currentTime = Math.max(seekableRange.start, video.currentTime - this.seconds);\n } else {\n video.currentTime = Math.min(seekableRange.end, video.currentTime + this.seconds);\n }\n return;\n }\n const duration = video.duration || 0;\n if (!duration || !isFinite(duration)) return;\n if (this.direction === \"backward\") {\n video.currentTime = Math.max(0, video.currentTime - this.seconds);\n } else {\n video.currentTime = Math.min(duration, video.currentTime + this.seconds);\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.clickHandler);\n this.el.remove();\n }\n};\n\n// src/controls/CaptionsButton.ts\nvar CaptionsButton = class {\n constructor(api) {\n this.clickHandler = () => {\n this.toggle();\n };\n this.api = api;\n this.el = createButton(\"sp-captions\", \"Captions\", icons.captionsOff);\n this.el.addEventListener(\"click\", this.clickHandler);\n }\n render() {\n return this.el;\n }\n update() {\n const textTracks = this.api.getState(\"textTracks\") || [];\n const currentTrack = this.api.getState(\"currentTextTrack\");\n if (textTracks.length === 0) {\n this.el.style.display = \"none\";\n return;\n }\n this.el.style.display = \"\";\n if (currentTrack) {\n setHTML(this.el, icons.captions);\n setAttr(this.el, \"aria-label\", `Captions: ${currentTrack.label}`);\n this.el.classList.add(\"sp-captions--active\");\n } else {\n setHTML(this.el, icons.captionsOff);\n setAttr(this.el, \"aria-label\", \"Captions\");\n this.el.classList.remove(\"sp-captions--active\");\n }\n }\n toggle() {\n const textTracks = this.api.getState(\"textTracks\") || [];\n const currentTrack = this.api.getState(\"currentTextTrack\");\n if (textTracks.length === 0) return;\n if (currentTrack) {\n this.api.emit(\"track:text\", { trackId: null });\n } else {\n this.api.emit(\"track:text\", { trackId: textTracks[0].id });\n }\n }\n destroy() {\n this.el.removeEventListener(\"click\", this.clickHandler);\n this.el.remove();\n }\n};\n\n// src/controls/BandwidthIndicator.ts\nvar ICON_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12.55a11 11 0 0 1 14.08 0\"/><path d=\"M1.42 9a16 16 0 0 1 21.16 0\"/><path d=\"M8.53 16.11a6 6 0 0 1 6.95 0\"/><line x1=\"12\" y1=\"20\" x2=\"12.01\" y2=\"20\"/><line x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>`;\nvar BandwidthIndicator = class {\n constructor(api) {\n this.api = api;\n this.el = createElement(\"div\", { className: \"sp-bandwidth-indicator\" });\n this.el.innerHTML = ICON_SVG;\n this.el.setAttribute(\"aria-label\", \"Bandwidth is limiting video quality\");\n this.el.setAttribute(\"title\", \"Bandwidth is limiting video quality\");\n this.el.style.display = \"none\";\n }\n render() {\n return this.el;\n }\n update() {\n const bandwidth = this.api.getState(\"bandwidth\");\n const qualities = this.api.getState(\"qualities\");\n if (!bandwidth || !qualities || qualities.length === 0) {\n this.el.style.display = \"none\";\n return;\n }\n const highestBitrate = Math.max(...qualities.map((q) => q.bitrate));\n if (highestBitrate > 0 && bandwidth < highestBitrate) {\n this.el.style.display = \"\";\n } else {\n this.el.style.display = \"none\";\n }\n }\n destroy() {\n this.el.remove();\n }\n};\n\n// src/control-registry.ts\nvar registry = /* @__PURE__ */ new Map();\nvar listeners = /* @__PURE__ */ new Set();\nfunction registerControl(id, factory) {\n registry.set(id, factory);\n for (const listener of listeners) {\n listener(id);\n }\n}\nfunction unregisterControl(id) {\n return registry.delete(id);\n}\nfunction getControlFactory(id) {\n return registry.get(id) ?? null;\n}\nfunction onControlRegistered(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\nfunction resetControlRegistry() {\n registry.clear();\n listeners.clear();\n}\n\n// src/index.ts\nvar DEFAULT_LAYOUT = [\n \"play\",\n \"skip-backward\",\n \"skip-forward\",\n \"volume\",\n \"time\",\n \"live-indicator\",\n \"bandwidth-indicator\",\n \"spacer\",\n \"settings\",\n \"captions\",\n \"chromecast\",\n \"airplay\",\n \"pip\",\n \"fullscreen\"\n];\nvar DEFAULT_HIDE_DELAY = 3e3;\nfunction uiPlugin(config = {}) {\n let api;\n let controlBar = null;\n let gradient = null;\n let progressBar = null;\n let bufferingIndicator = null;\n let errorOverlay = null;\n let styleEl = null;\n let controls = [];\n let hideTimeout = null;\n let stateUnsubscribe = null;\n let controlRegistryUnsubscribe = null;\n let errorUnsubscribe = null;\n let reconnectingUnsubscribe = null;\n let recoveredUnsubscribe = null;\n let controlsVisible = true;\n let rafHandle = null;\n const layout = config.controls || DEFAULT_LAYOUT;\n const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;\n const createControl = (slot) => {\n switch (slot) {\n case \"play\":\n return new PlayButton(api);\n case \"skip-backward\":\n return new SkipButton(api, \"backward\");\n case \"skip-forward\":\n return new SkipButton(api, \"forward\");\n case \"volume\":\n return new VolumeControl(api);\n case \"progress\":\n return null;\n case \"time\":\n return new TimeDisplay(api);\n case \"live-indicator\":\n return new LiveIndicator(api);\n case \"bandwidth-indicator\":\n return new BandwidthIndicator(api);\n case \"quality\":\n return new QualityMenu(api);\n case \"settings\":\n return new SettingsMenu(api);\n case \"captions\":\n return new CaptionsButton(api);\n case \"chromecast\":\n return new CastButton(api, \"chromecast\");\n case \"airplay\":\n return new CastButton(api, \"airplay\");\n case \"pip\":\n return new PipButton(api);\n case \"fullscreen\":\n return new FullscreenButton(api);\n case \"spacer\":\n return new Spacer();\n default: {\n const factory = getControlFactory(slot);\n if (factory) {\n try {\n return factory(api);\n } catch (error) {\n api.logger.error(`Control factory for \"${slot}\" threw`, { error });\n return null;\n }\n }\n api.logger.warn(`Unknown control slot: ${slot}`);\n return null;\n }\n }\n };\n const populateControlBar = () => {\n if (!controlBar) {\n return;\n }\n for (const slot of layout) {\n const control = createControl(slot);\n if (control) {\n controls.push(control);\n controlBar.appendChild(control.render());\n }\n }\n };\n const rebuildControlBar = () => {\n if (!controlBar) {\n return;\n }\n controls.forEach((c) => c.destroy());\n controls = [];\n controlBar.replaceChildren();\n populateControlBar();\n updateControls();\n };\n const updateControls = () => {\n controls.forEach((c) => c.update());\n progressBar?.update();\n const waiting = api?.getState(\"waiting\");\n const seeking = api?.getState(\"seeking\");\n const playbackState = api?.getState(\"playbackState\");\n const isLoading = playbackState === \"loading\";\n const showSpinner = waiting || seeking && !api?.getState(\"paused\") || isLoading;\n bufferingIndicator?.classList.toggle(\"sp-buffering--visible\", !!showSpinner);\n errorOverlay?.update();\n };\n const scheduleUpdate = () => {\n if (rafHandle !== null) return;\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n updateControls();\n });\n };\n const showControls = () => {\n if (controlsVisible) {\n resetHideTimer();\n return;\n }\n controlsVisible = true;\n controlBar?.classList.add(\"sp-controls--visible\");\n controlBar?.classList.remove(\"sp-controls--hidden\");\n gradient?.classList.add(\"sp-gradient--visible\");\n progressBar?.show();\n api?.setState(\"controlsVisible\", true);\n resetHideTimer();\n };\n const hideControls = () => {\n const paused = api?.getState(\"paused\");\n if (paused) return;\n controlsVisible = false;\n controlBar?.classList.remove(\"sp-controls--visible\");\n controlBar?.classList.add(\"sp-controls--hidden\");\n gradient?.classList.remove(\"sp-gradient--visible\");\n progressBar?.hide();\n api?.setState(\"controlsVisible\", false);\n };\n const resetHideTimer = () => {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n }\n hideTimeout = setTimeout(hideControls, hideDelay);\n };\n let last_pointer_type = null;\n const handlePointerActivity = (event) => {\n last_pointer_type = event.pointerType;\n };\n const handleInteraction = () => {\n if (last_pointer_type === \"touch\") {\n const gestures = api?.getPlugin(\"gestures\");\n if (gestures?.ownsTapInteraction()) return;\n }\n showControls();\n };\n const handleMouseLeave = () => {\n hideControls();\n };\n const handleKeyDown = (e) => {\n if (!api.container.contains(document.activeElement)) return;\n const activeEl = document.activeElement;\n if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {\n return;\n }\n const video = api.container.querySelector(\"video\");\n if (!video) return;\n const live = api.getState(\"live\");\n const seekableRange = api.getState(\"seekableRange\");\n switch (e.key) {\n case \" \":\n case \"k\":\n e.preventDefault();\n if (video.paused) {\n video.play().catch(() => {\n });\n } else {\n video.pause();\n }\n break;\n case \"m\":\n e.preventDefault();\n video.muted = !video.muted;\n break;\n case \"f\":\n e.preventDefault();\n if (document.fullscreenElement) {\n document.exitFullscreen().catch(() => {\n });\n } else {\n api.container.requestFullscreen?.().catch(() => {\n });\n }\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n if (live && seekableRange) {\n video.currentTime = Math.max(seekableRange.start, video.currentTime - 5);\n } else {\n video.currentTime = Math.max(0, video.currentTime - 5);\n }\n showControls();\n break;\n case \"ArrowRight\":\n e.preventDefault();\n if (live && seekableRange) {\n video.currentTime = Math.min(seekableRange.end, video.currentTime + 5);\n } else {\n video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);\n }\n showControls();\n break;\n case \"ArrowUp\":\n e.preventDefault();\n video.volume = Math.min(1, video.volume + 0.1);\n showControls();\n break;\n case \"ArrowDown\":\n e.preventDefault();\n video.volume = Math.max(0, video.volume - 0.1);\n showControls();\n break;\n }\n };\n return {\n id: \"ui-controls\",\n name: \"UI Controls\",\n type: \"ui\",\n version: \"1.0.0\",\n async init(pluginApi) {\n api = pluginApi;\n styleEl = document.createElement(\"style\");\n styleEl.textContent = styles;\n document.head.appendChild(styleEl);\n if (config.theme) {\n this.setTheme(config.theme);\n }\n const container = api.container;\n if (!container) {\n api.logger.error(\"UI plugin: container not found\");\n return;\n }\n const containerStyle = getComputedStyle(container);\n if (containerStyle.position === \"static\") {\n container.style.position = \"relative\";\n }\n const isPlaying = api.getState(\"playing\");\n gradient = document.createElement(\"div\");\n gradient.className = isPlaying ? \"sp-gradient\" : \"sp-gradient sp-gradient--visible\";\n container.appendChild(gradient);\n bufferingIndicator = document.createElement(\"div\");\n bufferingIndicator.className = \"sp-buffering\";\n bufferingIndicator.innerHTML = icons.spinner;\n bufferingIndicator.setAttribute(\"aria-hidden\", \"true\");\n container.appendChild(bufferingIndicator);\n errorOverlay = new ErrorOverlay(api);\n container.appendChild(errorOverlay.render());\n errorUnsubscribe = api.on(\"error\", (payload) => {\n if (payload?.fatal) {\n const error = api.getState(\"error\") || payload;\n errorOverlay?.show(error);\n }\n });\n reconnectingUnsubscribe = api.on(\"error:reconnecting\", () => {\n errorOverlay?.showReconnecting();\n });\n recoveredUnsubscribe = api.on(\"error:recovered\", () => {\n errorOverlay?.hide();\n });\n progressBar = new ProgressBar(api);\n container.appendChild(progressBar.render());\n if (!isPlaying) {\n progressBar.show();\n }\n controlBar = document.createElement(\"div\");\n controlBar.className = isPlaying ? \"sp-controls sp-controls--hidden\" : \"sp-controls sp-controls--visible\";\n controlBar.setAttribute(\"role\", \"toolbar\");\n controlBar.setAttribute(\"aria-label\", \"Video controls\");\n populateControlBar();\n container.appendChild(controlBar);\n controlRegistryUnsubscribe = onControlRegistered((id) => {\n if (!layout.includes(id)) {\n return;\n }\n api.logger.debug(`Control \"${id}\" registered after init, rebuilding control bar`);\n rebuildControlBar();\n });\n container.addEventListener(\"pointerdown\", handlePointerActivity, { passive: true });\n container.addEventListener(\"pointermove\", handlePointerActivity, { passive: true });\n container.addEventListener(\"mousemove\", handleInteraction);\n container.addEventListener(\"mouseenter\", handleInteraction);\n container.addEventListener(\"mouseleave\", handleMouseLeave);\n container.addEventListener(\"touchstart\", handleInteraction, { passive: true });\n container.addEventListener(\"click\", handleInteraction);\n document.addEventListener(\"keydown\", handleKeyDown);\n stateUnsubscribe = api.subscribeToState(scheduleUpdate);\n document.addEventListener(\"fullscreenchange\", scheduleUpdate);\n updateControls();\n if (!container.hasAttribute(\"tabindex\")) {\n container.setAttribute(\"tabindex\", \"0\");\n }\n controlsVisible = !isPlaying;\n api.setState(\"controlsVisible\", controlsVisible);\n if (isPlaying) {\n resetHideTimer();\n }\n api.logger.debug(\"UI controls plugin initialized\");\n },\n async destroy() {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n stateUnsubscribe?.();\n stateUnsubscribe = null;\n errorUnsubscribe?.();\n errorUnsubscribe = null;\n reconnectingUnsubscribe?.();\n reconnectingUnsubscribe = null;\n recoveredUnsubscribe?.();\n recoveredUnsubscribe = null;\n if (api?.container) {\n api.container.removeEventListener(\"pointerdown\", handlePointerActivity);\n api.container.removeEventListener(\"pointermove\", handlePointerActivity);\n api.container.removeEventListener(\"mousemove\", handleInteraction);\n api.container.removeEventListener(\"mouseenter\", handleInteraction);\n api.container.removeEventListener(\"mouseleave\", handleMouseLeave);\n api.container.removeEventListener(\"touchstart\", handleInteraction);\n api.container.removeEventListener(\"click\", handleInteraction);\n }\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.removeEventListener(\"fullscreenchange\", scheduleUpdate);\n controlRegistryUnsubscribe?.();\n controlRegistryUnsubscribe = null;\n controls.forEach((c) => c.destroy());\n controls = [];\n progressBar?.destroy();\n progressBar = null;\n errorOverlay?.destroy();\n errorOverlay = null;\n controlBar?.remove();\n controlBar = null;\n gradient?.remove();\n gradient = null;\n bufferingIndicator?.remove();\n bufferingIndicator = null;\n styleEl?.remove();\n styleEl = null;\n api?.logger.debug(\"UI controls plugin destroyed\");\n },\n // Public API\n show() {\n showControls();\n },\n hide() {\n controlsVisible = false;\n controlBar?.classList.remove(\"sp-controls--visible\");\n controlBar?.classList.add(\"sp-controls--hidden\");\n gradient?.classList.remove(\"sp-gradient--visible\");\n progressBar?.hide();\n api?.setState(\"controlsVisible\", false);\n },\n setTheme(theme) {\n const root = api?.container || document.documentElement;\n if (theme.primaryColor) {\n root.style.setProperty(\"--sp-color\", theme.primaryColor);\n }\n if (theme.accentColor) {\n root.style.setProperty(\"--sp-accent\", theme.accentColor);\n }\n if (theme.backgroundColor) {\n root.style.setProperty(\"--sp-bg\", theme.backgroundColor);\n }\n if (theme.controlBarHeight) {\n root.style.setProperty(\"--sp-control-height\", `${theme.controlBarHeight}px`);\n }\n if (theme.iconSize) {\n root.style.setProperty(\"--sp-icon-size\", `${theme.iconSize}px`);\n }\n },\n getControlBar() {\n return controlBar;\n }\n };\n}\nvar index_default = uiPlugin;\nexport {\n index_default as default,\n formatLiveTime,\n formatTime,\n getControlFactory,\n icons,\n registerControl,\n resetControlRegistry,\n styles,\n uiPlugin,\n unregisterControl\n};\n"],"names":[],"mappings":"AAqoFA,IAAI,WAA2B,oBAAI,IAAG;AACtC,IAAI,YAA4B,oBAAI,IAAG;AACvC,SAAS,gBAAgB,IAAI,SAAS;AACpC,WAAS,IAAI,IAAI,OAAO;AACxB,aAAW,YAAY,WAAW;AAChC,aAAS,EAAE;AAAA,EACb;AACF;"}