UNPKG

@mottosports/motto-video-player

Version:

React video player component for the Motto platform, powered by Shaka Player

1,397 lines (1,384 loc) 194 kB
"use client"; "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { BigPlayIcon: () => BigPlayIcon, ConcurrencyConnectionState: () => ConcurrencyConnectionState, CreativeWork: () => CreativeWork, Event: () => Event, Player: () => Player, PlayerProvider: () => PlayerProvider, QueryProvider: () => QueryProvider, SkipBackIcon: () => SkipBackIcon, SkipForwardIcon: () => SkipForwardIcon, Video: () => Video, clearDeviceId: () => clearDeviceId, getDeviceId: () => getDeviceId, queryClient: () => queryClient, useConcurrencySession: () => useConcurrencySession, usePlayerStore: () => usePlayerStore, usePlayerStoreApi: () => usePlayerStoreApi }); module.exports = __toCommonJS(index_exports); var import_controls = require("shaka-player/dist/controls.css"); // #style-inject:#style-inject function styleInject(css, { insertAt } = {}) { if (!css || typeof document === "undefined") return; const head = document.head || document.getElementsByTagName("head")[0]; const style = document.createElement("style"); style.type = "text/css"; if (insertAt === "top") { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } // src/styles.css styleInject(`@layer components { video::-webkit-media-controls { display: none !important; } video::-webkit-media-controls-panel { display: none !important; } video::-webkit-media-controls-play-button { display: none !important; } video::-webkit-media-controls-timeline { display: none !important; } video::-webkit-media-controls-current-time-display { display: none !important; } video::-webkit-media-controls-time-remaining-display { display: none !important; } video::-webkit-media-controls-mute-button { display: none !important; } video::-webkit-media-controls-volume-slider { display: none !important; } video::-webkit-media-controls-fullscreen-button { display: none !important; } video::-webkit-media-controls-overlay-play-button { display: none !important; } video::-moz-media-controls { display: none !important; } video { outline: none !important; } video[controls] { -webkit-appearance: none !important; appearance: none !important; } video::-webkit-media-controls-enclosure { display: none !important; } video::-webkit-media-controls-start-playback-button { display: none !important; } video[controls]::-webkit-media-controls, video[controls]::-webkit-media-controls-panel, video[controls]::-webkit-media-controls-play-button, video[controls]::-webkit-media-controls-timeline, video[controls]::-webkit-media-controls-current-time-display, video[controls]::-webkit-media-controls-time-remaining-display, video[controls]::-webkit-media-controls-mute-button, video[controls]::-webkit-media-controls-volume-slider, video[controls]::-webkit-media-controls-fullscreen-button, video[controls]::-webkit-media-controls-overlay-play-button, video[controls]::-webkit-media-controls-enclosure, video[controls]::-webkit-media-controls-start-playback-button { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } video[controls]::-moz-media-controls { display: none !important; visibility: hidden !important; opacity: 0 !important; } .motto-video-container { @apply relative w-full; min-height: 300px; } @supports (aspect-ratio: 16/9) { .motto-video-container { min-height: auto; } } .motto-video-responsive { @apply absolute top-0 left-0 w-full h-full; } .motto-skip-button { @apply absolute top-1/2 -translate-y-1/2 bg-black/70 text-white border-0 rounded-full w-16 h-16 text-2xl cursor-pointer flex items-center justify-center transition-all duration-200 z-10 opacity-80 hover:opacity-100 hover:scale-110 active:scale-95; } .motto-skip-button-back { @apply left-5; } .motto-skip-button-forward { @apply right-5; } } .shaka-seek-bar-container { height: 6px !important; width: 100% !important; margin: 8px 0 !important; border-radius: 4px !important; position: relative !important; border-top: none !important; border-bottom: none !important; box-shadow: none !important; } .shaka-seek-bar { height: 6px !important; width: 100% !important; -webkit-appearance: none !important; appearance: none !important; background: transparent !important; cursor: pointer !important; border: none !important; outline: none !important; position: absolute !important; top: 0 !important; left: 0 !important; border-radius: 4px !important; } .shaka-seek-bar::-webkit-slider-runnable-track { height: 6px !important; background: transparent !important; border-radius: 4px !important; border: none !important; } .shaka-seek-bar::-moz-range-track { height: 6px !important; background: transparent !important; border-radius: 4px !important; border: none !important; } .shaka-seek-bar::-webkit-slider-thumb { -webkit-appearance: none !important; appearance: none !important; width: 16px !important; height: 16px !important; border-radius: 50% !important; background: #ffffff !important; cursor: pointer !important; border: 2px solid #ffffff !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important; margin-top: -4px !important; } .shaka-seek-bar::-moz-range-thumb { width: 16px !important; height: 16px !important; border-radius: 50% !important; background: #ffffff !important; cursor: pointer !important; border: 2px solid #ffffff !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important; margin-top: -4px !important; } .motto-skip-back-button, .motto-skip-forward-button, .motto-native-skip-button { background: transparent !important; border: none !important; padding: 4px !important; margin: 0px !important; cursor: pointer !important; color: #ffffff !important; transition: all 0.2s ease !important; min-width: 32px !important; height: 32px !important; display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 4px !important; width: 25px; } .motto-skip-back-button:hover, .motto-skip-forward-button:hover, .motto-native-skip-button:hover { opacity: 0.8 !important; background: transparent !important; transform: scale(1.05) !important; } .motto-skip-back-button:active, .motto-skip-forward-button:active, .motto-native-skip-button:active { transform: scale(0.95) !important; } .motto-skip-back-button svg, .motto-skip-forward-button svg, .motto-native-skip-button svg { width: 24px !important; height: 24px !important; } .shaka-spinner-svg { color: white !important; fill: white !important; } .shaka-spinner-path { stroke: white !important; fill: none !important; } .shaka-spinner-container { color: white !important; } .shaka-buffering-spinner { color: white !important; fill: white !important; } .shaka-buffering-spinner svg { color: white !important; fill: white !important; } .shaka-buffering-spinner path { stroke: white !important; fill: none !important; } [data-shaka-player-container] .shaka-spinner, [data-shaka-player-container] .spinner { color: white !important; border-color: white !important; } .material-icons.shaka-spinner { color: white !important; } .shaka-controls-container .shaka-spinner, .shaka-video-container .shaka-spinner { color: white !important; fill: white !important; } .shaka-controls-container .shaka-spinner svg, .shaka-video-container .shaka-spinner svg { color: white !important; fill: white !important; } .shaka-controls-container .shaka-spinner path, .shaka-video-container .shaka-spinner path { stroke: white !important; } .motto-video-loading-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient( 135deg, #1a1a1a 0%, #2d2d2d 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10; transition: opacity 0.3s ease; } .motto-video-loading-overlay.hidden { opacity: 0; pointer-events: none; } .motto-video-loading-content { text-align: center; color: white; } .motto-video-loading-icon { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.7; } .motto-video-loading-text { font-size: 16px; font-weight: 500; margin-bottom: 8px; } .motto-video-loading-subtext { font-size: 14px; opacity: 0.7; } @keyframes pulse-live { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } 100% { opacity: 1; transform: scale(1); } } .shaka-play-button { background: rgba(255, 255, 255, 0.1) !important; border: none !important; color: white !important; padding: 10px !important; border-radius: 100% !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; justify-content: center !important; min-width: 55px !important; height: 55px !important; } .shaka-play-button-container { background: transparent; transition: all 0.2s ease !important; } .motto-video-container:not(.no-cursor) .shaka-play-button-container { background: rgba(0, 0, 0, 0.3); transition: all 0.s ease !important; } .shaka-play-button:hover { background: rgba(255, 255, 255, 0.2) !important; transform: scale(1.05) !important; } .shaka-play-button:active { transform: scale(0.95) !important; } .shaka-play-button > * { display: none !important; } .shaka-play-button::after { content: "" !important; width: 35px !important; height: 35px !important; background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path fill-rule="evenodd" d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" clip-rule="evenodd" /></svg>') !important; background-repeat: no-repeat !important; background-size: contain !important; background-position: center !important; display: block !important; } .shaka-play-button[aria-label*=Play]::after { background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path fill-rule="evenodd" d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" clip-rule="evenodd" /></svg>') !important; } .shaka-play-button[aria-label*=Pause]::after { background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path fill-rule="evenodd" d="M6.75 5.25a.75.75 0 0 1 .75-.75H9a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H7.5a.75.75 0 0 1-.75-.75V5.25Zm7.5 0A.75.75 0 0 1 15 4.5h1.5a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V5.25Z" clip-rule="evenodd" /></svg>') !important; } .motto-video-container { background: #111111; } .motto-video-container video { width: 100% !important; height: 100% !important; margin-left: auto !important; margin-right: auto !important; } html[dir=rtl] .shaka-controls-container, html[dir=rtl] .shaka-bottom-controls, html[dir=rtl] .shaka-controls-button-panel { direction: ltr !important; } html[dir=rtl] .shaka-overflow-menu, html[dir=rtl] .shaka-settings-menu { direction: rtl !important; text-align: right !important; } html[dir=rtl] .shaka-overflow-menu .shaka-overflow-button .material-svg-icon:not(:first-child) { margin-right: 12px !important; margin-left: 0 !important; } html[dir=rtl] .shaka-overflow-menu .shaka-overflow-button .material-svg-icon:first-child { margin-right: 0 !important; margin-left: 12px !important; } .shaka-hidden-fast-forward-container, .shaka-hidden-rewind-container { pointer-events: none !important; display: none !important; } `); // src/Player.tsx var import_react21 = require("react"); // src/hooks/useShakaPlayer.ts var import_react = require("react"); var import_shaka_player = __toESM(require("shaka-player/dist/shaka-player.ui")); // src/utils/devices.ts var isAppleDevice = () => { if (typeof navigator === "undefined") return false; const ua = navigator.userAgent || ""; const isIOS = /iPad|iPhone|iPod/.test(ua) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1; const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS|Edg/.test(ua); const isMacSafari = /Macintosh/.test(ua) && isSafari; return isIOS || isMacSafari; }; var isPlayReadySupported = () => { if (typeof navigator === "undefined" || typeof window === "undefined") { return false; } if (!navigator.requestMediaKeySystemAccess) { return false; } const userAgent = navigator.userAgent || ""; const isXbox = /Xbox/.test(userAgent); const isEdge = /Edg/.test(userAgent); const isIE = /Trident|MSIE/.test(userAgent); const isWindows = /Windows/.test(userAgent); return isXbox || (isEdge || isIE) && isWindows; }; var supportsWidevinePersistentLicenses = () => { return false; if (typeof navigator === "undefined") { return false; } const userAgent = navigator.userAgent || ""; const isChromeMatch = userAgent.match(/Chrome\/(\d+)/); if (!isChromeMatch) { return false; } if (/Edg|OPR|Opera/.test(userAgent)) { return false; } const chromeVersion = parseInt(isChromeMatch[1], 10); if (chromeVersion < 64) { return false; } const isMacOS = /Mac OS X|Macintosh/.test(userAgent); const isWindows = /Windows/.test(userAgent); return isMacOS || isWindows; }; // src/hooks/useShakaPlayer.ts var import_mux_data_shakaplayer = __toESM(require("@mux/mux-data-shakaplayer")); // package.json var version = "1.1.0"; // src/utils/licenseCache.ts var PERSISTENT_LICENSE_PREFIX = "motto_lic_"; var LICENSE_EXPIRY_MS = 2 * 60 * 60 * 1e3; var LICENSE_MAX_RETRY_ATTEMPTS = 10; var getAllLicenseCacheKeys = () => { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(PERSISTENT_LICENSE_PREFIX)) { keys.push(key); } } return keys; }; var getAllLicenseCacheEntries = () => { const keys = getAllLicenseCacheKeys(); const entries = []; for (const key of keys) { try { const stored = localStorage.getItem(key); if (stored) { const data = JSON.parse(stored); entries.push({ key, data }); } } catch (error) { console.warn(`Failed to parse license cache entry for key ${key}:`, error); localStorage.removeItem(key); } } return entries; }; var evictLRUEntry = () => { const entries = getAllLicenseCacheEntries(); if (entries.length === 0) { return false; } entries.sort((a, b) => { const oldestA = Math.min(...a.data.map((session) => session.timestamp)); const oldestB = Math.min(...b.data.map((session) => session.timestamp)); return oldestA - oldestB; }); const lruEntry = entries[0]; localStorage.removeItem(lruEntry.key); console.log(`Evicted LRU license cache entry: ${lruEntry.key}`); return true; }; var storeWithQuotaHandling = (key, data) => { let attempts = 0; while (attempts < LICENSE_MAX_RETRY_ATTEMPTS) { try { localStorage.setItem(key, data); return true; } catch (error) { if (error instanceof DOMException && (error.code === 22 || // QUOTA_EXCEEDED_ERR error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED")) { console.warn(`QuotaExceededError on attempt ${attempts + 1}, attempting LRU eviction...`); if (!evictLRUEntry()) { console.error("No more entries to evict, storage operation failed"); return false; } attempts++; } else { console.error("Failed to store license cache data:", error); return false; } } } console.error(`Failed to store license cache data after ${LICENSE_MAX_RETRY_ATTEMPTS} eviction attempts`); return false; }; var managePersistentLicenseStorage = (playlistId, licenseCacheKey, newSessionMetadata) => { try { const storageKey = `${PERSISTENT_LICENSE_PREFIX}${playlistId}_${licenseCacheKey}`; const stored = localStorage.getItem(storageKey); let existingSessions = []; if (stored) { try { existingSessions = JSON.parse(stored); } catch (parseError) { console.warn("Failed to parse existing persistent license data:", parseError); existingSessions = []; } } const now = Date.now(); let validSessions = existingSessions.filter( (session) => now - session.timestamp < LICENSE_EXPIRY_MS ); if (newSessionMetadata && newSessionMetadata.length > 0) { const newSessions = newSessionMetadata.map((session) => { const uint8Array = new Uint8Array(session.initData); const binaryString = Array.from(uint8Array).map((byte) => String.fromCharCode(byte)).join(""); return { sessionId: session.sessionId, initData: btoa(binaryString), initDataType: session.initDataType, keySystem: session.keySystem, timestamp: now }; }); const newSessionIds = new Set(newSessions.map((s) => s.sessionId)); validSessions = validSessions.filter((session) => !newSessionIds.has(session.sessionId)); validSessions = [...validSessions, ...newSessions]; } if (validSessions.length === 0) { localStorage.removeItem(storageKey); } else { const dataToStore = JSON.stringify(validSessions); const success = storeWithQuotaHandling(storageKey, dataToStore); if (!success) { console.error(`Failed to store license cache for ${storageKey} after quota handling`); return validSessions; } } return validSessions; } catch (error) { console.warn("Failed to manage persistent license storage:", error); return []; } }; var storePersistentLicense = (playlistId, licenseCacheKey, sessionMetadata) => { managePersistentLicenseStorage(playlistId, licenseCacheKey, sessionMetadata); }; var retrievePersistentLicense = (playlistId, licenseCacheKey) => { try { const validSessions = managePersistentLicenseStorage(playlistId, licenseCacheKey); return validSessions.map((session) => ({ sessionId: session.sessionId, initData: Uint8Array.from(atob(session.initData), (c) => c.charCodeAt(0)).buffer, initDataType: session.initDataType, keySystem: session.keySystem })); } catch (error) { console.warn("Failed to retrieve persistent license:", error); return []; } }; var clearAllPersistentLicenses = () => { try { const keys = getAllLicenseCacheKeys(); for (const key of keys) { localStorage.removeItem(key); } console.log(`Cleared ${keys.length} persistent license cache entries`); return keys.length; } catch (error) { console.error("Failed to clear persistent licenses:", error); return 0; } }; // src/hooks/useShakaPlayer.ts var stripUrlQuery = (url) => { if (!url) return url; const queryIndex = url.search(/[?#]/); return queryIndex === -1 ? url : url.slice(0, queryIndex); }; var createMuxScrubbingPlayer = (player) => new Proxy(player, { get(target, prop) { if (prop === "getAssetUri") { return () => stripUrlQuery(target.getAssetUri?.() ?? ""); } if (prop === "getNetworkingEngine") { return () => { const engine = target.getNetworkingEngine(); if (!engine) return engine; return new Proxy(engine, { get(engineTarget, engineProp) { if (engineProp === "registerResponseFilter") { return (filter) => engineTarget.registerResponseFilter( (type, response, ...rest) => filter( type, { ...response, uri: stripUrlQuery(response?.uri) }, ...rest ) ); } const value2 = engineTarget[engineProp]; return typeof value2 === "function" ? value2.bind(engineTarget) : value2; } }); }; } const value = target[prop]; if (typeof value !== "function" || prop === "constructor") return value; return value.bind(target); } }); var SEGMENT_ERROR_MESSAGE_KEYS = { "965": "ERROR_CODE_IP_CHANGED", "970": "ERROR_CODE_VPN_DETECTED", "971": "ERROR_CODE_ANONYMOUS_PROXY" }; var normalizeSource = (src) => { if (typeof src === "string") { return { id: "", url: src, format: "auto", drm: {} }; } return src; }; var useShakaPlayer = ({ src, shakaConfig, drmConfig, onError, onPlaybackBlocked, onFatalError, onPlayerReady, muxConfig, onMuxReady, onMuxDataUpdate, publicKey, mottoToken, hasAds = false, hasSystem73 = false, apiToken }) => { const playerRef = (0, import_react.useRef)(null); const [isRetrying, setIsRetrying] = (0, import_react.useState)(false); const videoElementRef = (0, import_react.useRef)(null); const muxMonitorRef = (0, import_react.useRef)(null); const destroyInProgressRef = (0, import_react.useRef)(null); const isDestroyingRef = (0, import_react.useRef)(false); const initSequenceRef = (0, import_react.useRef)(0); const activeInitIdRef = (0, import_react.useRef)(null); const waitingForKeyTimerRef = (0, import_react.useRef)(null); const waitingForKeyHandlerRef = (0, import_react.useRef)(null); const playbackResumedHandlerRef = (0, import_react.useRef)(null); const usingPersistentLicenseRef = (0, import_react.useRef)(false); const storedPersistentThisLoadRef = (0, import_react.useRef)(false); const drmExpirationHandlerRef = (0, import_react.useRef)(null); const segmentErrorHandledRef = (0, import_react.useRef)(false); const getManifestUrl = (0, import_react.useCallback)(() => { const playlistSrc = normalizeSource(src); let manifestUrl = playlistSrc.url; const isDRM = Boolean(playlistSrc.drm?.widevine || playlistSrc.drm?.fairplay || playlistSrc.drm?.playready); if (isDRM) { const isPlayReady = isPlayReadySupported(); if (isAppleDevice() && playlistSrc.drm?.fairplay?.certificateUrl) { manifestUrl = playlistSrc.drm.fairplay.playlistUrl; } else if (isPlayReady && playlistSrc.drm?.playready?.licenseUrl) { manifestUrl = playlistSrc.drm.playready.playlistUrl; } else { manifestUrl = playlistSrc.drm?.widevine?.playlistUrl || ""; } } return manifestUrl; }, [src]); const handleSegmentErrorCode = (0, import_react.useCallback)((errorCode, responseBody) => { let code = errorCode === void 0 || errorCode === null ? "" : String(errorCode).trim(); if (!code && typeof responseBody === "string") { code = /^error\s+(\d+)$/i.exec(responseBody.trim())?.[1] ?? ""; } if (!code) return false; if (code !== "940" && !SEGMENT_ERROR_MESSAGE_KEYS[code]) return false; if (segmentErrorHandledRef.current) return true; segmentErrorHandledRef.current = true; if (code === "940") { console.warn("Segment response returned X-Error-Code 940 (token expired), reloading page..."); window.location.reload(); return true; } console.warn(`Segment response returned X-Error-Code ${code}, blocking playback`); try { videoElementRef.current?.pause(); } catch { } onPlaybackBlocked?.(SEGMENT_ERROR_MESSAGE_KEYS[code]); return true; }, [onPlaybackBlocked]); const handleSegmentHttpError = (0, import_react.useCallback)((error) => { if (error?.code !== 1001 || !Array.isArray(error?.data)) return false; if (error.data[4] !== import_shaka_player.default.net.NetworkingEngine.RequestType.SEGMENT) return false; return handleSegmentErrorCode(error.data[3]?.["x-error-code"], error.data[2]); }, [handleSegmentErrorCode]); const initializePlayerInternal = (0, import_react.useCallback)(async (video) => { try { if (destroyInProgressRef.current) { try { await destroyInProgressRef.current; } catch { } } const myInitId = ++initSequenceRef.current; activeInitIdRef.current = myInitId; videoElementRef.current = video; import_shaka_player.default.polyfill.installAll(); if (!import_shaka_player.default.Player.isBrowserSupported()) { throw new Error("Browser not supported by Shaka Player"); } if (isDestroyingRef.current) { return; } const player = new import_shaka_player.default.Player(); playerRef.current = player; await player.attach(video); const defaultConfig = { manifest: { // Override availability window to allow DVR window to grow from start // Set to a very large value (24 hours in seconds) to effectively allow unlimited growth availabilityWindowOverride: 86400 // 24 hours in seconds }, streaming: { // Allow seeking to any point within the availability window safeSeekOffset: 5, // 5 seconds from live edge to prevent buffering // Increase tolerance for manifest timing inaccuracies in live streams inaccurateManifestTolerance: 2 } }; player.configure(defaultConfig); if (shakaConfig) { player.configure(shakaConfig); } const playlistSrc = normalizeSource(src); const manifestUrl = getManifestUrl(); let playlistId = playlistSrc.id; const isDRM = Boolean(playlistSrc.drm?.widevine || playlistSrc.drm?.fairplay || playlistSrc.drm?.playready); storedPersistentThisLoadRef.current = false; segmentErrorHandledRef.current = false; if (activeInitIdRef.current !== myInitId || isDestroyingRef.current) { try { await player.destroy(); } catch { } if (playerRef.current === player) playerRef.current = null; return; } let storedSessionsMetadata = []; if (isDRM && playlistId) { storedSessionsMetadata = retrievePersistentLicense(playlistId, playlistSrc.drm?.licenseCacheKey ?? ""); } if (isDRM) { const drmConfig2 = { servers: { "com.widevine.alpha": playlistSrc.drm?.widevine?.licenseUrl, "com.microsoft.playready": playlistSrc.drm?.playready?.licenseUrl, "com.apple.fps": playlistSrc.drm?.fairplay?.licenseUrl }, keySystemsMapping: { // Fall back or reroute to the platform's recommended PlayReady CDM if needed "com.microsoft.playready": "com.microsoft.playready.recommendation" }, ...playlistSrc.drm?.fairplay && { advanced: { "com.apple.fps": { serverCertificateUri: playlistSrc.drm.fairplay.certificateUrl } } } }; drmConfig2.advanced = { ...drmConfig2.advanced, "com.widevine.alpha": { ...drmConfig2.advanced?.["com.widevine.alpha"], sessionType: supportsWidevinePersistentLicenses() ? "persistent-license" : "temporary" }, "com.microsoft.playready": { ...drmConfig2.advanced?.["com.microsoft.playready"], sessionType: "persistent-license" // PlayReady seems to always require persistent-license (temporary won't work, not compatible with license server response) }, "com.apple.fps": { ...drmConfig2.advanced?.["com.apple.fps"], sessionType: "temporary" // FairPlay always uses temporary sessions - Safari won't play with persistent-license } }; if (storedSessionsMetadata.length > 0) { drmConfig2.persistentSessionOnlinePlayback = true; drmConfig2.persistentSessionsMetadata = storedSessionsMetadata.map((session) => ({ sessionId: session.sessionId, initData: new Uint8Array(session.initData), initDataType: session.initDataType })); } usingPersistentLicenseRef.current = storedSessionsMetadata.length > 0; player.configure({ drm: drmConfig2 }); const clearWaitingForKeyTimer = () => { if (waitingForKeyTimerRef.current !== null) { window.clearTimeout(waitingForKeyTimerRef.current); waitingForKeyTimerRef.current = null; } }; const onPlaybackResumed = () => { clearWaitingForKeyTimer(); }; const onWaitingForKey = () => { if (!usingPersistentLicenseRef.current) return; if (isRetrying) return; clearWaitingForKeyTimer(); waitingForKeyTimerRef.current = window.setTimeout(async () => { try { if (isRetrying) return; console.warn("Stuck waiting for decryption key; attempting license cache reset and reload..."); const clearedCount = clearAllPersistentLicenses(); if (clearedCount > 0 && playerRef.current) { setIsRetrying(true); await playerRef.current.load(getManifestUrl()); console.log("Reloaded manifest after clearing cached licenses"); } else { console.warn("No cached licenses found to clear, skipping reload"); } } catch (e) { console.error("Failed during recovery from waitingforkey state:", e); onError?.(e); } finally { setIsRetrying(false); clearWaitingForKeyTimer(); } }, 3e3); }; try { video.addEventListener("waitingforkey", onWaitingForKey); video.addEventListener("playing", onPlaybackResumed); video.addEventListener("ended", onPlaybackResumed); video.addEventListener("pause", onPlaybackResumed); waitingForKeyHandlerRef.current = onWaitingForKey; playbackResumedHandlerRef.current = onPlaybackResumed; } catch (e) { console.warn("Failed to attach waitingforkey/playback listeners:", e); } const netEngine = player.getNetworkingEngine(); if (netEngine) { netEngine.registerRequestFilter((type, request) => { switch (type) { case import_shaka_player.default.net.NetworkingEngine.RequestType.LICENSE: if (publicKey) { request.headers["authorization"] = `Bearer ${publicKey}`; } if (mottoToken) { request.headers["x-motto-token"] = mottoToken; } break; case import_shaka_player.default.net.NetworkingEngine.RequestType.MANIFEST: case import_shaka_player.default.net.NetworkingEngine.RequestType.SEGMENT: request.allowCrossSiteCredentials = true; break; } }); netEngine.registerResponseFilter((type, response) => { if (type === import_shaka_player.default.net.NetworkingEngine.RequestType.LICENSE) { const ks = player.keySystem && player.keySystem(); if (ks === "com.apple.fps") { const responseText = import_shaka_player.default.util.StringUtils.fromUTF8(response.data); response.data = import_shaka_player.default.util.Uint8ArrayUtils.fromBase64(responseText).buffer; } } }); } } const segmentNetEngine = player.getNetworkingEngine(); if (segmentNetEngine) { segmentNetEngine.registerResponseFilter((type, response) => { if (type === import_shaka_player.default.net.NetworkingEngine.RequestType.SEGMENT) { handleSegmentErrorCode(response?.headers?.["x-error-code"]); } }); } if (apiToken) { const netEngine = player.getNetworkingEngine(); if (netEngine) { netEngine.registerRequestFilter((type, request) => { if (type === import_shaka_player.default.net.NetworkingEngine.RequestType.MANIFEST) { request.headers["authorization"] = `Bearer ${apiToken}`; } }); } } if (isDRM && playlistId) { const onDRMSessionUpdate = () => { try { if (storedPersistentThisLoadRef.current) return; const activeDrmSessions = player.getActiveSessionsMetadata?.(); if (!activeDrmSessions) return; const persistentSessions = activeDrmSessions.filter( (session) => session.sessionType === "persistent-license" ); if (persistentSessions.length > 0) { const sessionsToStore = persistentSessions.map((session) => ({ sessionId: session.sessionId, initData: session.initData, initDataType: session.initDataType, keySystem: session.keySystem || "com.widevine.alpha" })); storePersistentLicense(playlistId, playlistSrc.drm?.licenseCacheKey ?? "", sessionsToStore); storedPersistentThisLoadRef.current = true; } } catch (e) { console.warn("Failed to persist licenses on expiration update:", e); } }; try { player.addEventListener("drmsessionupdate", onDRMSessionUpdate); drmExpirationHandlerRef.current = onDRMSessionUpdate; } catch (e) { console.warn("Failed to attach drmsessionupdate listener:", e); } } player?.addEventListener("error", (event) => { const error = event.detail; if (error?.code === 7e3) { return; } if (handleSegmentHttpError(error)) { return; } if (error?.code >= 6e3 && error?.code < 7e3 && !isRetrying && videoElementRef.current) { console.warn(`DRM error detected (code: ${error.code}), checking for cached licenses...`); const clearedCount = clearAllPersistentLicenses(); if (clearedCount > 0) { console.warn(`Cleared ${clearedCount} cached licenses, retrying...`); setIsRetrying(true); setTimeout(async () => { try { const video2 = videoElementRef.current; const currentPlayer = playerRef.current; if (video2 && currentPlayer) { console.log("Reloading manifest after license cache clear..."); await currentPlayer.load(getManifestUrl()); console.log("Manifest reloaded successfully"); } } catch (retryError) { console.error("Failed to retry after license clear:", retryError); onError?.(retryError); onFatalError?.(); } finally { setIsRetrying(false); } }, 500); return; } else { console.warn(`No cached licenses found to clear for error code ${error.code}, not retrying to avoid infinite loop`); } } console.error("Shaka Player Error:", error); onError?.(new Error(`Shaka Player Error: ${error.message || "Unknown error"}`)); if (error?.severity === import_shaka_player.default.util.Error.Severity.CRITICAL) { onFatalError?.(); } }); if (muxConfig) { try { const playerInitTime = import_mux_data_shakaplayer.default.utils.now(); const muxOptions = { debug: muxConfig.debug || false, disableCookies: muxConfig.disableCookies || false, respectDoNotTrack: muxConfig.respectDoNotTrack || false, automaticErrorTracking: muxConfig.automaticErrorTracking !== false, ...muxConfig.beaconCollectionDomain && { beaconCollectionDomain: muxConfig.beaconCollectionDomain }, ...muxConfig.errorTranslator && { errorTranslator: muxConfig.errorTranslator }, data: { env_key: muxConfig.envKey, player_name: muxConfig?.metadata?.player_name, player_version: version, player_init_time: playerInitTime, video_title: muxConfig?.metadata?.video_title ?? "", video_id: muxConfig?.metadata?.video_id ?? "", viewer_user_id: muxConfig?.metadata?.viewer_user_id ?? "", ...muxConfig.metadata } }; muxMonitorRef.current = (0, import_mux_data_shakaplayer.default)(createMuxScrubbingPlayer(player), muxOptions, import_shaka_player.default); onMuxReady?.(); } catch (error) { console.error("Failed to initialize Mux Analytics:", error); } } if (activeInitIdRef.current !== myInitId || isDestroyingRef.current) { try { await player.destroy(); } catch { } if (playerRef.current === player) playerRef.current = null; return; } if (!hasAds && !hasSystem73) { await player.load(manifestUrl); } else { } onPlayerReady?.(player); return player; } catch (error) { if (error?.code === 7e3) { return; } if (handleSegmentHttpError(error)) { throw error; } console.error("Error initializing Shaka Player:", error); onError?.(error); onFatalError?.(); throw error; } }, [shakaConfig, drmConfig, src, onError, onFatalError, onPlayerReady, muxConfig, onMuxReady, isRetrying, getManifestUrl, apiToken, handleSegmentErrorCode, handleSegmentHttpError]); const initializePlayer = (0, import_react.useCallback)(async (video) => { return initializePlayerInternal(video); }, [initializePlayerInternal]); const handleMuxError = (0, import_react.useCallback)((error) => { try { muxMonitorRef.current?.loadErrorHandler?.(error); } catch (e) { console.warn("Failed to report load error to Mux:", e); } }, []); const loadManifest = (0, import_react.useCallback)(async () => { const player = playerRef.current; if (!player) { console.warn("\u26A0\uFE0F [Shaka] Cannot load manifest: player not initialized"); return; } try { const manifestUrl = getManifestUrl(); await player.load(manifestUrl); } catch (error) { if (error?.code === 7e3) { return; } if (handleSegmentHttpError(error)) { throw error; } console.error("\u274C [Shaka] Error loading manifest:", error); onError?.(error); onFatalError?.(); throw error; } }, [getManifestUrl, onError, onFatalError, handleSegmentHttpError]); const destroyPlayer = (0, import_react.useCallback)(async () => { const playerInstance = playerRef.current; if (playerInstance) { try { isDestroyingRef.current = true; activeInitIdRef.current = null; if (videoElementRef.current) { try { if (waitingForKeyHandlerRef.current) { videoElementRef.current.removeEventListener("waitingforkey", waitingForKeyHandlerRef.current); } if (playbackResumedHandlerRef.current) { videoElementRef.current.removeEventListener("playing", playbackResumedHandlerRef.current); videoElementRef.current.removeEventListener("ended", playbackResumedHandlerRef.current); videoElementRef.current.removeEventListener("pause", playbackResumedHandlerRef.current); } } catch (e) { console.warn("Error removing media event listeners:", e); } } if (waitingForKeyTimerRef.current !== null) { window.clearTimeout(waitingForKeyTimerRef.current); waitingForKeyTimerRef.current = null; } try { if (drmExpirationHandlerRef.current && playerInstance.removeEventListener) { playerInstance.removeEventListener("drmsessionupdate", drmExpirationHandlerRef.current); } } catch (e) { console.warn("Error removing DRM expiration listener:", e); } finally { drmExpirationHandlerRef.current = null; } const performDestroy = async () => { try { await playerInstance.destroy(); } finally { try { if (videoElementRef.current) { videoElementRef.current.removeAttribute("src"); videoElementRef.current.load(); } } catch { } } }; destroyInProgressRef.current = performDestroy(); await destroyInProgressRef.current; } catch (error) { console.warn("Error destroying Shaka Player:", error); } finally { if (playerRef.current === playerInstance) { playerRef.current = null; } muxMonitorRef.current = null; storedPersistentThisLoadRef.current = false; isDestroyingRef.current = false; destroyInProgressRef.current = null; } } }, [playerRef]); return { playerRef, initializePlayer, loadManifest, destroyPlayer, handleMuxError, isRetrying }; }; // src/hooks/usePlayerInitialization.ts var import_react2 = require("react"); var usePlayerInitialization = ({ videoRef, playerRef, isScriptsLoaded, setIsInitialLoading, system73Config, shakaConfig, imaConfig, initializeSystem73, initializePlayer, loadManifest, setupEventListeners, setupQualityTracking, configureQuality, updateAvailableQualities, initializeUI, initializeAds, handleMuxError, cleanupEventListeners, cleanupAdEventListeners, destroyUI, destroyMux, destroyPlayer, src, hasPlaylist, managedMode, bfResetKey }) => { (0, import_react2.useEffect)(() => { const video = videoRef.current; if (!video || !isScriptsLoaded) return; const onLoadStart = () => setIsInitialLoading(true); const onCanPlay = () => setIsInitialLoading(false); const onPlaying = () => setIsInitialLoading(false); video.addEventListener("loadstart", onLoadStart); video.addEventListener("canplay", onCanPlay); video.addEventListener("playing", onPlaying); let currentShakaConfig = shakaConfig; const initialize = async () => { try { setIsInitialLoading(true); let system73Wrapper = null; if (system73Config?.apiKey && window.S73ShakaPlayerWrapper) { const playerConfig = { ...currentShakaConfig }; system73Wrapper = initializeSystem73(playerConfig); if (system73Wrapper) { currentShakaConfig = playerConfig; } } await initializePlayer(video); if (system73Wrapper && playerRef.current) { system73Wrapper.wrapPlayer(playerRef.current); if (!imaConfig?.adTagUrl) { await loadManifest(); } } setupEventListeners(); setupQualityTracking(); configureQuality(); setTimeout(() => { updateAvailableQualities(); }, 100); await initializeUI(); if (imaConfig?.adTagUrl && window.google?.ima) { await initializeAds(); } } catch (error) { console.error("Player initialization error:", error); handleMuxError(error); } }; initialize(); return () => { video.removeEventListener("loadstart", onLoadStart); video.removeEventListener("canplay", onCanPlay); video.removeEventListener("playing", onPlaying); cleanupEventListeners(); cleanupAdEventListeners(); destroyUI(); destroyMux(); destroyPlayer(); }; }, [managedMode ? hasPlaylist : src, isScriptsLoaded, bfResetKey, managedMode]); }; // src/hooks/useEventHandlers.ts var import_react3 = require("react"); var useEventHandlers = (videoRef, playerRef, handlers) => { const timeUpdateHandlerRef = (0, import_react3.useRef)(null); const setupEventListeners = (0, import_react3.useCallback)(() => { const video = videoRef.current; if (!video) return; const { onPlay, onPause, onEnded, onLoadStart, onCanPlay, onTimeUpdate } = handlers; if (onPlay) video.addEventListener("play", onPlay); if (onPause) video.addEventListener("pause", onPause); if (onEnded) video.addEventListener("ended", onEnded); if (onLoadStart) video.addEventListener("loadstart", onLoadStart); if (onCanPlay) video.addEventListener("canplay", onCanPlay); if (onTimeUpdate) { const handleTimeUpdate = () => { const player = playerRef.current; const currentTime = video.currentTime; const duration = video.duration; const isLive = player && typeof player.isLive === "function" ? player.isLive() : false; const timeData = { currentTime, duration, isLive }; if (isLive && player && typeof player.seekRange === "function") { const seekRange = player.seekRange(); if (seekRange && seekRange.end !== void 0) { timeData.liveEdge = seekRange.end; timeData.timeBehindLive = seekRange.end - currentTime; } } else if (duration && isFinite(duration) && duration > 0) { timeData.progress = currentTime / duration * 100; } onTimeUpdate(timeData); }; timeUpdateHandlerRef.current = handleTimeUpdate; video.addEventListener("timeupdate", handleTimeUpdate); } }, [videoRef, playerRef, handlers]); const cleanupEventListeners = (0, import_react3.useCallback)(() => { const video = videoRef.current; if (!video) return; const { onPlay, onPause, onEnded, onLoadStart, onCanPlay } = handlers; if (onPlay) video.removeEventListener("play", onPlay); if (onPause) video.removeEventListener("pause", onPause); if (onEnded) video.removeEventListener("ended", onEnded); if (onLoadStart) video.removeEventListener("loadstart", onLoadStart); if (onCanPlay) video.removeEventListener("canplay", onCanPlay); if (timeUpdateHandlerRef.current) { video.removeEventListener("timeupdate", timeUpdateHandlerRef.current); timeUpdateHandlerRef.current = null; } }, [videoRef, handlers]); return { setupEventListeners, cleanupEventListeners }; }; // src/hooks/useAdEvents.ts var import_react4 = require("react"); var useAdEvents = (playerRef, handlers) => { const listenersRef = (0, import_react4.useRef)([]); const setupAdEventListeners = (0, import_react4.useCallback)(() => { const player = playerRef.current; if (!player) return; const adManager = player.getAdManager(); if (!adManager) return; const onAdsManagerLoaded = (event) => { const adsManager = event.getAdsManager(); const onAdStarted = () => { console.log("Ad started"); handlers.onAdStart?.(); }; const onAdComplete = () => { console.log("Ad completed"); handlers.onAdComplete?.(); }; const onAdError = (adErrorEvent) => { console.error("Ad error:", adErrorEvent.getError()); handlers.onAdError?.(adErrorEvent.getError()); }; const onAdSkipped = () => { console.log("Ad skipped"); handlers.onAdSkipped?.(); }; const onAdPaused = () => { console.log("Ad paused"); handlers.onAdPaused?.(); }; const onAdResumed = () => { console.log("Ad resumed"); handlers.onAdResumed?.(); }; const onAdProgress = (adProgressData) => { handlers.onAdProgress?.(adProgressData); }; const onAllAdsCompleted = () => { console.log("All ads completed"); handlers.onAllAdsCompleted?.(); }; const adEventListeners = []; if (window.google?.ima) { const AdEvent = window.google.ima.AdEvent.Type; const AdErrorEvent = window.google.ima.AdErrorEvent.Type; if (handlers.onAdStart) { adsManager.addEventListener(AdEvent.STARTED, onAdStarted); adEventListeners.push({ event: AdEvent.STARTED, list