@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
JavaScript
"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