stormcloud-video-player 0.1.13 → 0.2.1
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/README.md +418 -48
- package/dist/stormcloud-vp.min.js +9 -9
- package/lib/index.cjs +937 -5
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +3591 -2
- package/lib/index.d.ts +3591 -2
- package/lib/index.js +919 -4
- package/lib/index.js.map +1 -1
- package/lib/player/StormcloudVideoPlayer.cjs +1669 -0
- package/lib/player/StormcloudVideoPlayer.cjs.map +1 -0
- package/lib/player/StormcloudVideoPlayer.d.cts +74 -0
- package/lib/players/FilePlayer.cjs +233 -0
- package/lib/players/FilePlayer.cjs.map +1 -0
- package/lib/players/FilePlayer.d.cts +48 -0
- package/lib/players/HlsPlayer.cjs +1847 -0
- package/lib/players/HlsPlayer.cjs.map +1 -0
- package/lib/players/HlsPlayer.d.cts +37 -0
- package/lib/players/index.cjs +2055 -0
- package/lib/players/index.cjs.map +1 -0
- package/lib/players/index.d.cts +10 -0
- package/lib/sdk/ima.cjs +420 -0
- package/lib/sdk/ima.cjs.map +1 -0
- package/lib/sdk/ima.d.cts +10 -0
- package/lib/types-GpA_hKek.d.cts +67 -0
- package/lib/ui/StormcloudVideoPlayer.cjs +2910 -0
- package/lib/ui/StormcloudVideoPlayer.cjs.map +1 -0
- package/lib/ui/StormcloudVideoPlayer.d.cts +13 -0
- package/lib/utils/tracking.cjs +250 -0
- package/lib/utils/tracking.cjs.map +1 -0
- package/lib/utils/tracking.d.cts +8 -0
- package/package.json +12 -5
- package/rollup.config.js +62 -0
|
@@ -0,0 +1,2055 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/players/index.ts
|
|
31
|
+
var players_exports = {};
|
|
32
|
+
__export(players_exports, {
|
|
33
|
+
default: () => players_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(players_exports);
|
|
36
|
+
|
|
37
|
+
// src/utils.ts
|
|
38
|
+
var import_react = require("react");
|
|
39
|
+
var lazy = import_react.lazy;
|
|
40
|
+
var IS_BROWSER = typeof window !== "undefined" && window.document;
|
|
41
|
+
var IS_GLOBAL = typeof globalThis !== "undefined" && globalThis.window && globalThis.window.document;
|
|
42
|
+
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
43
|
+
var IS_SAFARI = IS_BROWSER && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
44
|
+
|
|
45
|
+
// src/patterns.ts
|
|
46
|
+
var HLS_EXTENSIONS = /\.(m3u8)($|\?)/i;
|
|
47
|
+
var HLS_PATHS = /\/hls\//i;
|
|
48
|
+
var DASH_EXTENSIONS = /\.(mpd)($|\?)/i;
|
|
49
|
+
var VIDEO_EXTENSIONS = /\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv)($|\?)/i;
|
|
50
|
+
var AUDIO_EXTENSIONS = /\.(mp3|wav|ogg|aac|wma|flac|m4a)($|\?)/i;
|
|
51
|
+
var canPlay = {
|
|
52
|
+
hls: (url) => {
|
|
53
|
+
if (!url || typeof url !== "string") return false;
|
|
54
|
+
return HLS_EXTENSIONS.test(url) || HLS_PATHS.test(url);
|
|
55
|
+
},
|
|
56
|
+
dash: (url) => {
|
|
57
|
+
if (!url || typeof url !== "string") return false;
|
|
58
|
+
return DASH_EXTENSIONS.test(url);
|
|
59
|
+
},
|
|
60
|
+
video: (url) => {
|
|
61
|
+
if (!url || typeof url !== "string") return false;
|
|
62
|
+
return VIDEO_EXTENSIONS.test(url);
|
|
63
|
+
},
|
|
64
|
+
audio: (url) => {
|
|
65
|
+
if (!url || typeof url !== "string") return false;
|
|
66
|
+
return AUDIO_EXTENSIONS.test(url);
|
|
67
|
+
},
|
|
68
|
+
file: (url) => {
|
|
69
|
+
if (!url || typeof url !== "string") return false;
|
|
70
|
+
return VIDEO_EXTENSIONS.test(url) || AUDIO_EXTENSIONS.test(url);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/players/HlsPlayer.tsx
|
|
75
|
+
var import_react2 = require("react");
|
|
76
|
+
|
|
77
|
+
// src/player/StormcloudVideoPlayer.ts
|
|
78
|
+
var import_hls = __toESM(require("hls.js"), 1);
|
|
79
|
+
|
|
80
|
+
// src/sdk/ima.ts
|
|
81
|
+
function createImaController(video) {
|
|
82
|
+
let adPlaying = false;
|
|
83
|
+
let originalMutedState = false;
|
|
84
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
85
|
+
function emit(event, payload) {
|
|
86
|
+
const set = listeners.get(event);
|
|
87
|
+
if (!set) return;
|
|
88
|
+
for (const fn of Array.from(set)) {
|
|
89
|
+
try {
|
|
90
|
+
fn(payload);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function ensureImaLoaded() {
|
|
96
|
+
try {
|
|
97
|
+
const frameEl = window.frameElement;
|
|
98
|
+
const sandboxAttr = frameEl?.getAttribute?.("sandbox") || "";
|
|
99
|
+
if (sandboxAttr) {
|
|
100
|
+
const tokens = new Set(
|
|
101
|
+
sandboxAttr.split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 0)
|
|
102
|
+
);
|
|
103
|
+
const allowsScripts = tokens.has("allow-scripts");
|
|
104
|
+
if (!allowsScripts) {
|
|
105
|
+
console.error(
|
|
106
|
+
"StormcloudVideoPlayer: The host page is inside a sandboxed iframe without 'allow-scripts'. Google IMA cannot run ads within sandboxed frames. Remove the sandbox attribute or include 'allow-scripts allow-same-origin'."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
if (typeof window !== "undefined" && window.google?.ima)
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
const existing = document.querySelector(
|
|
115
|
+
'script[data-ima="true"]'
|
|
116
|
+
);
|
|
117
|
+
if (existing) {
|
|
118
|
+
return new Promise(
|
|
119
|
+
(resolve) => existing.addEventListener("load", () => resolve())
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const script = document.createElement("script");
|
|
124
|
+
script.src = "https://imasdk.googleapis.com/js/sdkloader/ima3.js";
|
|
125
|
+
script.async = true;
|
|
126
|
+
script.defer = true;
|
|
127
|
+
script.setAttribute("data-ima", "true");
|
|
128
|
+
script.onload = () => resolve();
|
|
129
|
+
script.onerror = () => reject(new Error("IMA SDK load failed"));
|
|
130
|
+
document.head.appendChild(script);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
let adsManager;
|
|
134
|
+
let adsLoader;
|
|
135
|
+
let adDisplayContainer;
|
|
136
|
+
let adContainerEl;
|
|
137
|
+
let lastAdTagUrl;
|
|
138
|
+
let retryAttempts = 0;
|
|
139
|
+
const maxRetries = 2;
|
|
140
|
+
const backoffBaseMs = 500;
|
|
141
|
+
let adsLoadedPromise;
|
|
142
|
+
let adsLoadedResolve;
|
|
143
|
+
let adsLoadedReject;
|
|
144
|
+
function makeAdsRequest(google, vastTagUrl) {
|
|
145
|
+
const adsRequest = new google.ima.AdsRequest();
|
|
146
|
+
adsRequest.adTagUrl = vastTagUrl;
|
|
147
|
+
adsLoader.requestAds(adsRequest);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
initialize() {
|
|
151
|
+
ensureImaLoaded().then(() => {
|
|
152
|
+
const google = window.google;
|
|
153
|
+
if (!adDisplayContainer) {
|
|
154
|
+
const container = document.createElement("div");
|
|
155
|
+
container.style.position = "absolute";
|
|
156
|
+
container.style.left = "0";
|
|
157
|
+
container.style.top = "0";
|
|
158
|
+
container.style.right = "0";
|
|
159
|
+
container.style.bottom = "0";
|
|
160
|
+
container.style.display = "flex";
|
|
161
|
+
container.style.alignItems = "center";
|
|
162
|
+
container.style.justifyContent = "center";
|
|
163
|
+
container.style.pointerEvents = "none";
|
|
164
|
+
container.style.zIndex = "2";
|
|
165
|
+
video.parentElement?.appendChild(container);
|
|
166
|
+
adContainerEl = container;
|
|
167
|
+
adDisplayContainer = new google.ima.AdDisplayContainer(
|
|
168
|
+
container,
|
|
169
|
+
video
|
|
170
|
+
);
|
|
171
|
+
try {
|
|
172
|
+
adDisplayContainer.initialize?.();
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}).catch(() => {
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
async requestAds(vastTagUrl) {
|
|
180
|
+
console.log("[IMA] Requesting ads:", vastTagUrl);
|
|
181
|
+
adsLoadedPromise = new Promise((resolve, reject) => {
|
|
182
|
+
adsLoadedResolve = resolve;
|
|
183
|
+
adsLoadedReject = reject;
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (adsLoadedReject) {
|
|
186
|
+
adsLoadedReject(new Error("Ad request timeout"));
|
|
187
|
+
adsLoadedReject = void 0;
|
|
188
|
+
adsLoadedResolve = void 0;
|
|
189
|
+
}
|
|
190
|
+
}, 1e4);
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
await ensureImaLoaded();
|
|
194
|
+
const google = window.google;
|
|
195
|
+
lastAdTagUrl = vastTagUrl;
|
|
196
|
+
retryAttempts = 0;
|
|
197
|
+
if (!adDisplayContainer) {
|
|
198
|
+
console.log("[IMA] Creating ad display container");
|
|
199
|
+
const container = document.createElement("div");
|
|
200
|
+
container.style.position = "absolute";
|
|
201
|
+
container.style.left = "0";
|
|
202
|
+
container.style.top = "0";
|
|
203
|
+
container.style.right = "0";
|
|
204
|
+
container.style.bottom = "0";
|
|
205
|
+
container.style.display = "flex";
|
|
206
|
+
container.style.alignItems = "center";
|
|
207
|
+
container.style.justifyContent = "center";
|
|
208
|
+
container.style.pointerEvents = "none";
|
|
209
|
+
container.style.zIndex = "2";
|
|
210
|
+
if (!video.parentElement) {
|
|
211
|
+
throw new Error("Video element has no parent for ad container");
|
|
212
|
+
}
|
|
213
|
+
video.parentElement.appendChild(container);
|
|
214
|
+
adContainerEl = container;
|
|
215
|
+
adDisplayContainer = new google.ima.AdDisplayContainer(
|
|
216
|
+
container,
|
|
217
|
+
video
|
|
218
|
+
);
|
|
219
|
+
try {
|
|
220
|
+
adDisplayContainer.initialize();
|
|
221
|
+
console.log("[IMA] Ad display container initialized");
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn(
|
|
224
|
+
"[IMA] Failed to initialize ad display container:",
|
|
225
|
+
error
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!adsLoader) {
|
|
230
|
+
console.log("[IMA] Creating ads loader");
|
|
231
|
+
const adsLoaderCls = new google.ima.AdsLoader(adDisplayContainer);
|
|
232
|
+
adsLoader = adsLoaderCls;
|
|
233
|
+
adsLoader.addEventListener(
|
|
234
|
+
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
|
|
235
|
+
(evt) => {
|
|
236
|
+
console.log("[IMA] Ads manager loaded");
|
|
237
|
+
try {
|
|
238
|
+
adsManager = evt.getAdsManager(video);
|
|
239
|
+
const AdEvent = google.ima.AdEvent.Type;
|
|
240
|
+
const AdErrorEvent = google.ima.AdErrorEvent.Type;
|
|
241
|
+
adsManager.addEventListener(
|
|
242
|
+
AdErrorEvent.AD_ERROR,
|
|
243
|
+
(errorEvent) => {
|
|
244
|
+
console.error("[IMA] Ad error:", errorEvent.getError());
|
|
245
|
+
try {
|
|
246
|
+
adsManager?.destroy?.();
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
adPlaying = false;
|
|
250
|
+
video.muted = originalMutedState;
|
|
251
|
+
if (adContainerEl)
|
|
252
|
+
adContainerEl.style.pointerEvents = "none";
|
|
253
|
+
if (adsLoadedReject) {
|
|
254
|
+
adsLoadedReject(new Error("Ad playback error"));
|
|
255
|
+
adsLoadedReject = void 0;
|
|
256
|
+
adsLoadedResolve = void 0;
|
|
257
|
+
}
|
|
258
|
+
if (lastAdTagUrl && retryAttempts < maxRetries) {
|
|
259
|
+
const delay = backoffBaseMs * Math.pow(2, retryAttempts++);
|
|
260
|
+
console.log(
|
|
261
|
+
`[IMA] Retrying ad request in ${delay}ms (attempt ${retryAttempts})`
|
|
262
|
+
);
|
|
263
|
+
window.setTimeout(() => {
|
|
264
|
+
try {
|
|
265
|
+
makeAdsRequest(google, lastAdTagUrl);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}, delay);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(
|
|
271
|
+
"[IMA] Max retries reached, emitting ad_error"
|
|
272
|
+
);
|
|
273
|
+
emit("ad_error");
|
|
274
|
+
video.play().catch(() => {
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
adsManager.addEventListener(
|
|
280
|
+
AdEvent.CONTENT_PAUSE_REQUESTED,
|
|
281
|
+
() => {
|
|
282
|
+
console.log("[IMA] Content pause requested");
|
|
283
|
+
originalMutedState = video.muted;
|
|
284
|
+
video.muted = true;
|
|
285
|
+
video.pause();
|
|
286
|
+
adPlaying = true;
|
|
287
|
+
if (adContainerEl)
|
|
288
|
+
adContainerEl.style.pointerEvents = "auto";
|
|
289
|
+
emit("content_pause");
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
adsManager.addEventListener(
|
|
293
|
+
AdEvent.CONTENT_RESUME_REQUESTED,
|
|
294
|
+
() => {
|
|
295
|
+
console.log("[IMA] Content resume requested");
|
|
296
|
+
adPlaying = false;
|
|
297
|
+
video.muted = originalMutedState;
|
|
298
|
+
if (adContainerEl)
|
|
299
|
+
adContainerEl.style.pointerEvents = "none";
|
|
300
|
+
video.play().catch(() => {
|
|
301
|
+
});
|
|
302
|
+
emit("content_resume");
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
adsManager.addEventListener(AdEvent.ALL_ADS_COMPLETED, () => {
|
|
306
|
+
console.log("[IMA] All ads completed");
|
|
307
|
+
adPlaying = false;
|
|
308
|
+
video.muted = originalMutedState;
|
|
309
|
+
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
310
|
+
video.play().catch(() => {
|
|
311
|
+
});
|
|
312
|
+
emit("all_ads_completed");
|
|
313
|
+
});
|
|
314
|
+
console.log("[IMA] Ads manager event listeners attached");
|
|
315
|
+
if (adsLoadedResolve) {
|
|
316
|
+
adsLoadedResolve();
|
|
317
|
+
adsLoadedResolve = void 0;
|
|
318
|
+
adsLoadedReject = void 0;
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
console.error("[IMA] Error setting up ads manager:", e);
|
|
322
|
+
adPlaying = false;
|
|
323
|
+
video.muted = originalMutedState;
|
|
324
|
+
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
325
|
+
video.play().catch(() => {
|
|
326
|
+
});
|
|
327
|
+
if (adsLoadedReject) {
|
|
328
|
+
adsLoadedReject(new Error("Failed to setup ads manager"));
|
|
329
|
+
adsLoadedReject = void 0;
|
|
330
|
+
adsLoadedResolve = void 0;
|
|
331
|
+
}
|
|
332
|
+
emit("ad_error");
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
false
|
|
336
|
+
);
|
|
337
|
+
adsLoader.addEventListener(
|
|
338
|
+
google.ima.AdErrorEvent.Type.AD_ERROR,
|
|
339
|
+
(adErrorEvent) => {
|
|
340
|
+
console.error("[IMA] Ads loader error:", adErrorEvent.getError());
|
|
341
|
+
if (adsLoadedReject) {
|
|
342
|
+
adsLoadedReject(new Error("Ads loader error"));
|
|
343
|
+
adsLoadedReject = void 0;
|
|
344
|
+
adsLoadedResolve = void 0;
|
|
345
|
+
}
|
|
346
|
+
emit("ad_error");
|
|
347
|
+
},
|
|
348
|
+
false
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
console.log("[IMA] Making ads request");
|
|
352
|
+
makeAdsRequest(google, vastTagUrl);
|
|
353
|
+
return adsLoadedPromise;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error("[IMA] Failed to request ads:", error);
|
|
356
|
+
if (adsLoadedReject) {
|
|
357
|
+
adsLoadedReject(error);
|
|
358
|
+
adsLoadedReject = void 0;
|
|
359
|
+
adsLoadedResolve = void 0;
|
|
360
|
+
}
|
|
361
|
+
return Promise.reject(error);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
async play() {
|
|
365
|
+
if (!window.google?.ima || !adDisplayContainer) {
|
|
366
|
+
console.warn(
|
|
367
|
+
"[IMA] Cannot play ad: IMA SDK or ad container not available"
|
|
368
|
+
);
|
|
369
|
+
return Promise.reject(new Error("IMA SDK not available"));
|
|
370
|
+
}
|
|
371
|
+
if (!adsManager) {
|
|
372
|
+
console.warn("[IMA] Cannot play ad: No ads manager available");
|
|
373
|
+
return Promise.reject(new Error("No ads manager"));
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const width = video.clientWidth || 640;
|
|
377
|
+
const height = video.clientHeight || 360;
|
|
378
|
+
console.log(`[IMA] Initializing ads manager (${width}x${height})`);
|
|
379
|
+
adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
|
|
380
|
+
console.log("[IMA] Pausing video for ad playback");
|
|
381
|
+
video.pause();
|
|
382
|
+
adPlaying = true;
|
|
383
|
+
console.log("[IMA] Starting ad playback");
|
|
384
|
+
adsManager.start();
|
|
385
|
+
return Promise.resolve();
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error("[IMA] Error starting ad playback:", error);
|
|
388
|
+
adPlaying = false;
|
|
389
|
+
video.play().catch(() => {
|
|
390
|
+
});
|
|
391
|
+
return Promise.reject(error);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
async stop() {
|
|
395
|
+
adPlaying = false;
|
|
396
|
+
video.muted = originalMutedState;
|
|
397
|
+
try {
|
|
398
|
+
adsManager?.stop?.();
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
video.play().catch(() => {
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
destroy() {
|
|
405
|
+
try {
|
|
406
|
+
adsManager?.destroy?.();
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
adPlaying = false;
|
|
410
|
+
video.muted = originalMutedState;
|
|
411
|
+
try {
|
|
412
|
+
adsLoader?.destroy?.();
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
if (adContainerEl?.parentElement) {
|
|
416
|
+
adContainerEl.parentElement.removeChild(adContainerEl);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
isAdPlaying() {
|
|
420
|
+
return adPlaying;
|
|
421
|
+
},
|
|
422
|
+
resize(width, height) {
|
|
423
|
+
if (!adsManager || !window.google?.ima) {
|
|
424
|
+
console.warn(
|
|
425
|
+
"[IMA] Cannot resize: No ads manager or IMA SDK available"
|
|
426
|
+
);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
console.log(`[IMA] Resizing ads manager to ${width}x${height}`);
|
|
431
|
+
adsManager.resize(width, height, window.google.ima.ViewMode.NORMAL);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.warn("[IMA] Error resizing ads manager:", error);
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
on(event, listener) {
|
|
437
|
+
if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
|
|
438
|
+
listeners.get(event).add(listener);
|
|
439
|
+
},
|
|
440
|
+
off(event, listener) {
|
|
441
|
+
listeners.get(event)?.delete(listener);
|
|
442
|
+
},
|
|
443
|
+
updateOriginalMutedState(muted) {
|
|
444
|
+
originalMutedState = muted;
|
|
445
|
+
},
|
|
446
|
+
getOriginalMutedState() {
|
|
447
|
+
return originalMutedState;
|
|
448
|
+
},
|
|
449
|
+
setAdVolume(volume) {
|
|
450
|
+
if (adsManager && adPlaying) {
|
|
451
|
+
try {
|
|
452
|
+
adsManager.setVolume(Math.max(0, Math.min(1, volume)));
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.warn("[IMA] Failed to set ad volume:", error);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
getAdVolume() {
|
|
459
|
+
if (adsManager && adPlaying) {
|
|
460
|
+
try {
|
|
461
|
+
return adsManager.getVolume();
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.warn("[IMA] Failed to get ad volume:", error);
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/utils/tracking.ts
|
|
473
|
+
function getClientInfo() {
|
|
474
|
+
const ua = navigator.userAgent;
|
|
475
|
+
const platform = navigator.platform;
|
|
476
|
+
const vendor = navigator.vendor || "";
|
|
477
|
+
const maxTouchPoints = navigator.maxTouchPoints || 0;
|
|
478
|
+
const memory = navigator.deviceMemory || null;
|
|
479
|
+
const hardwareConcurrency = navigator.hardwareConcurrency || 1;
|
|
480
|
+
const screenInfo = {
|
|
481
|
+
width: screen?.width,
|
|
482
|
+
height: screen?.height,
|
|
483
|
+
availWidth: screen?.availWidth,
|
|
484
|
+
availHeight: screen?.availHeight,
|
|
485
|
+
orientation: screen?.orientation?.type || "",
|
|
486
|
+
pixelDepth: screen?.pixelDepth
|
|
487
|
+
};
|
|
488
|
+
let deviceType = "desktop";
|
|
489
|
+
let brand = "Unknown";
|
|
490
|
+
let os = "Unknown";
|
|
491
|
+
let model = "";
|
|
492
|
+
let isSmartTV = false;
|
|
493
|
+
let isAndroid = false;
|
|
494
|
+
let isWebView = false;
|
|
495
|
+
let isWebApp = false;
|
|
496
|
+
if (ua.includes("Web0S")) {
|
|
497
|
+
brand = "LG";
|
|
498
|
+
os = "webOS";
|
|
499
|
+
isSmartTV = true;
|
|
500
|
+
deviceType = "tv";
|
|
501
|
+
const webosMatch = ua.match(/Web0S\/([^\s]+)/);
|
|
502
|
+
model = webosMatch ? `webOS ${webosMatch[1]}` : "webOS TV";
|
|
503
|
+
} else if (ua.includes("Tizen")) {
|
|
504
|
+
brand = "Samsung";
|
|
505
|
+
os = "Tizen";
|
|
506
|
+
isSmartTV = true;
|
|
507
|
+
deviceType = "tv";
|
|
508
|
+
const tizenMatch = ua.match(/Tizen\/([^\s]+)/);
|
|
509
|
+
const tvMatch = ua.match(/(?:Smart-TV|SMART-TV|TV)/i) ? "Smart TV" : "";
|
|
510
|
+
model = tizenMatch ? `Tizen ${tizenMatch[1]} ${tvMatch}`.trim() : "Tizen TV";
|
|
511
|
+
} else if (ua.includes("Philips")) {
|
|
512
|
+
brand = "Philips";
|
|
513
|
+
os = "Saphi";
|
|
514
|
+
isSmartTV = true;
|
|
515
|
+
deviceType = "tv";
|
|
516
|
+
} else if (ua.includes("Sharp") || ua.includes("AQUOS")) {
|
|
517
|
+
brand = "Sharp";
|
|
518
|
+
os = "Android TV";
|
|
519
|
+
isSmartTV = true;
|
|
520
|
+
deviceType = "tv";
|
|
521
|
+
} else if (ua.includes("Android") && (ua.includes("Sony") || vendor.includes("Sony"))) {
|
|
522
|
+
brand = "Sony";
|
|
523
|
+
os = "Android TV";
|
|
524
|
+
isSmartTV = true;
|
|
525
|
+
deviceType = "tv";
|
|
526
|
+
} else if (ua.includes("Android") && (ua.includes("NetCast") || ua.includes("LG"))) {
|
|
527
|
+
brand = "LG";
|
|
528
|
+
os = "Android TV";
|
|
529
|
+
isSmartTV = true;
|
|
530
|
+
deviceType = "tv";
|
|
531
|
+
} else if (ua.includes(" Roku") || ua.includes("Roku/")) {
|
|
532
|
+
brand = "Roku";
|
|
533
|
+
os = "Roku OS";
|
|
534
|
+
isSmartTV = true;
|
|
535
|
+
deviceType = "tv";
|
|
536
|
+
} else if (ua.includes("AppleTV")) {
|
|
537
|
+
brand = "Apple";
|
|
538
|
+
os = "tvOS";
|
|
539
|
+
isSmartTV = true;
|
|
540
|
+
deviceType = "tv";
|
|
541
|
+
}
|
|
542
|
+
if (ua.includes("Android")) {
|
|
543
|
+
isAndroid = true;
|
|
544
|
+
os = "Android";
|
|
545
|
+
deviceType = /Mobile/.test(ua) ? "mobile" : "tablet";
|
|
546
|
+
if (ua.includes("Android") && (maxTouchPoints === 0 || ua.includes("Google TV") || ua.includes("XiaoMi"))) {
|
|
547
|
+
deviceType = "tv";
|
|
548
|
+
isSmartTV = true;
|
|
549
|
+
brand = brand === "Unknown" ? "Android TV" : brand;
|
|
550
|
+
}
|
|
551
|
+
const androidModelMatch = ua.match(/\(([^)]*Android[^)]*)\)/);
|
|
552
|
+
if (androidModelMatch && androidModelMatch[1]) {
|
|
553
|
+
model = androidModelMatch[1];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (/iPad|iPhone|iPod/.test(ua)) {
|
|
557
|
+
os = "iOS";
|
|
558
|
+
deviceType = "mobile";
|
|
559
|
+
brand = "Apple";
|
|
560
|
+
if (navigator.maxTouchPoints > 1 && /iPad/.test(ua)) {
|
|
561
|
+
deviceType = "tablet";
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!isAndroid && !isSmartTV && !/Mobile/.test(ua)) {
|
|
565
|
+
if (ua.includes("Windows")) {
|
|
566
|
+
os = "Windows";
|
|
567
|
+
deviceType = "desktop";
|
|
568
|
+
} else if (ua.includes("Mac") && !/iPhone/.test(ua)) {
|
|
569
|
+
os = "macOS";
|
|
570
|
+
deviceType = "desktop";
|
|
571
|
+
if (maxTouchPoints > 1) deviceType = "tablet";
|
|
572
|
+
} else if (ua.includes("Linux")) {
|
|
573
|
+
os = "Linux";
|
|
574
|
+
deviceType = "desktop";
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (brand === "Unknown") {
|
|
578
|
+
if (vendor.includes("Google") || ua.includes("Chrome")) brand = "Google";
|
|
579
|
+
if (vendor.includes("Apple")) brand = "Apple";
|
|
580
|
+
if (vendor.includes("Samsung") || ua.includes("SM-")) brand = "Samsung";
|
|
581
|
+
}
|
|
582
|
+
isWebView = /wv|WebView|Linux; U;/.test(ua);
|
|
583
|
+
if (window?.outerHeight === 0 && window?.outerWidth === 0) {
|
|
584
|
+
isWebView = true;
|
|
585
|
+
}
|
|
586
|
+
isWebApp = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true || window.screen?.orientation?.angle !== void 0;
|
|
587
|
+
return {
|
|
588
|
+
brand,
|
|
589
|
+
os,
|
|
590
|
+
model: model || ua.substring(0, 50) + "...",
|
|
591
|
+
deviceType,
|
|
592
|
+
isSmartTV,
|
|
593
|
+
isAndroid,
|
|
594
|
+
isWebView,
|
|
595
|
+
isWebApp,
|
|
596
|
+
domain: window.location.hostname,
|
|
597
|
+
origin: window.location.origin,
|
|
598
|
+
path: window.location.pathname,
|
|
599
|
+
userAgent: ua,
|
|
600
|
+
vendor,
|
|
601
|
+
platform,
|
|
602
|
+
screen: screenInfo,
|
|
603
|
+
hardwareConcurrency,
|
|
604
|
+
deviceMemory: memory,
|
|
605
|
+
maxTouchPoints,
|
|
606
|
+
language: navigator.language,
|
|
607
|
+
languages: navigator.languages?.join(",") || "",
|
|
608
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
609
|
+
doNotTrack: navigator.doNotTrack || "",
|
|
610
|
+
referrer: document.referrer,
|
|
611
|
+
visibilityState: document.visibilityState
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async function getBrowserID(clientInfo) {
|
|
615
|
+
const fingerprintString = JSON.stringify(clientInfo);
|
|
616
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
617
|
+
"SHA-256",
|
|
618
|
+
new TextEncoder().encode(fingerprintString)
|
|
619
|
+
);
|
|
620
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
621
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
622
|
+
return hashHex;
|
|
623
|
+
}
|
|
624
|
+
async function sendInitialTracking(licenseKey) {
|
|
625
|
+
try {
|
|
626
|
+
const clientInfo = getClientInfo();
|
|
627
|
+
const browserId = await getBrowserID(clientInfo);
|
|
628
|
+
const trackingData = {
|
|
629
|
+
browserId,
|
|
630
|
+
...clientInfo
|
|
631
|
+
};
|
|
632
|
+
const headers = {
|
|
633
|
+
"Content-Type": "application/json"
|
|
634
|
+
};
|
|
635
|
+
if (licenseKey) {
|
|
636
|
+
headers["Authorization"] = `Bearer ${licenseKey}`;
|
|
637
|
+
}
|
|
638
|
+
const response = await fetch(
|
|
639
|
+
"https://adstorm.co/api-adstorm-dev/adstorm/player-tracking/track",
|
|
640
|
+
{
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers,
|
|
643
|
+
body: JSON.stringify(trackingData)
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
648
|
+
}
|
|
649
|
+
await response.json();
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error(
|
|
652
|
+
"[StormcloudVideoPlayer] Error sending initial tracking data:",
|
|
653
|
+
error
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function sendHeartbeat(licenseKey) {
|
|
658
|
+
try {
|
|
659
|
+
const clientInfo = getClientInfo();
|
|
660
|
+
const browserId = await getBrowserID(clientInfo);
|
|
661
|
+
const heartbeatData = {
|
|
662
|
+
browserId,
|
|
663
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
664
|
+
};
|
|
665
|
+
const headers = {
|
|
666
|
+
"Content-Type": "application/json"
|
|
667
|
+
};
|
|
668
|
+
if (licenseKey) {
|
|
669
|
+
headers["Authorization"] = `Bearer ${licenseKey}`;
|
|
670
|
+
}
|
|
671
|
+
const response = await fetch(
|
|
672
|
+
"https://adstorm.co/api-adstorm-dev/adstorm/player-tracking/heartbeat",
|
|
673
|
+
{
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers,
|
|
676
|
+
body: JSON.stringify(heartbeatData)
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
681
|
+
}
|
|
682
|
+
await response.json();
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.error("[StormcloudVideoPlayer] Error sending heartbeat:", error);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/player/StormcloudVideoPlayer.ts
|
|
689
|
+
var StormcloudVideoPlayer = class {
|
|
690
|
+
constructor(config) {
|
|
691
|
+
this.attached = false;
|
|
692
|
+
this.inAdBreak = false;
|
|
693
|
+
this.ptsDriftEmaMs = 0;
|
|
694
|
+
this.adPodQueue = [];
|
|
695
|
+
this.lastHeartbeatTime = 0;
|
|
696
|
+
this.currentAdIndex = 0;
|
|
697
|
+
this.totalAdsInBreak = 0;
|
|
698
|
+
this.showAds = false;
|
|
699
|
+
this.config = config;
|
|
700
|
+
this.video = config.videoElement;
|
|
701
|
+
this.ima = createImaController(this.video);
|
|
702
|
+
}
|
|
703
|
+
async load() {
|
|
704
|
+
if (!this.attached) {
|
|
705
|
+
this.attach();
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
await this.fetchAdConfiguration();
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (this.config.debugAdTiming) {
|
|
711
|
+
console.warn(
|
|
712
|
+
"[StormcloudVideoPlayer] Failed to fetch ad configuration:",
|
|
713
|
+
error
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
this.initializeTracking();
|
|
718
|
+
if (this.shouldUseNativeHls()) {
|
|
719
|
+
this.video.src = this.config.src;
|
|
720
|
+
if (this.config.autoplay) {
|
|
721
|
+
await this.video.play().catch(() => {
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.hls = new import_hls.default({
|
|
727
|
+
enableWorker: true,
|
|
728
|
+
backBufferLength: 30,
|
|
729
|
+
liveDurationInfinity: true,
|
|
730
|
+
lowLatencyMode: !!this.config.lowLatencyMode,
|
|
731
|
+
maxLiveSyncPlaybackRate: this.config.lowLatencyMode ? 1.5 : 1,
|
|
732
|
+
...this.config.lowLatencyMode ? { liveSyncDuration: 2 } : {}
|
|
733
|
+
});
|
|
734
|
+
this.hls.on(import_hls.default.Events.MEDIA_ATTACHED, () => {
|
|
735
|
+
this.hls?.loadSource(this.config.src);
|
|
736
|
+
});
|
|
737
|
+
this.hls.on(import_hls.default.Events.MANIFEST_PARSED, async () => {
|
|
738
|
+
if (this.config.autoplay) {
|
|
739
|
+
await this.video.play().catch(() => {
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
this.hls.on(import_hls.default.Events.FRAG_PARSING_METADATA, (_evt, data) => {
|
|
744
|
+
const id3Tags = (data?.samples || []).map((s) => ({
|
|
745
|
+
key: "ID3",
|
|
746
|
+
value: s?.data,
|
|
747
|
+
ptsSeconds: s?.pts
|
|
748
|
+
}));
|
|
749
|
+
id3Tags.forEach((tag) => this.onId3Tag(tag));
|
|
750
|
+
});
|
|
751
|
+
this.hls.on(import_hls.default.Events.FRAG_CHANGED, (_evt, data) => {
|
|
752
|
+
const frag = data?.frag;
|
|
753
|
+
const tagList = frag?.tagList;
|
|
754
|
+
if (!Array.isArray(tagList)) return;
|
|
755
|
+
for (const entry of tagList) {
|
|
756
|
+
let tag = "";
|
|
757
|
+
let value = "";
|
|
758
|
+
if (Array.isArray(entry)) {
|
|
759
|
+
tag = String(entry[0] ?? "");
|
|
760
|
+
value = String(entry[1] ?? "");
|
|
761
|
+
} else if (typeof entry === "string") {
|
|
762
|
+
const idx = entry.indexOf(":");
|
|
763
|
+
if (idx >= 0) {
|
|
764
|
+
tag = entry.substring(0, idx);
|
|
765
|
+
value = entry.substring(idx + 1);
|
|
766
|
+
} else {
|
|
767
|
+
tag = entry;
|
|
768
|
+
value = "";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!tag) continue;
|
|
772
|
+
if (tag.includes("EXT-X-CUE-OUT")) {
|
|
773
|
+
const durationSeconds = this.parseCueOutDuration(value);
|
|
774
|
+
const marker = {
|
|
775
|
+
type: "start",
|
|
776
|
+
...durationSeconds !== void 0 ? { durationSeconds } : {},
|
|
777
|
+
raw: { tag, value }
|
|
778
|
+
};
|
|
779
|
+
this.onScte35Marker(marker);
|
|
780
|
+
} else if (tag.includes("EXT-X-CUE-OUT-CONT")) {
|
|
781
|
+
const prog = this.parseCueOutCont(value);
|
|
782
|
+
const marker = {
|
|
783
|
+
type: "progress",
|
|
784
|
+
...prog?.duration !== void 0 ? { durationSeconds: prog.duration } : {},
|
|
785
|
+
...prog?.elapsed !== void 0 ? { ptsSeconds: prog.elapsed } : {},
|
|
786
|
+
raw: { tag, value }
|
|
787
|
+
};
|
|
788
|
+
this.onScte35Marker(marker);
|
|
789
|
+
} else if (tag.includes("EXT-X-CUE-IN")) {
|
|
790
|
+
this.onScte35Marker({ type: "end", raw: { tag, value } });
|
|
791
|
+
} else if (tag.includes("EXT-X-DATERANGE")) {
|
|
792
|
+
const attrs = this.parseAttributeList(value);
|
|
793
|
+
const hasScteOut = "SCTE35-OUT" in attrs || attrs["SCTE35-OUT"] !== void 0;
|
|
794
|
+
const hasScteIn = "SCTE35-IN" in attrs || attrs["SCTE35-IN"] !== void 0;
|
|
795
|
+
const klass = String(attrs["CLASS"] ?? "");
|
|
796
|
+
const duration = this.toNumber(attrs["DURATION"]);
|
|
797
|
+
if (hasScteOut || /com\.apple\.hls\.cue/i.test(klass)) {
|
|
798
|
+
const marker = {
|
|
799
|
+
type: "start",
|
|
800
|
+
...duration !== void 0 ? { durationSeconds: duration } : {},
|
|
801
|
+
raw: { tag, value, attrs }
|
|
802
|
+
};
|
|
803
|
+
this.onScte35Marker(marker);
|
|
804
|
+
}
|
|
805
|
+
if (hasScteIn) {
|
|
806
|
+
this.onScte35Marker({ type: "end", raw: { tag, value, attrs } });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
this.hls.on(import_hls.default.Events.ERROR, (_evt, data) => {
|
|
812
|
+
if (data?.fatal) {
|
|
813
|
+
switch (data.type) {
|
|
814
|
+
case import_hls.default.ErrorTypes.NETWORK_ERROR:
|
|
815
|
+
this.hls?.startLoad();
|
|
816
|
+
break;
|
|
817
|
+
case import_hls.default.ErrorTypes.MEDIA_ERROR:
|
|
818
|
+
this.hls?.recoverMediaError();
|
|
819
|
+
break;
|
|
820
|
+
default:
|
|
821
|
+
this.destroy();
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
this.hls.attachMedia(this.video);
|
|
827
|
+
}
|
|
828
|
+
attach() {
|
|
829
|
+
if (this.attached) return;
|
|
830
|
+
this.attached = true;
|
|
831
|
+
this.video.autoplay = !!this.config.autoplay;
|
|
832
|
+
this.video.muted = !!this.config.muted;
|
|
833
|
+
this.ima.initialize();
|
|
834
|
+
this.ima.on("all_ads_completed", () => {
|
|
835
|
+
if (!this.inAdBreak) return;
|
|
836
|
+
const remaining = this.getRemainingAdMs();
|
|
837
|
+
if (remaining > 500 && this.adPodQueue.length > 0) {
|
|
838
|
+
const next = this.adPodQueue.shift();
|
|
839
|
+
this.currentAdIndex++;
|
|
840
|
+
this.playSingleAd(next).catch(() => {
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
this.currentAdIndex = 0;
|
|
844
|
+
this.totalAdsInBreak = 0;
|
|
845
|
+
this.showAds = false;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
this.ima.on("ad_error", () => {
|
|
849
|
+
if (this.config.debugAdTiming) {
|
|
850
|
+
console.log("[StormcloudVideoPlayer] IMA ad_error event received");
|
|
851
|
+
}
|
|
852
|
+
if (!this.inAdBreak) return;
|
|
853
|
+
const remaining = this.getRemainingAdMs();
|
|
854
|
+
if (remaining > 500 && this.adPodQueue.length > 0) {
|
|
855
|
+
const next = this.adPodQueue.shift();
|
|
856
|
+
this.currentAdIndex++;
|
|
857
|
+
this.playSingleAd(next).catch(() => {
|
|
858
|
+
});
|
|
859
|
+
} else {
|
|
860
|
+
this.handleAdFailure();
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
this.ima.on("content_pause", () => {
|
|
864
|
+
if (this.config.debugAdTiming) {
|
|
865
|
+
console.log("[StormcloudVideoPlayer] IMA content_pause event received");
|
|
866
|
+
}
|
|
867
|
+
this.clearAdFailsafeTimer();
|
|
868
|
+
});
|
|
869
|
+
this.ima.on("content_resume", () => {
|
|
870
|
+
if (this.config.debugAdTiming) {
|
|
871
|
+
console.log(
|
|
872
|
+
"[StormcloudVideoPlayer] IMA content_resume event received"
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
this.clearAdFailsafeTimer();
|
|
876
|
+
});
|
|
877
|
+
this.video.addEventListener("timeupdate", () => {
|
|
878
|
+
this.onTimeUpdate(this.video.currentTime);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
shouldUseNativeHls() {
|
|
882
|
+
const streamType = this.getStreamType();
|
|
883
|
+
if (streamType === "other") {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
const canNative = this.video.canPlayType("application/vnd.apple.mpegURL");
|
|
887
|
+
return !!(this.config.allowNativeHls && canNative);
|
|
888
|
+
}
|
|
889
|
+
onId3Tag(tag) {
|
|
890
|
+
if (typeof tag.ptsSeconds === "number") {
|
|
891
|
+
this.updatePtsDrift(tag.ptsSeconds);
|
|
892
|
+
}
|
|
893
|
+
const marker = this.parseScte35FromId3(tag);
|
|
894
|
+
if (marker) {
|
|
895
|
+
this.onScte35Marker(marker);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
parseScte35FromId3(tag) {
|
|
899
|
+
const text = this.decodeId3ValueToText(tag.value);
|
|
900
|
+
if (!text) return void 0;
|
|
901
|
+
const cueOutMatch = text.match(/EXT-X-CUE-OUT(?::([^\r\n]*))?/i) || text.match(/CUE-OUT(?::([^\r\n]*))?/i);
|
|
902
|
+
if (cueOutMatch) {
|
|
903
|
+
const arg = (cueOutMatch[1] ?? "").trim();
|
|
904
|
+
const dur = this.parseCueOutDuration(arg);
|
|
905
|
+
const marker = {
|
|
906
|
+
type: "start",
|
|
907
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
908
|
+
...dur !== void 0 ? { durationSeconds: dur } : {},
|
|
909
|
+
raw: { id3: text }
|
|
910
|
+
};
|
|
911
|
+
return marker;
|
|
912
|
+
}
|
|
913
|
+
const cueOutContMatch = text.match(/EXT-X-CUE-OUT-CONT:([^\r\n]*)/i);
|
|
914
|
+
if (cueOutContMatch) {
|
|
915
|
+
const arg = (cueOutContMatch[1] ?? "").trim();
|
|
916
|
+
const cont = this.parseCueOutCont(arg);
|
|
917
|
+
const marker = {
|
|
918
|
+
type: "progress",
|
|
919
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
920
|
+
...cont?.duration !== void 0 ? { durationSeconds: cont.duration } : {},
|
|
921
|
+
raw: { id3: text }
|
|
922
|
+
};
|
|
923
|
+
return marker;
|
|
924
|
+
}
|
|
925
|
+
const cueInMatch = text.match(/EXT-X-CUE-IN\b/i) || text.match(/CUE-IN\b/i);
|
|
926
|
+
if (cueInMatch) {
|
|
927
|
+
const marker = {
|
|
928
|
+
type: "end",
|
|
929
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
930
|
+
raw: { id3: text }
|
|
931
|
+
};
|
|
932
|
+
return marker;
|
|
933
|
+
}
|
|
934
|
+
const daterangeMatch = text.match(/EXT-X-DATERANGE:([^\r\n]*)/i);
|
|
935
|
+
if (daterangeMatch) {
|
|
936
|
+
const attrs = this.parseAttributeList(daterangeMatch[1] ?? "");
|
|
937
|
+
const hasScteOut = "SCTE35-OUT" in attrs || attrs["SCTE35-OUT"] !== void 0;
|
|
938
|
+
const hasScteIn = "SCTE35-IN" in attrs || attrs["SCTE35-IN"] !== void 0;
|
|
939
|
+
const klass = String(attrs["CLASS"] ?? "");
|
|
940
|
+
const duration = this.toNumber(attrs["DURATION"]);
|
|
941
|
+
if (hasScteOut || /com\.apple\.hls\.cue/i.test(klass)) {
|
|
942
|
+
const marker = {
|
|
943
|
+
type: "start",
|
|
944
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
945
|
+
...duration !== void 0 ? { durationSeconds: duration } : {},
|
|
946
|
+
raw: { id3: text, attrs }
|
|
947
|
+
};
|
|
948
|
+
return marker;
|
|
949
|
+
}
|
|
950
|
+
if (hasScteIn) {
|
|
951
|
+
const marker = {
|
|
952
|
+
type: "end",
|
|
953
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
954
|
+
raw: { id3: text, attrs }
|
|
955
|
+
};
|
|
956
|
+
return marker;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (/SCTE35-OUT/i.test(text)) {
|
|
960
|
+
const marker = {
|
|
961
|
+
type: "start",
|
|
962
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
963
|
+
raw: { id3: text }
|
|
964
|
+
};
|
|
965
|
+
return marker;
|
|
966
|
+
}
|
|
967
|
+
if (/SCTE35-IN/i.test(text)) {
|
|
968
|
+
const marker = {
|
|
969
|
+
type: "end",
|
|
970
|
+
...tag.ptsSeconds !== void 0 ? { ptsSeconds: tag.ptsSeconds } : {},
|
|
971
|
+
raw: { id3: text }
|
|
972
|
+
};
|
|
973
|
+
return marker;
|
|
974
|
+
}
|
|
975
|
+
if (tag.value instanceof Uint8Array) {
|
|
976
|
+
const bin = this.parseScte35Binary(tag.value);
|
|
977
|
+
if (bin) return bin;
|
|
978
|
+
}
|
|
979
|
+
return void 0;
|
|
980
|
+
}
|
|
981
|
+
decodeId3ValueToText(value) {
|
|
982
|
+
try {
|
|
983
|
+
if (typeof value === "string") return value;
|
|
984
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
985
|
+
const text = decoder.decode(value);
|
|
986
|
+
if (text && /[\x20-\x7E]/.test(text)) return text;
|
|
987
|
+
let out = "";
|
|
988
|
+
for (let i = 0; i < value.length; i++)
|
|
989
|
+
out += String.fromCharCode(value[i]);
|
|
990
|
+
return out;
|
|
991
|
+
} catch {
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
onScte35Marker(marker) {
|
|
996
|
+
if (this.config.debugAdTiming) {
|
|
997
|
+
console.log("[StormcloudVideoPlayer] SCTE-35 marker detected:", {
|
|
998
|
+
type: marker.type,
|
|
999
|
+
ptsSeconds: marker.ptsSeconds,
|
|
1000
|
+
durationSeconds: marker.durationSeconds,
|
|
1001
|
+
currentTime: this.video.currentTime,
|
|
1002
|
+
raw: marker.raw
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (marker.type === "start") {
|
|
1006
|
+
this.inAdBreak = true;
|
|
1007
|
+
const durationMs = marker.durationSeconds != null ? marker.durationSeconds * 1e3 : void 0;
|
|
1008
|
+
this.expectedAdBreakDurationMs = durationMs;
|
|
1009
|
+
this.currentAdBreakStartWallClockMs = Date.now();
|
|
1010
|
+
const isManifestMarker = this.isManifestBasedMarker(marker);
|
|
1011
|
+
const forceImmediate = this.config.immediateManifestAds ?? true;
|
|
1012
|
+
if (this.config.debugAdTiming) {
|
|
1013
|
+
console.log("[StormcloudVideoPlayer] Ad start decision:", {
|
|
1014
|
+
isManifestMarker,
|
|
1015
|
+
forceImmediate,
|
|
1016
|
+
hasPts: typeof marker.ptsSeconds === "number"
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
if (isManifestMarker && forceImmediate) {
|
|
1020
|
+
if (this.config.debugAdTiming) {
|
|
1021
|
+
console.log(
|
|
1022
|
+
"[StormcloudVideoPlayer] Starting ad immediately (manifest-based)"
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
this.clearAdStartTimer();
|
|
1026
|
+
this.handleAdStart(marker);
|
|
1027
|
+
} else if (typeof marker.ptsSeconds === "number") {
|
|
1028
|
+
const tol = this.config.driftToleranceMs ?? 1e3;
|
|
1029
|
+
const nowMs = this.video.currentTime * 1e3;
|
|
1030
|
+
const estCurrentPtsMs = nowMs - this.ptsDriftEmaMs;
|
|
1031
|
+
const deltaMs = Math.floor(marker.ptsSeconds * 1e3 - estCurrentPtsMs);
|
|
1032
|
+
if (this.config.debugAdTiming) {
|
|
1033
|
+
console.log("[StormcloudVideoPlayer] PTS-based timing calculation:", {
|
|
1034
|
+
nowMs,
|
|
1035
|
+
estCurrentPtsMs,
|
|
1036
|
+
markerPtsMs: marker.ptsSeconds * 1e3,
|
|
1037
|
+
deltaMs,
|
|
1038
|
+
tolerance: tol
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
if (deltaMs > tol) {
|
|
1042
|
+
if (this.config.debugAdTiming) {
|
|
1043
|
+
console.log(
|
|
1044
|
+
`[StormcloudVideoPlayer] Scheduling ad start in ${deltaMs}ms`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
this.scheduleAdStartIn(deltaMs);
|
|
1048
|
+
} else {
|
|
1049
|
+
if (this.config.debugAdTiming) {
|
|
1050
|
+
console.log(
|
|
1051
|
+
"[StormcloudVideoPlayer] Starting ad immediately (within tolerance)"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
this.clearAdStartTimer();
|
|
1055
|
+
this.handleAdStart(marker);
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
if (this.config.debugAdTiming) {
|
|
1059
|
+
console.log(
|
|
1060
|
+
"[StormcloudVideoPlayer] Starting ad immediately (fallback)"
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
this.clearAdStartTimer();
|
|
1064
|
+
this.handleAdStart(marker);
|
|
1065
|
+
}
|
|
1066
|
+
if (this.expectedAdBreakDurationMs != null) {
|
|
1067
|
+
this.scheduleAdStopCountdown(this.expectedAdBreakDurationMs);
|
|
1068
|
+
}
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (marker.type === "progress" && this.inAdBreak) {
|
|
1072
|
+
if (marker.durationSeconds != null) {
|
|
1073
|
+
this.expectedAdBreakDurationMs = marker.durationSeconds * 1e3;
|
|
1074
|
+
}
|
|
1075
|
+
if (this.expectedAdBreakDurationMs != null && this.currentAdBreakStartWallClockMs != null) {
|
|
1076
|
+
const elapsedMs = Date.now() - this.currentAdBreakStartWallClockMs;
|
|
1077
|
+
const remainingMs = Math.max(
|
|
1078
|
+
0,
|
|
1079
|
+
this.expectedAdBreakDurationMs - elapsedMs
|
|
1080
|
+
);
|
|
1081
|
+
this.scheduleAdStopCountdown(remainingMs);
|
|
1082
|
+
}
|
|
1083
|
+
if (!this.ima.isAdPlaying()) {
|
|
1084
|
+
const scheduled = this.findCurrentOrNextBreak(
|
|
1085
|
+
this.video.currentTime * 1e3
|
|
1086
|
+
);
|
|
1087
|
+
const tags = this.selectVastTagsForBreak(scheduled) || (this.apiVastTagUrl ? [this.apiVastTagUrl] : void 0);
|
|
1088
|
+
if (tags && tags.length > 0) {
|
|
1089
|
+
const first = tags[0];
|
|
1090
|
+
const rest = tags.slice(1);
|
|
1091
|
+
this.adPodQueue = rest;
|
|
1092
|
+
this.playSingleAd(first).catch(() => {
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (marker.type === "end") {
|
|
1099
|
+
this.inAdBreak = false;
|
|
1100
|
+
this.expectedAdBreakDurationMs = void 0;
|
|
1101
|
+
this.currentAdBreakStartWallClockMs = void 0;
|
|
1102
|
+
this.clearAdStartTimer();
|
|
1103
|
+
this.clearAdStopTimer();
|
|
1104
|
+
if (this.ima.isAdPlaying()) {
|
|
1105
|
+
this.ima.stop().catch(() => {
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
parseCueOutDuration(value) {
|
|
1112
|
+
const num = parseFloat(value.trim());
|
|
1113
|
+
if (!Number.isNaN(num)) return num;
|
|
1114
|
+
const match = value.match(/(?:^|[,\s])DURATION\s*=\s*([0-9.]+)/i) || value.match(/Duration\s*=\s*([0-9.]+)/i);
|
|
1115
|
+
if (match && match[1] != null) {
|
|
1116
|
+
const dStr = match[1];
|
|
1117
|
+
const d = parseFloat(dStr);
|
|
1118
|
+
return Number.isNaN(d) ? void 0 : d;
|
|
1119
|
+
}
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
parseCueOutCont(value) {
|
|
1123
|
+
const elapsedMatch = value.match(/Elapsed\s*=\s*([0-9.]+)/i);
|
|
1124
|
+
const durationMatch = value.match(/Duration\s*=\s*([0-9.]+)/i);
|
|
1125
|
+
const res = {};
|
|
1126
|
+
if (elapsedMatch && elapsedMatch[1] != null) {
|
|
1127
|
+
const e = parseFloat(elapsedMatch[1]);
|
|
1128
|
+
if (!Number.isNaN(e)) res.elapsed = e;
|
|
1129
|
+
}
|
|
1130
|
+
if (durationMatch && durationMatch[1] != null) {
|
|
1131
|
+
const d = parseFloat(durationMatch[1]);
|
|
1132
|
+
if (!Number.isNaN(d)) res.duration = d;
|
|
1133
|
+
}
|
|
1134
|
+
if ("elapsed" in res || "duration" in res) return res;
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
parseAttributeList(value) {
|
|
1138
|
+
const attrs = {};
|
|
1139
|
+
const regex = /([A-Z0-9-]+)=(("[^"]*")|([^",]*))(?:,|$)/gi;
|
|
1140
|
+
let match;
|
|
1141
|
+
while ((match = regex.exec(value)) !== null) {
|
|
1142
|
+
const key = match[1] ?? "";
|
|
1143
|
+
let rawVal = match[3] ?? match[4] ?? "";
|
|
1144
|
+
if (rawVal.startsWith('"') && rawVal.endsWith('"')) {
|
|
1145
|
+
rawVal = rawVal.slice(1, -1);
|
|
1146
|
+
}
|
|
1147
|
+
if (key) {
|
|
1148
|
+
attrs[key] = rawVal;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return attrs;
|
|
1152
|
+
}
|
|
1153
|
+
toNumber(val) {
|
|
1154
|
+
if (val == null) return void 0;
|
|
1155
|
+
const n = typeof val === "string" ? parseFloat(val) : Number(val);
|
|
1156
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
1157
|
+
}
|
|
1158
|
+
isManifestBasedMarker(marker) {
|
|
1159
|
+
const raw = marker.raw;
|
|
1160
|
+
if (!raw) return false;
|
|
1161
|
+
if (raw.tag) {
|
|
1162
|
+
const tag = String(raw.tag);
|
|
1163
|
+
return tag.includes("EXT-X-CUE-OUT") || tag.includes("EXT-X-CUE-IN") || tag.includes("EXT-X-DATERANGE");
|
|
1164
|
+
}
|
|
1165
|
+
if (raw.id3) return false;
|
|
1166
|
+
if (raw.splice_command_type) return false;
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
parseScte35Binary(data) {
|
|
1170
|
+
class BitReader {
|
|
1171
|
+
constructor(buf) {
|
|
1172
|
+
this.buf = buf;
|
|
1173
|
+
this.bytePos = 0;
|
|
1174
|
+
this.bitPos = 0;
|
|
1175
|
+
}
|
|
1176
|
+
readBits(numBits) {
|
|
1177
|
+
let result = 0;
|
|
1178
|
+
while (numBits > 0) {
|
|
1179
|
+
if (this.bytePos >= this.buf.length) return result;
|
|
1180
|
+
const remainingInByte = 8 - this.bitPos;
|
|
1181
|
+
const toRead = Math.min(numBits, remainingInByte);
|
|
1182
|
+
const currentByte = this.buf[this.bytePos];
|
|
1183
|
+
const shift = remainingInByte - toRead;
|
|
1184
|
+
const mask = (1 << toRead) - 1 & 255;
|
|
1185
|
+
const bits = currentByte >> shift & mask;
|
|
1186
|
+
result = result << toRead | bits;
|
|
1187
|
+
this.bitPos += toRead;
|
|
1188
|
+
if (this.bitPos >= 8) {
|
|
1189
|
+
this.bitPos = 0;
|
|
1190
|
+
this.bytePos += 1;
|
|
1191
|
+
}
|
|
1192
|
+
numBits -= toRead;
|
|
1193
|
+
}
|
|
1194
|
+
return result >>> 0;
|
|
1195
|
+
}
|
|
1196
|
+
skipBits(n) {
|
|
1197
|
+
this.readBits(n);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const r = new BitReader(data);
|
|
1201
|
+
const tableId = r.readBits(8);
|
|
1202
|
+
if (tableId !== 252) return void 0;
|
|
1203
|
+
r.readBits(1);
|
|
1204
|
+
r.readBits(1);
|
|
1205
|
+
r.readBits(2);
|
|
1206
|
+
const sectionLength = r.readBits(12);
|
|
1207
|
+
r.readBits(8);
|
|
1208
|
+
r.readBits(1);
|
|
1209
|
+
r.readBits(6);
|
|
1210
|
+
const ptsAdjHigh = r.readBits(1);
|
|
1211
|
+
const ptsAdjLow = r.readBits(32);
|
|
1212
|
+
void ptsAdjHigh;
|
|
1213
|
+
void ptsAdjLow;
|
|
1214
|
+
r.readBits(8);
|
|
1215
|
+
r.readBits(12);
|
|
1216
|
+
const spliceCommandLength = r.readBits(12);
|
|
1217
|
+
const spliceCommandType = r.readBits(8);
|
|
1218
|
+
if (spliceCommandType !== 5) {
|
|
1219
|
+
return void 0;
|
|
1220
|
+
}
|
|
1221
|
+
r.readBits(32);
|
|
1222
|
+
const cancel = r.readBits(1) === 1;
|
|
1223
|
+
r.readBits(7);
|
|
1224
|
+
if (cancel) return void 0;
|
|
1225
|
+
const outOfNetwork = r.readBits(1) === 1;
|
|
1226
|
+
const programSpliceFlag = r.readBits(1) === 1;
|
|
1227
|
+
const durationFlag = r.readBits(1) === 1;
|
|
1228
|
+
const spliceImmediateFlag = r.readBits(1) === 1;
|
|
1229
|
+
r.readBits(4);
|
|
1230
|
+
if (programSpliceFlag && !spliceImmediateFlag) {
|
|
1231
|
+
const timeSpecifiedFlag = r.readBits(1) === 1;
|
|
1232
|
+
if (timeSpecifiedFlag) {
|
|
1233
|
+
r.readBits(6);
|
|
1234
|
+
r.readBits(33);
|
|
1235
|
+
} else {
|
|
1236
|
+
r.readBits(7);
|
|
1237
|
+
}
|
|
1238
|
+
} else if (!programSpliceFlag) {
|
|
1239
|
+
const componentCount = r.readBits(8);
|
|
1240
|
+
for (let i = 0; i < componentCount; i++) {
|
|
1241
|
+
r.readBits(8);
|
|
1242
|
+
if (!spliceImmediateFlag) {
|
|
1243
|
+
const timeSpecifiedFlag = r.readBits(1) === 1;
|
|
1244
|
+
if (timeSpecifiedFlag) {
|
|
1245
|
+
r.readBits(6);
|
|
1246
|
+
r.readBits(33);
|
|
1247
|
+
} else {
|
|
1248
|
+
r.readBits(7);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
let durationSeconds = void 0;
|
|
1254
|
+
if (durationFlag) {
|
|
1255
|
+
r.readBits(6);
|
|
1256
|
+
r.readBits(1);
|
|
1257
|
+
const high = r.readBits(1);
|
|
1258
|
+
const low = r.readBits(32);
|
|
1259
|
+
const durationTicks = high * 4294967296 + low;
|
|
1260
|
+
durationSeconds = durationTicks / 9e4;
|
|
1261
|
+
}
|
|
1262
|
+
r.readBits(16);
|
|
1263
|
+
r.readBits(8);
|
|
1264
|
+
r.readBits(8);
|
|
1265
|
+
if (outOfNetwork) {
|
|
1266
|
+
const marker = {
|
|
1267
|
+
type: "start",
|
|
1268
|
+
...durationSeconds !== void 0 ? { durationSeconds } : {},
|
|
1269
|
+
raw: { splice_command_type: 5 }
|
|
1270
|
+
};
|
|
1271
|
+
return marker;
|
|
1272
|
+
}
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
initializeTracking() {
|
|
1276
|
+
sendInitialTracking(this.config.licenseKey).catch((error) => {
|
|
1277
|
+
if (this.config.debugAdTiming) {
|
|
1278
|
+
console.warn(
|
|
1279
|
+
"[StormcloudVideoPlayer] Failed to send initial tracking:",
|
|
1280
|
+
error
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
this.heartbeatInterval = window.setInterval(() => {
|
|
1285
|
+
this.sendHeartbeatIfNeeded();
|
|
1286
|
+
}, 5e3);
|
|
1287
|
+
}
|
|
1288
|
+
sendHeartbeatIfNeeded() {
|
|
1289
|
+
const now = Date.now();
|
|
1290
|
+
if (!this.lastHeartbeatTime || now - this.lastHeartbeatTime > 3e4) {
|
|
1291
|
+
this.lastHeartbeatTime = now;
|
|
1292
|
+
sendHeartbeat(this.config.licenseKey).catch((error) => {
|
|
1293
|
+
if (this.config.debugAdTiming) {
|
|
1294
|
+
console.warn(
|
|
1295
|
+
"[StormcloudVideoPlayer] Failed to send heartbeat:",
|
|
1296
|
+
error
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async fetchAdConfiguration() {
|
|
1303
|
+
const apiUrl = "https://adstorm.co/api-adstorm-dev/adstorm/ads/web";
|
|
1304
|
+
if (this.config.debugAdTiming) {
|
|
1305
|
+
console.log(
|
|
1306
|
+
"[StormcloudVideoPlayer] Fetching ad configuration from:",
|
|
1307
|
+
apiUrl
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const headers = {};
|
|
1311
|
+
if (this.config.licenseKey) {
|
|
1312
|
+
headers["Authorization"] = `Bearer ${this.config.licenseKey}`;
|
|
1313
|
+
}
|
|
1314
|
+
const response = await fetch(apiUrl, { headers });
|
|
1315
|
+
if (!response.ok) {
|
|
1316
|
+
throw new Error(`Failed to fetch ad configuration: ${response.status}`);
|
|
1317
|
+
}
|
|
1318
|
+
const data = await response.json();
|
|
1319
|
+
const imaPayload = data.response?.ima?.["publisherdesk.ima"]?.payload;
|
|
1320
|
+
if (imaPayload) {
|
|
1321
|
+
this.apiVastTagUrl = decodeURIComponent(imaPayload);
|
|
1322
|
+
if (this.config.debugAdTiming) {
|
|
1323
|
+
console.log(
|
|
1324
|
+
"[StormcloudVideoPlayer] Extracted VAST tag URL:",
|
|
1325
|
+
this.apiVastTagUrl
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
this.vastConfig = data.response?.options?.vast;
|
|
1330
|
+
if (this.config.debugAdTiming) {
|
|
1331
|
+
console.log("[StormcloudVideoPlayer] Ad configuration loaded:", {
|
|
1332
|
+
vastTagUrl: this.apiVastTagUrl,
|
|
1333
|
+
vastConfig: this.vastConfig
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
getCurrentAdIndex() {
|
|
1338
|
+
return this.currentAdIndex;
|
|
1339
|
+
}
|
|
1340
|
+
getTotalAdsInBreak() {
|
|
1341
|
+
return this.totalAdsInBreak;
|
|
1342
|
+
}
|
|
1343
|
+
isAdPlaying() {
|
|
1344
|
+
return this.inAdBreak && this.ima.isAdPlaying();
|
|
1345
|
+
}
|
|
1346
|
+
isShowingAds() {
|
|
1347
|
+
return this.showAds;
|
|
1348
|
+
}
|
|
1349
|
+
getStreamType() {
|
|
1350
|
+
const url = this.config.src.toLowerCase();
|
|
1351
|
+
if (url.includes(".m3u8") || url.includes("/hls/") || url.includes("application/vnd.apple.mpegurl")) {
|
|
1352
|
+
return "hls";
|
|
1353
|
+
}
|
|
1354
|
+
return "other";
|
|
1355
|
+
}
|
|
1356
|
+
shouldShowNativeControls() {
|
|
1357
|
+
const streamType = this.getStreamType();
|
|
1358
|
+
if (streamType === "other") {
|
|
1359
|
+
return !(this.config.showCustomControls ?? false);
|
|
1360
|
+
}
|
|
1361
|
+
return !!(this.config.allowNativeHls && !(this.config.showCustomControls ?? false));
|
|
1362
|
+
}
|
|
1363
|
+
async loadDefaultVastFromAdstorm(adstormApiUrl, params) {
|
|
1364
|
+
const usp = new URLSearchParams(params || {});
|
|
1365
|
+
const url = `${adstormApiUrl}?${usp.toString()}`;
|
|
1366
|
+
const res = await fetch(url);
|
|
1367
|
+
if (!res.ok) throw new Error(`Failed to fetch adstorm ads: ${res.status}`);
|
|
1368
|
+
const data = await res.json();
|
|
1369
|
+
const tag = data?.adTagUrl || data?.vastTagUrl || data?.tagUrl;
|
|
1370
|
+
if (typeof tag === "string" && tag.length > 0) {
|
|
1371
|
+
this.apiVastTagUrl = tag;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async handleAdStart(_marker) {
|
|
1375
|
+
const scheduled = this.findCurrentOrNextBreak(
|
|
1376
|
+
this.video.currentTime * 1e3
|
|
1377
|
+
);
|
|
1378
|
+
const tags = this.selectVastTagsForBreak(scheduled);
|
|
1379
|
+
let vastTagUrl;
|
|
1380
|
+
let adsNumber = 1;
|
|
1381
|
+
if (this.apiVastTagUrl) {
|
|
1382
|
+
vastTagUrl = this.apiVastTagUrl;
|
|
1383
|
+
if (this.vastConfig) {
|
|
1384
|
+
const isHls = this.config.src.includes(".m3u8") || this.config.src.includes("hls");
|
|
1385
|
+
if (isHls && this.vastConfig.cue_tones?.number_ads) {
|
|
1386
|
+
adsNumber = this.vastConfig.cue_tones.number_ads;
|
|
1387
|
+
} else if (!isHls && this.vastConfig.timer_vod?.number_ads) {
|
|
1388
|
+
adsNumber = this.vastConfig.timer_vod.number_ads;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
this.adPodQueue = new Array(adsNumber - 1).fill(vastTagUrl);
|
|
1392
|
+
this.currentAdIndex = 0;
|
|
1393
|
+
this.totalAdsInBreak = adsNumber;
|
|
1394
|
+
if (this.config.debugAdTiming) {
|
|
1395
|
+
console.log(
|
|
1396
|
+
`[StormcloudVideoPlayer] Using API VAST tag with ${adsNumber} ads:`,
|
|
1397
|
+
vastTagUrl
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
} else if (tags && tags.length > 0) {
|
|
1401
|
+
vastTagUrl = tags[0];
|
|
1402
|
+
const rest = tags.slice(1);
|
|
1403
|
+
this.adPodQueue = rest;
|
|
1404
|
+
this.currentAdIndex = 0;
|
|
1405
|
+
this.totalAdsInBreak = tags.length;
|
|
1406
|
+
if (this.config.debugAdTiming) {
|
|
1407
|
+
console.log(
|
|
1408
|
+
"[StormcloudVideoPlayer] Using scheduled VAST tag:",
|
|
1409
|
+
vastTagUrl
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
if (this.config.debugAdTiming) {
|
|
1414
|
+
console.log("[StormcloudVideoPlayer] No VAST tag available for ad");
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (vastTagUrl) {
|
|
1419
|
+
this.showAds = true;
|
|
1420
|
+
this.currentAdIndex++;
|
|
1421
|
+
await this.playSingleAd(vastTagUrl);
|
|
1422
|
+
}
|
|
1423
|
+
if (this.expectedAdBreakDurationMs == null && scheduled?.durationMs != null) {
|
|
1424
|
+
this.expectedAdBreakDurationMs = scheduled.durationMs;
|
|
1425
|
+
this.currentAdBreakStartWallClockMs = this.currentAdBreakStartWallClockMs ?? Date.now();
|
|
1426
|
+
this.scheduleAdStopCountdown(this.expectedAdBreakDurationMs);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
findCurrentOrNextBreak(nowMs) {
|
|
1430
|
+
const schedule = [];
|
|
1431
|
+
let candidate;
|
|
1432
|
+
for (const b of schedule) {
|
|
1433
|
+
const tol = this.config.driftToleranceMs ?? 1e3;
|
|
1434
|
+
if (b.startTimeMs <= nowMs + tol && (candidate == null || b.startTimeMs > (candidate.startTimeMs || 0))) {
|
|
1435
|
+
candidate = b;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return candidate;
|
|
1439
|
+
}
|
|
1440
|
+
onTimeUpdate(currentTimeSec) {
|
|
1441
|
+
if (this.ima.isAdPlaying()) return;
|
|
1442
|
+
const nowMs = currentTimeSec * 1e3;
|
|
1443
|
+
const breakToPlay = this.findBreakForTime(nowMs);
|
|
1444
|
+
if (breakToPlay) {
|
|
1445
|
+
this.handleMidAdJoin(breakToPlay, nowMs);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async handleMidAdJoin(adBreak, nowMs) {
|
|
1449
|
+
const durationMs = adBreak.durationMs ?? 0;
|
|
1450
|
+
const endMs = adBreak.startTimeMs + durationMs;
|
|
1451
|
+
if (durationMs > 0 && nowMs > adBreak.startTimeMs && nowMs < endMs) {
|
|
1452
|
+
const remainingMs = endMs - nowMs;
|
|
1453
|
+
const tags = this.selectVastTagsForBreak(adBreak) || (this.apiVastTagUrl ? [this.apiVastTagUrl] : void 0);
|
|
1454
|
+
if (tags && tags.length > 0) {
|
|
1455
|
+
const first = tags[0];
|
|
1456
|
+
const rest = tags.slice(1);
|
|
1457
|
+
this.adPodQueue = rest;
|
|
1458
|
+
await this.playSingleAd(first);
|
|
1459
|
+
this.inAdBreak = true;
|
|
1460
|
+
this.expectedAdBreakDurationMs = remainingMs;
|
|
1461
|
+
this.currentAdBreakStartWallClockMs = Date.now();
|
|
1462
|
+
this.scheduleAdStopCountdown(remainingMs);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
scheduleAdStopCountdown(remainingMs) {
|
|
1467
|
+
this.clearAdStopTimer();
|
|
1468
|
+
const ms = Math.max(0, Math.floor(remainingMs));
|
|
1469
|
+
if (ms === 0) {
|
|
1470
|
+
this.ensureAdStoppedByTimer();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
this.adStopTimerId = window.setTimeout(() => {
|
|
1474
|
+
this.ensureAdStoppedByTimer();
|
|
1475
|
+
}, ms);
|
|
1476
|
+
}
|
|
1477
|
+
clearAdStopTimer() {
|
|
1478
|
+
if (this.adStopTimerId != null) {
|
|
1479
|
+
clearTimeout(this.adStopTimerId);
|
|
1480
|
+
this.adStopTimerId = void 0;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
ensureAdStoppedByTimer() {
|
|
1484
|
+
if (!this.inAdBreak) return;
|
|
1485
|
+
this.inAdBreak = false;
|
|
1486
|
+
this.expectedAdBreakDurationMs = void 0;
|
|
1487
|
+
this.currentAdBreakStartWallClockMs = void 0;
|
|
1488
|
+
this.adStopTimerId = void 0;
|
|
1489
|
+
if (this.ima.isAdPlaying()) {
|
|
1490
|
+
this.ima.stop().catch(() => {
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
scheduleAdStartIn(delayMs) {
|
|
1495
|
+
this.clearAdStartTimer();
|
|
1496
|
+
const ms = Math.max(0, Math.floor(delayMs));
|
|
1497
|
+
if (ms === 0) {
|
|
1498
|
+
this.handleAdStart({ type: "start" }).catch(() => {
|
|
1499
|
+
});
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
this.adStartTimerId = window.setTimeout(() => {
|
|
1503
|
+
this.handleAdStart({ type: "start" }).catch(() => {
|
|
1504
|
+
});
|
|
1505
|
+
}, ms);
|
|
1506
|
+
}
|
|
1507
|
+
clearAdStartTimer() {
|
|
1508
|
+
if (this.adStartTimerId != null) {
|
|
1509
|
+
clearTimeout(this.adStartTimerId);
|
|
1510
|
+
this.adStartTimerId = void 0;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
updatePtsDrift(ptsSecondsSample) {
|
|
1514
|
+
const sampleMs = (this.video.currentTime - ptsSecondsSample) * 1e3;
|
|
1515
|
+
if (!Number.isFinite(sampleMs) || Math.abs(sampleMs) > 6e4) return;
|
|
1516
|
+
const alpha = 0.1;
|
|
1517
|
+
this.ptsDriftEmaMs = this.ptsDriftEmaMs * (1 - alpha) + sampleMs * alpha;
|
|
1518
|
+
}
|
|
1519
|
+
async playSingleAd(vastTagUrl) {
|
|
1520
|
+
if (this.config.debugAdTiming) {
|
|
1521
|
+
console.log("[StormcloudVideoPlayer] Attempting to play ad:", vastTagUrl);
|
|
1522
|
+
}
|
|
1523
|
+
this.startAdFailsafeTimer();
|
|
1524
|
+
try {
|
|
1525
|
+
await this.ima.requestAds(vastTagUrl);
|
|
1526
|
+
await this.ima.play();
|
|
1527
|
+
if (this.config.debugAdTiming) {
|
|
1528
|
+
console.log("[StormcloudVideoPlayer] Ad playback started successfully");
|
|
1529
|
+
}
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
if (this.config.debugAdTiming) {
|
|
1532
|
+
console.error("[StormcloudVideoPlayer] Ad playback failed:", error);
|
|
1533
|
+
}
|
|
1534
|
+
this.handleAdFailure();
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
handleAdFailure() {
|
|
1538
|
+
if (this.config.debugAdTiming) {
|
|
1539
|
+
console.log(
|
|
1540
|
+
"[StormcloudVideoPlayer] Handling ad failure - resuming content"
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
this.inAdBreak = false;
|
|
1544
|
+
this.expectedAdBreakDurationMs = void 0;
|
|
1545
|
+
this.currentAdBreakStartWallClockMs = void 0;
|
|
1546
|
+
this.clearAdStartTimer();
|
|
1547
|
+
this.clearAdStopTimer();
|
|
1548
|
+
this.clearAdFailsafeTimer();
|
|
1549
|
+
this.adPodQueue = [];
|
|
1550
|
+
this.showAds = false;
|
|
1551
|
+
this.currentAdIndex = 0;
|
|
1552
|
+
this.totalAdsInBreak = 0;
|
|
1553
|
+
if (this.video.paused) {
|
|
1554
|
+
this.video.play().catch(() => {
|
|
1555
|
+
if (this.config.debugAdTiming) {
|
|
1556
|
+
console.error(
|
|
1557
|
+
"[StormcloudVideoPlayer] Failed to resume video after ad failure"
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
startAdFailsafeTimer() {
|
|
1564
|
+
this.clearAdFailsafeTimer();
|
|
1565
|
+
const failsafeMs = this.config.adFailsafeTimeoutMs ?? 1e4;
|
|
1566
|
+
if (this.config.debugAdTiming) {
|
|
1567
|
+
console.log(
|
|
1568
|
+
`[StormcloudVideoPlayer] Starting failsafe timer (${failsafeMs}ms)`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
this.adFailsafeTimerId = window.setTimeout(() => {
|
|
1572
|
+
if (this.video.paused) {
|
|
1573
|
+
if (this.config.debugAdTiming) {
|
|
1574
|
+
console.warn(
|
|
1575
|
+
"[StormcloudVideoPlayer] Failsafe timer triggered - forcing video resume"
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
this.handleAdFailure();
|
|
1579
|
+
}
|
|
1580
|
+
}, failsafeMs);
|
|
1581
|
+
}
|
|
1582
|
+
clearAdFailsafeTimer() {
|
|
1583
|
+
if (this.adFailsafeTimerId != null) {
|
|
1584
|
+
clearTimeout(this.adFailsafeTimerId);
|
|
1585
|
+
this.adFailsafeTimerId = void 0;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
selectVastTagsForBreak(b) {
|
|
1589
|
+
if (!b || !b.vastTagUrl) return void 0;
|
|
1590
|
+
if (b.vastTagUrl.includes(",")) {
|
|
1591
|
+
return b.vastTagUrl.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1592
|
+
}
|
|
1593
|
+
return [b.vastTagUrl];
|
|
1594
|
+
}
|
|
1595
|
+
getRemainingAdMs() {
|
|
1596
|
+
if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
|
|
1597
|
+
return 0;
|
|
1598
|
+
const elapsed = Date.now() - this.currentAdBreakStartWallClockMs;
|
|
1599
|
+
return Math.max(0, this.expectedAdBreakDurationMs - elapsed);
|
|
1600
|
+
}
|
|
1601
|
+
findBreakForTime(nowMs) {
|
|
1602
|
+
const schedule = [];
|
|
1603
|
+
for (const b of schedule) {
|
|
1604
|
+
const end = (b.startTimeMs || 0) + (b.durationMs || 0);
|
|
1605
|
+
if (nowMs >= (b.startTimeMs || 0) && (b.durationMs ? nowMs < end : true)) {
|
|
1606
|
+
return b;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return void 0;
|
|
1610
|
+
}
|
|
1611
|
+
toggleMute() {
|
|
1612
|
+
if (this.ima.isAdPlaying()) {
|
|
1613
|
+
const currentPerceptualState = this.isMuted();
|
|
1614
|
+
const newMutedState = !currentPerceptualState;
|
|
1615
|
+
this.ima.updateOriginalMutedState(newMutedState);
|
|
1616
|
+
this.ima.setAdVolume(newMutedState ? 0 : 1);
|
|
1617
|
+
if (this.config.debugAdTiming) {
|
|
1618
|
+
console.log(
|
|
1619
|
+
"[StormcloudVideoPlayer] Mute toggle during ad - immediately applied:",
|
|
1620
|
+
newMutedState
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
this.video.muted = !this.video.muted;
|
|
1625
|
+
if (this.config.debugAdTiming) {
|
|
1626
|
+
console.log("[StormcloudVideoPlayer] Muted:", this.video.muted);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
toggleFullscreen() {
|
|
1631
|
+
return new Promise((resolve, reject) => {
|
|
1632
|
+
if (!document.fullscreenElement) {
|
|
1633
|
+
const container = this.video.parentElement;
|
|
1634
|
+
if (!container) {
|
|
1635
|
+
reject(new Error("No parent container found for fullscreen"));
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
container.requestFullscreen().then(() => {
|
|
1639
|
+
if (this.config.debugAdTiming) {
|
|
1640
|
+
console.log("[StormcloudVideoPlayer] Entered fullscreen");
|
|
1641
|
+
}
|
|
1642
|
+
resolve();
|
|
1643
|
+
}).catch((err) => {
|
|
1644
|
+
if (this.config.debugAdTiming) {
|
|
1645
|
+
console.error("[StormcloudVideoPlayer] Fullscreen error:", err);
|
|
1646
|
+
}
|
|
1647
|
+
reject(err);
|
|
1648
|
+
});
|
|
1649
|
+
} else {
|
|
1650
|
+
document.exitFullscreen().then(() => {
|
|
1651
|
+
if (this.config.debugAdTiming) {
|
|
1652
|
+
console.log("[StormcloudVideoPlayer] Exited fullscreen");
|
|
1653
|
+
}
|
|
1654
|
+
resolve();
|
|
1655
|
+
}).catch((err) => {
|
|
1656
|
+
if (this.config.debugAdTiming) {
|
|
1657
|
+
console.error(
|
|
1658
|
+
"[StormcloudVideoPlayer] Exit fullscreen error:",
|
|
1659
|
+
err
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
reject(err);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
isMuted() {
|
|
1668
|
+
if (this.ima.isAdPlaying()) {
|
|
1669
|
+
const adVolume = this.ima.getAdVolume();
|
|
1670
|
+
return adVolume === 0;
|
|
1671
|
+
}
|
|
1672
|
+
return this.video.muted;
|
|
1673
|
+
}
|
|
1674
|
+
isFullscreen() {
|
|
1675
|
+
return !!document.fullscreenElement;
|
|
1676
|
+
}
|
|
1677
|
+
get videoElement() {
|
|
1678
|
+
return this.video;
|
|
1679
|
+
}
|
|
1680
|
+
resize() {
|
|
1681
|
+
if (this.config.debugAdTiming) {
|
|
1682
|
+
console.log("[StormcloudVideoPlayer] Resizing player");
|
|
1683
|
+
}
|
|
1684
|
+
if (this.ima && this.ima.isAdPlaying()) {
|
|
1685
|
+
const width = this.video.clientWidth || 640;
|
|
1686
|
+
const height = this.video.clientHeight || 360;
|
|
1687
|
+
if (this.config.debugAdTiming) {
|
|
1688
|
+
console.log(
|
|
1689
|
+
`[StormcloudVideoPlayer] Resizing ads manager to ${width}x${height}`
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
this.ima.resize(width, height);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
destroy() {
|
|
1696
|
+
this.clearAdStartTimer();
|
|
1697
|
+
this.clearAdStopTimer();
|
|
1698
|
+
this.clearAdFailsafeTimer();
|
|
1699
|
+
if (this.heartbeatInterval) {
|
|
1700
|
+
clearInterval(this.heartbeatInterval);
|
|
1701
|
+
this.heartbeatInterval = void 0;
|
|
1702
|
+
}
|
|
1703
|
+
this.hls?.destroy();
|
|
1704
|
+
this.ima?.destroy();
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
// src/players/HlsPlayer.tsx
|
|
1709
|
+
var HlsPlayer = class extends import_react2.Component {
|
|
1710
|
+
constructor() {
|
|
1711
|
+
super(...arguments);
|
|
1712
|
+
this.player = null;
|
|
1713
|
+
this.mounted = false;
|
|
1714
|
+
this.load = async () => {
|
|
1715
|
+
if (!this.props.videoElement || !this.props.src) return;
|
|
1716
|
+
try {
|
|
1717
|
+
if (this.player) {
|
|
1718
|
+
this.player.destroy();
|
|
1719
|
+
this.player = null;
|
|
1720
|
+
}
|
|
1721
|
+
const config = {
|
|
1722
|
+
src: this.props.src,
|
|
1723
|
+
videoElement: this.props.videoElement
|
|
1724
|
+
};
|
|
1725
|
+
if (this.props.autoplay !== void 0)
|
|
1726
|
+
config.autoplay = this.props.autoplay;
|
|
1727
|
+
if (this.props.muted !== void 0) config.muted = this.props.muted;
|
|
1728
|
+
if (this.props.lowLatencyMode !== void 0)
|
|
1729
|
+
config.lowLatencyMode = this.props.lowLatencyMode;
|
|
1730
|
+
if (this.props.allowNativeHls !== void 0)
|
|
1731
|
+
config.allowNativeHls = this.props.allowNativeHls;
|
|
1732
|
+
if (this.props.driftToleranceMs !== void 0)
|
|
1733
|
+
config.driftToleranceMs = this.props.driftToleranceMs;
|
|
1734
|
+
if (this.props.immediateManifestAds !== void 0)
|
|
1735
|
+
config.immediateManifestAds = this.props.immediateManifestAds;
|
|
1736
|
+
if (this.props.debugAdTiming !== void 0)
|
|
1737
|
+
config.debugAdTiming = this.props.debugAdTiming;
|
|
1738
|
+
if (this.props.showCustomControls !== void 0)
|
|
1739
|
+
config.showCustomControls = this.props.showCustomControls;
|
|
1740
|
+
if (this.props.onVolumeToggle !== void 0)
|
|
1741
|
+
config.onVolumeToggle = this.props.onVolumeToggle;
|
|
1742
|
+
if (this.props.onFullscreenToggle !== void 0)
|
|
1743
|
+
config.onFullscreenToggle = this.props.onFullscreenToggle;
|
|
1744
|
+
if (this.props.onControlClick !== void 0)
|
|
1745
|
+
config.onControlClick = this.props.onControlClick;
|
|
1746
|
+
if (this.props.licenseKey !== void 0)
|
|
1747
|
+
config.licenseKey = this.props.licenseKey;
|
|
1748
|
+
if (this.props.adFailsafeTimeoutMs !== void 0)
|
|
1749
|
+
config.adFailsafeTimeoutMs = this.props.adFailsafeTimeoutMs;
|
|
1750
|
+
this.player = new StormcloudVideoPlayer(config);
|
|
1751
|
+
this.props.onMount?.(this);
|
|
1752
|
+
await this.player.load();
|
|
1753
|
+
if (this.mounted) {
|
|
1754
|
+
this.props.onReady?.();
|
|
1755
|
+
}
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
if (this.mounted) {
|
|
1758
|
+
this.props.onError?.(error);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
this.play = () => {
|
|
1763
|
+
if (this.props.videoElement) {
|
|
1764
|
+
this.props.videoElement.play();
|
|
1765
|
+
this.props.onPlay?.();
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
this.pause = () => {
|
|
1769
|
+
if (this.props.videoElement) {
|
|
1770
|
+
this.props.videoElement.pause();
|
|
1771
|
+
this.props.onPause?.();
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
this.stop = () => {
|
|
1775
|
+
this.pause();
|
|
1776
|
+
if (this.props.videoElement) {
|
|
1777
|
+
this.props.videoElement.currentTime = 0;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
this.seekTo = (seconds, keepPlaying) => {
|
|
1781
|
+
if (this.props.videoElement) {
|
|
1782
|
+
this.props.videoElement.currentTime = seconds;
|
|
1783
|
+
if (!keepPlaying) {
|
|
1784
|
+
this.pause();
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
this.setVolume = (volume) => {
|
|
1789
|
+
if (this.props.videoElement) {
|
|
1790
|
+
this.props.videoElement.volume = Math.max(0, Math.min(1, volume));
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
this.mute = () => {
|
|
1794
|
+
if (this.props.videoElement) {
|
|
1795
|
+
this.props.videoElement.muted = true;
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
this.unmute = () => {
|
|
1799
|
+
if (this.props.videoElement) {
|
|
1800
|
+
this.props.videoElement.muted = false;
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
this.setPlaybackRate = (rate) => {
|
|
1804
|
+
if (this.props.videoElement && rate > 0) {
|
|
1805
|
+
this.props.videoElement.playbackRate = rate;
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
this.getDuration = () => {
|
|
1809
|
+
if (this.props.videoElement && isFinite(this.props.videoElement.duration)) {
|
|
1810
|
+
return this.props.videoElement.duration;
|
|
1811
|
+
}
|
|
1812
|
+
return null;
|
|
1813
|
+
};
|
|
1814
|
+
this.getCurrentTime = () => {
|
|
1815
|
+
if (this.props.videoElement && isFinite(this.props.videoElement.currentTime)) {
|
|
1816
|
+
return this.props.videoElement.currentTime;
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
};
|
|
1820
|
+
this.getSecondsLoaded = () => {
|
|
1821
|
+
if (this.props.videoElement && this.props.videoElement.buffered.length > 0) {
|
|
1822
|
+
return this.props.videoElement.buffered.end(
|
|
1823
|
+
this.props.videoElement.buffered.length - 1
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
return null;
|
|
1827
|
+
};
|
|
1828
|
+
this.getInternalPlayer = (key = "player") => {
|
|
1829
|
+
if (key === "player") return this.player;
|
|
1830
|
+
if (key === "video") return this.props.videoElement;
|
|
1831
|
+
if (key === "hls" && this.player) return this.player.hls;
|
|
1832
|
+
return null;
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
componentDidMount() {
|
|
1836
|
+
this.mounted = true;
|
|
1837
|
+
this.load();
|
|
1838
|
+
}
|
|
1839
|
+
componentWillUnmount() {
|
|
1840
|
+
this.mounted = false;
|
|
1841
|
+
if (this.player) {
|
|
1842
|
+
this.player.destroy();
|
|
1843
|
+
this.player = null;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
componentDidUpdate(prevProps) {
|
|
1847
|
+
if (prevProps.src !== this.props.src) {
|
|
1848
|
+
this.load();
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
render() {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
HlsPlayer.displayName = "HlsPlayer";
|
|
1856
|
+
HlsPlayer.canPlay = canPlay.hls;
|
|
1857
|
+
|
|
1858
|
+
// src/players/FilePlayer.tsx
|
|
1859
|
+
var import_react3 = require("react");
|
|
1860
|
+
var FilePlayer = class extends import_react3.Component {
|
|
1861
|
+
constructor() {
|
|
1862
|
+
super(...arguments);
|
|
1863
|
+
this.mounted = false;
|
|
1864
|
+
this.ready = false;
|
|
1865
|
+
this.load = () => {
|
|
1866
|
+
if (!this.props.videoElement || !this.props.src) return;
|
|
1867
|
+
const video = this.props.videoElement;
|
|
1868
|
+
const handleLoadedMetadata = () => {
|
|
1869
|
+
if (this.mounted && !this.ready) {
|
|
1870
|
+
this.ready = true;
|
|
1871
|
+
this.props.onReady?.();
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
const handlePlay = () => {
|
|
1875
|
+
if (this.mounted) {
|
|
1876
|
+
this.props.onPlay?.();
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
const handlePause = () => {
|
|
1880
|
+
if (this.mounted) {
|
|
1881
|
+
this.props.onPause?.();
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
const handleEnded = () => {
|
|
1885
|
+
if (this.mounted) {
|
|
1886
|
+
this.props.onEnded?.();
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const handleError = (error) => {
|
|
1890
|
+
if (this.mounted) {
|
|
1891
|
+
this.props.onError?.(error);
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const handleLoadedData = () => {
|
|
1895
|
+
if (this.mounted) {
|
|
1896
|
+
this.props.onLoaded?.();
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
1900
|
+
video.addEventListener("play", handlePlay);
|
|
1901
|
+
video.addEventListener("pause", handlePause);
|
|
1902
|
+
video.addEventListener("ended", handleEnded);
|
|
1903
|
+
video.addEventListener("error", handleError);
|
|
1904
|
+
video.addEventListener("loadeddata", handleLoadedData);
|
|
1905
|
+
video.src = this.props.src;
|
|
1906
|
+
if (this.props.autoplay !== void 0) video.autoplay = this.props.autoplay;
|
|
1907
|
+
if (this.props.muted !== void 0) video.muted = this.props.muted;
|
|
1908
|
+
if (this.props.loop !== void 0) video.loop = this.props.loop;
|
|
1909
|
+
if (this.props.controls !== void 0) video.controls = this.props.controls;
|
|
1910
|
+
if (this.props.playsInline !== void 0)
|
|
1911
|
+
video.playsInline = this.props.playsInline;
|
|
1912
|
+
if (this.props.preload !== void 0)
|
|
1913
|
+
video.preload = this.props.preload;
|
|
1914
|
+
if (this.props.poster !== void 0) video.poster = this.props.poster;
|
|
1915
|
+
this.props.onMount?.(this);
|
|
1916
|
+
return () => {
|
|
1917
|
+
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
1918
|
+
video.removeEventListener("play", handlePlay);
|
|
1919
|
+
video.removeEventListener("pause", handlePause);
|
|
1920
|
+
video.removeEventListener("ended", handleEnded);
|
|
1921
|
+
video.removeEventListener("error", handleError);
|
|
1922
|
+
video.removeEventListener("loadeddata", handleLoadedData);
|
|
1923
|
+
};
|
|
1924
|
+
};
|
|
1925
|
+
this.play = () => {
|
|
1926
|
+
if (this.props.videoElement) {
|
|
1927
|
+
this.props.videoElement.play();
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
this.pause = () => {
|
|
1931
|
+
if (this.props.videoElement) {
|
|
1932
|
+
this.props.videoElement.pause();
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
this.stop = () => {
|
|
1936
|
+
this.pause();
|
|
1937
|
+
if (this.props.videoElement) {
|
|
1938
|
+
this.props.videoElement.currentTime = 0;
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
this.seekTo = (seconds, keepPlaying) => {
|
|
1942
|
+
if (this.props.videoElement) {
|
|
1943
|
+
this.props.videoElement.currentTime = seconds;
|
|
1944
|
+
if (!keepPlaying) {
|
|
1945
|
+
this.pause();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
this.setVolume = (volume) => {
|
|
1950
|
+
if (this.props.videoElement) {
|
|
1951
|
+
this.props.videoElement.volume = Math.max(0, Math.min(1, volume));
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
this.mute = () => {
|
|
1955
|
+
if (this.props.videoElement) {
|
|
1956
|
+
this.props.videoElement.muted = true;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
this.unmute = () => {
|
|
1960
|
+
if (this.props.videoElement) {
|
|
1961
|
+
this.props.videoElement.muted = false;
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
this.setPlaybackRate = (rate) => {
|
|
1965
|
+
if (this.props.videoElement && rate > 0) {
|
|
1966
|
+
this.props.videoElement.playbackRate = rate;
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
this.setLoop = (loop) => {
|
|
1970
|
+
if (this.props.videoElement) {
|
|
1971
|
+
this.props.videoElement.loop = loop;
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
this.getDuration = () => {
|
|
1975
|
+
if (this.props.videoElement && isFinite(this.props.videoElement.duration)) {
|
|
1976
|
+
return this.props.videoElement.duration;
|
|
1977
|
+
}
|
|
1978
|
+
return null;
|
|
1979
|
+
};
|
|
1980
|
+
this.getCurrentTime = () => {
|
|
1981
|
+
if (this.props.videoElement && isFinite(this.props.videoElement.currentTime)) {
|
|
1982
|
+
return this.props.videoElement.currentTime;
|
|
1983
|
+
}
|
|
1984
|
+
return null;
|
|
1985
|
+
};
|
|
1986
|
+
this.getSecondsLoaded = () => {
|
|
1987
|
+
if (this.props.videoElement && this.props.videoElement.buffered.length > 0) {
|
|
1988
|
+
return this.props.videoElement.buffered.end(
|
|
1989
|
+
this.props.videoElement.buffered.length - 1
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
return null;
|
|
1993
|
+
};
|
|
1994
|
+
this.getInternalPlayer = (key = "player") => {
|
|
1995
|
+
if (key === "video") return this.props.videoElement;
|
|
1996
|
+
return null;
|
|
1997
|
+
};
|
|
1998
|
+
this.enablePIP = async () => {
|
|
1999
|
+
if (this.props.videoElement && "requestPictureInPicture" in this.props.videoElement) {
|
|
2000
|
+
try {
|
|
2001
|
+
await this.props.videoElement.requestPictureInPicture();
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
console.warn("Picture-in-Picture failed:", error);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
this.disablePIP = async () => {
|
|
2008
|
+
if (document.pictureInPictureElement) {
|
|
2009
|
+
try {
|
|
2010
|
+
await document.exitPictureInPicture();
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
console.warn("Exit Picture-in-Picture failed:", error);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
componentDidMount() {
|
|
2018
|
+
this.mounted = true;
|
|
2019
|
+
this.load();
|
|
2020
|
+
}
|
|
2021
|
+
componentWillUnmount() {
|
|
2022
|
+
this.mounted = false;
|
|
2023
|
+
}
|
|
2024
|
+
componentDidUpdate(prevProps) {
|
|
2025
|
+
if (prevProps.src !== this.props.src) {
|
|
2026
|
+
this.load();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
render() {
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
FilePlayer.displayName = "FilePlayer";
|
|
2034
|
+
FilePlayer.canPlay = canPlay.file;
|
|
2035
|
+
|
|
2036
|
+
// src/players/index.ts
|
|
2037
|
+
var players = [
|
|
2038
|
+
{
|
|
2039
|
+
key: "hls",
|
|
2040
|
+
name: "HLS Player",
|
|
2041
|
+
canPlay: canPlay.hls,
|
|
2042
|
+
lazyPlayer: lazy(() => Promise.resolve({ default: HlsPlayer }))
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
key: "file",
|
|
2046
|
+
name: "File Player",
|
|
2047
|
+
canPlay: canPlay.file,
|
|
2048
|
+
canEnablePIP: (url) => {
|
|
2049
|
+
return canPlay.file(url) && (document.pictureInPictureEnabled || typeof document.webkitSupportsPresentationMode === "function");
|
|
2050
|
+
},
|
|
2051
|
+
lazyPlayer: lazy(() => Promise.resolve({ default: FilePlayer }))
|
|
2052
|
+
}
|
|
2053
|
+
];
|
|
2054
|
+
var players_default = players;
|
|
2055
|
+
//# sourceMappingURL=index.cjs.map
|