stormcloud-video-player 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/stormcloud-vp.min.js +2 -2
- package/lib/index.cjs +523 -74
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +32 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +523 -74
- package/lib/index.js.map +1 -1
- package/lib/player/StormcloudVideoPlayer.cjs +105 -18
- package/lib/player/StormcloudVideoPlayer.cjs.map +1 -1
- package/lib/player/StormcloudVideoPlayer.d.cts +3 -0
- package/lib/players/HlsPlayer.cjs +105 -18
- package/lib/players/HlsPlayer.cjs.map +1 -1
- package/lib/players/index.cjs +105 -18
- package/lib/players/index.cjs.map +1 -1
- package/lib/sdk/ima.cjs +56 -16
- package/lib/sdk/ima.cjs.map +1 -1
- package/lib/sdk/ima.d.cts +3 -1
- package/lib/types-DOcCdwQI.d.cts +78 -0
- package/lib/ui/StormcloudVideoPlayer.cjs +171 -21
- package/lib/ui/StormcloudVideoPlayer.cjs.map +1 -1
- package/package.json +1 -1
package/lib/index.cjs
CHANGED
|
@@ -64,7 +64,7 @@ var import_react = __toESM(require("react"), 1);
|
|
|
64
64
|
var import_hls = __toESM(require("hls.js"), 1);
|
|
65
65
|
|
|
66
66
|
// src/sdk/ima.ts
|
|
67
|
-
function createImaController(video) {
|
|
67
|
+
function createImaController(video, options) {
|
|
68
68
|
let adPlaying = false;
|
|
69
69
|
let originalMutedState = false;
|
|
70
70
|
const listeners = /* @__PURE__ */ new Map();
|
|
@@ -127,6 +127,8 @@ function createImaController(video) {
|
|
|
127
127
|
let adsLoadedPromise;
|
|
128
128
|
let adsLoadedResolve;
|
|
129
129
|
let adsLoadedReject;
|
|
130
|
+
let currentAdDuration = 0;
|
|
131
|
+
let preloadedAds = [];
|
|
130
132
|
function makeAdsRequest(google, vastTagUrl) {
|
|
131
133
|
const adsRequest = new google.ima.AdsRequest();
|
|
132
134
|
adsRequest.adTagUrl = vastTagUrl;
|
|
@@ -233,6 +235,7 @@ function createImaController(video) {
|
|
|
233
235
|
} catch {
|
|
234
236
|
}
|
|
235
237
|
adPlaying = false;
|
|
238
|
+
currentAdDuration = 0;
|
|
236
239
|
video.muted = originalMutedState;
|
|
237
240
|
if (adContainerEl)
|
|
238
241
|
adContainerEl.style.pointerEvents = "none";
|
|
@@ -257,8 +260,10 @@ function createImaController(video) {
|
|
|
257
260
|
"[IMA] Max retries reached, emitting ad_error"
|
|
258
261
|
);
|
|
259
262
|
emit("ad_error");
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
264
|
+
video.play().catch(() => {
|
|
265
|
+
});
|
|
266
|
+
}
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
);
|
|
@@ -268,7 +273,14 @@ function createImaController(video) {
|
|
|
268
273
|
console.log("[IMA] Content pause requested");
|
|
269
274
|
originalMutedState = video.muted;
|
|
270
275
|
video.muted = true;
|
|
271
|
-
|
|
276
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
277
|
+
video.pause();
|
|
278
|
+
console.log("[IMA] Video paused (VOD mode)");
|
|
279
|
+
} else {
|
|
280
|
+
console.log(
|
|
281
|
+
"[IMA] Video continues playing but muted (Live mode)"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
272
284
|
adPlaying = true;
|
|
273
285
|
if (adContainerEl)
|
|
274
286
|
adContainerEl.style.pointerEvents = "auto";
|
|
@@ -283,18 +295,47 @@ function createImaController(video) {
|
|
|
283
295
|
video.muted = originalMutedState;
|
|
284
296
|
if (adContainerEl)
|
|
285
297
|
adContainerEl.style.pointerEvents = "none";
|
|
286
|
-
|
|
287
|
-
|
|
298
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
299
|
+
video.play().catch(() => {
|
|
300
|
+
});
|
|
301
|
+
console.log("[IMA] Video resumed (VOD mode)");
|
|
302
|
+
} else {
|
|
303
|
+
console.log(
|
|
304
|
+
"[IMA] Video unmuted (Live mode - was never paused)"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
288
307
|
emit("content_resume");
|
|
289
308
|
}
|
|
290
309
|
);
|
|
310
|
+
adsManager.addEventListener(AdEvent.STARTED, (adEvent) => {
|
|
311
|
+
console.log("[IMA] Ad started");
|
|
312
|
+
try {
|
|
313
|
+
const ad = adEvent.getAd();
|
|
314
|
+
if (ad && ad.getDuration) {
|
|
315
|
+
currentAdDuration = ad.getDuration();
|
|
316
|
+
console.log(`[IMA] Ad duration: ${currentAdDuration}s`);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn("[IMA] Could not get ad duration:", error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
291
322
|
adsManager.addEventListener(AdEvent.ALL_ADS_COMPLETED, () => {
|
|
292
323
|
console.log("[IMA] All ads completed");
|
|
293
324
|
adPlaying = false;
|
|
325
|
+
currentAdDuration = 0;
|
|
294
326
|
video.muted = originalMutedState;
|
|
295
327
|
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
296
|
-
|
|
297
|
-
|
|
328
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
329
|
+
video.play().catch(() => {
|
|
330
|
+
});
|
|
331
|
+
console.log(
|
|
332
|
+
"[IMA] Video resumed after all ads completed (VOD mode)"
|
|
333
|
+
);
|
|
334
|
+
} else {
|
|
335
|
+
console.log(
|
|
336
|
+
"[IMA] Video unmuted after all ads completed (Live mode)"
|
|
337
|
+
);
|
|
338
|
+
}
|
|
298
339
|
emit("all_ads_completed");
|
|
299
340
|
});
|
|
300
341
|
console.log("[IMA] Ads manager event listeners attached");
|
|
@@ -308,8 +349,10 @@ function createImaController(video) {
|
|
|
308
349
|
adPlaying = false;
|
|
309
350
|
video.muted = originalMutedState;
|
|
310
351
|
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
311
|
-
|
|
312
|
-
|
|
352
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
353
|
+
video.play().catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}
|
|
313
356
|
if (adsLoadedReject) {
|
|
314
357
|
adsLoadedReject(new Error("Failed to setup ads manager"));
|
|
315
358
|
adsLoadedReject = void 0;
|
|
@@ -363,8 +406,14 @@ function createImaController(video) {
|
|
|
363
406
|
const height = video.clientHeight || 360;
|
|
364
407
|
console.log(`[IMA] Initializing ads manager (${width}x${height})`);
|
|
365
408
|
adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
|
|
366
|
-
|
|
367
|
-
|
|
409
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
410
|
+
console.log("[IMA] Pausing video for ad playback (VOD mode)");
|
|
411
|
+
video.pause();
|
|
412
|
+
} else {
|
|
413
|
+
console.log(
|
|
414
|
+
"[IMA] Keeping video playing but muted for ad playback (Live mode)"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
368
417
|
adPlaying = true;
|
|
369
418
|
console.log("[IMA] Starting ad playback");
|
|
370
419
|
adsManager.start();
|
|
@@ -372,8 +421,10 @@ function createImaController(video) {
|
|
|
372
421
|
} catch (error) {
|
|
373
422
|
console.error("[IMA] Error starting ad playback:", error);
|
|
374
423
|
adPlaying = false;
|
|
375
|
-
|
|
376
|
-
|
|
424
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
425
|
+
video.play().catch(() => {
|
|
426
|
+
});
|
|
427
|
+
}
|
|
377
428
|
return Promise.reject(error);
|
|
378
429
|
}
|
|
379
430
|
},
|
|
@@ -384,8 +435,13 @@ function createImaController(video) {
|
|
|
384
435
|
adsManager?.stop?.();
|
|
385
436
|
} catch {
|
|
386
437
|
}
|
|
387
|
-
|
|
388
|
-
|
|
438
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
439
|
+
video.play().catch(() => {
|
|
440
|
+
});
|
|
441
|
+
console.log("[IMA] Video resumed after stop (VOD mode)");
|
|
442
|
+
} else {
|
|
443
|
+
console.log("[IMA] Video unmuted after stop (Live mode)");
|
|
444
|
+
}
|
|
389
445
|
},
|
|
390
446
|
destroy() {
|
|
391
447
|
try {
|
|
@@ -451,6 +507,84 @@ function createImaController(video) {
|
|
|
451
507
|
}
|
|
452
508
|
}
|
|
453
509
|
return 1;
|
|
510
|
+
},
|
|
511
|
+
getAdDuration() {
|
|
512
|
+
return currentAdDuration;
|
|
513
|
+
},
|
|
514
|
+
async preloadAds(vastTagUrls) {
|
|
515
|
+
console.log(`[IMA] Preloading ${vastTagUrls.length} ads`);
|
|
516
|
+
const adInfos = [];
|
|
517
|
+
for (const vastTagUrl of vastTagUrls) {
|
|
518
|
+
try {
|
|
519
|
+
await ensureImaLoaded();
|
|
520
|
+
const google = window.google;
|
|
521
|
+
const tempAdsLoader = new google.ima.AdsLoader(adDisplayContainer);
|
|
522
|
+
const adInfo = await new Promise((resolve, reject) => {
|
|
523
|
+
const timeout = setTimeout(() => {
|
|
524
|
+
reject(new Error("Preload timeout"));
|
|
525
|
+
}, 5e3);
|
|
526
|
+
tempAdsLoader.addEventListener(
|
|
527
|
+
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
|
|
528
|
+
(evt) => {
|
|
529
|
+
clearTimeout(timeout);
|
|
530
|
+
try {
|
|
531
|
+
const tempAdsManager = evt.getAdsManager(video);
|
|
532
|
+
let duration = 30;
|
|
533
|
+
try {
|
|
534
|
+
const ads = tempAdsManager.getCuePoints?.() || [];
|
|
535
|
+
if (ads.length > 0) {
|
|
536
|
+
duration = 15;
|
|
537
|
+
}
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
tempAdsManager.destroy();
|
|
541
|
+
resolve({
|
|
542
|
+
duration,
|
|
543
|
+
vastTagUrl,
|
|
544
|
+
isPreloaded: true
|
|
545
|
+
});
|
|
546
|
+
} catch (error) {
|
|
547
|
+
clearTimeout(timeout);
|
|
548
|
+
reject(error);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
tempAdsLoader.addEventListener(
|
|
553
|
+
google.ima.AdErrorEvent.Type.AD_ERROR,
|
|
554
|
+
(errorEvent) => {
|
|
555
|
+
clearTimeout(timeout);
|
|
556
|
+
console.warn(
|
|
557
|
+
`[IMA] Preload error for ${vastTagUrl}:`,
|
|
558
|
+
errorEvent.getError()
|
|
559
|
+
);
|
|
560
|
+
resolve({
|
|
561
|
+
duration: 30,
|
|
562
|
+
vastTagUrl,
|
|
563
|
+
isPreloaded: false
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
const adsRequest = new google.ima.AdsRequest();
|
|
568
|
+
adsRequest.adTagUrl = vastTagUrl;
|
|
569
|
+
tempAdsLoader.requestAds(adsRequest);
|
|
570
|
+
});
|
|
571
|
+
adInfos.push(adInfo);
|
|
572
|
+
tempAdsLoader.destroy();
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.warn(`[IMA] Failed to preload ad ${vastTagUrl}:`, error);
|
|
575
|
+
adInfos.push({
|
|
576
|
+
duration: 30,
|
|
577
|
+
vastTagUrl,
|
|
578
|
+
isPreloaded: false
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
preloadedAds = adInfos;
|
|
583
|
+
console.log(
|
|
584
|
+
`[IMA] Preloaded ${adInfos.length} ads with total duration:`,
|
|
585
|
+
adInfos.reduce((sum, ad) => sum + ad.duration, 0)
|
|
586
|
+
);
|
|
587
|
+
return adInfos;
|
|
454
588
|
}
|
|
455
589
|
};
|
|
456
590
|
}
|
|
@@ -682,9 +816,14 @@ var StormcloudVideoPlayer = class {
|
|
|
682
816
|
this.currentAdIndex = 0;
|
|
683
817
|
this.totalAdsInBreak = 0;
|
|
684
818
|
this.showAds = false;
|
|
819
|
+
this.isLiveStream = false;
|
|
820
|
+
this.preloadedAdInfo = [];
|
|
821
|
+
this.cumulativeAdDurationMs = 0;
|
|
685
822
|
this.config = config;
|
|
686
823
|
this.video = config.videoElement;
|
|
687
|
-
this.ima = createImaController(this.video
|
|
824
|
+
this.ima = createImaController(this.video, {
|
|
825
|
+
continueLiveStreamDuringAds: false
|
|
826
|
+
});
|
|
688
827
|
}
|
|
689
828
|
async load() {
|
|
690
829
|
if (!this.attached) {
|
|
@@ -703,6 +842,22 @@ var StormcloudVideoPlayer = class {
|
|
|
703
842
|
this.initializeTracking();
|
|
704
843
|
if (this.shouldUseNativeHls()) {
|
|
705
844
|
this.video.src = this.config.src;
|
|
845
|
+
this.isLiveStream = this.config.lowLatencyMode ?? false;
|
|
846
|
+
if (this.config.debugAdTiming) {
|
|
847
|
+
console.log(
|
|
848
|
+
"[StormcloudVideoPlayer] allowNativeHls: true - VOD mode detected:",
|
|
849
|
+
{
|
|
850
|
+
isLive: this.isLiveStream,
|
|
851
|
+
allowNativeHls: this.config.allowNativeHls,
|
|
852
|
+
adBehavior: "vod (main video pauses during ads)"
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
this.ima.destroy();
|
|
857
|
+
this.ima = createImaController(this.video, {
|
|
858
|
+
continueLiveStreamDuringAds: false
|
|
859
|
+
});
|
|
860
|
+
this.ima.initialize();
|
|
706
861
|
if (this.config.autoplay) {
|
|
707
862
|
await this.video.play().catch(() => {
|
|
708
863
|
});
|
|
@@ -720,7 +875,23 @@ var StormcloudVideoPlayer = class {
|
|
|
720
875
|
this.hls.on(import_hls.default.Events.MEDIA_ATTACHED, () => {
|
|
721
876
|
this.hls?.loadSource(this.config.src);
|
|
722
877
|
});
|
|
723
|
-
this.hls.on(import_hls.default.Events.MANIFEST_PARSED, async () => {
|
|
878
|
+
this.hls.on(import_hls.default.Events.MANIFEST_PARSED, async (_, data) => {
|
|
879
|
+
this.isLiveStream = this.hls?.levels?.some(
|
|
880
|
+
(level) => level?.details?.live === true || level?.details?.type === "LIVE"
|
|
881
|
+
) ?? false;
|
|
882
|
+
if (this.config.debugAdTiming) {
|
|
883
|
+
const adBehavior = this.shouldContinueLiveStreamDuringAds() ? "live (main video continues muted during ads)" : "vod (main video pauses during ads)";
|
|
884
|
+
console.log("[StormcloudVideoPlayer] Stream type detected:", {
|
|
885
|
+
isLive: this.isLiveStream,
|
|
886
|
+
allowNativeHls: this.config.allowNativeHls,
|
|
887
|
+
adBehavior
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
this.ima.destroy();
|
|
891
|
+
this.ima = createImaController(this.video, {
|
|
892
|
+
continueLiveStreamDuringAds: this.shouldContinueLiveStreamDuringAds()
|
|
893
|
+
});
|
|
894
|
+
this.ima.initialize();
|
|
724
895
|
if (this.config.autoplay) {
|
|
725
896
|
await this.video.play().catch(() => {
|
|
726
897
|
});
|
|
@@ -819,16 +990,28 @@ var StormcloudVideoPlayer = class {
|
|
|
819
990
|
this.ima.initialize();
|
|
820
991
|
this.ima.on("all_ads_completed", () => {
|
|
821
992
|
if (!this.inAdBreak) return;
|
|
993
|
+
const actualAdDuration = this.ima.getAdDuration();
|
|
994
|
+
if (actualAdDuration > 0) {
|
|
995
|
+
this.cumulativeAdDurationMs += actualAdDuration * 1e3;
|
|
996
|
+
if (this.config.debugAdTiming) {
|
|
997
|
+
console.log(
|
|
998
|
+
`[StormcloudVideoPlayer] Ad completed. Duration: ${actualAdDuration}s, Cumulative: ${this.cumulativeAdDurationMs}ms`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
822
1002
|
const remaining = this.getRemainingAdMs();
|
|
823
|
-
|
|
1003
|
+
const shouldContinue = this.shouldContinueAdBreak(remaining);
|
|
1004
|
+
if (shouldContinue && this.adPodQueue.length > 0) {
|
|
824
1005
|
const next = this.adPodQueue.shift();
|
|
825
1006
|
this.currentAdIndex++;
|
|
826
1007
|
this.playSingleAd(next).catch(() => {
|
|
827
1008
|
});
|
|
1009
|
+
} else if (shouldContinue && this.canRequestMoreAds()) {
|
|
1010
|
+
this.requestAdditionalAds().catch(() => {
|
|
1011
|
+
this.endAdBreak();
|
|
1012
|
+
});
|
|
828
1013
|
} else {
|
|
829
|
-
this.
|
|
830
|
-
this.totalAdsInBreak = 0;
|
|
831
|
-
this.showAds = false;
|
|
1014
|
+
this.endAdBreak();
|
|
832
1015
|
}
|
|
833
1016
|
});
|
|
834
1017
|
this.ima.on("ad_error", () => {
|
|
@@ -837,11 +1020,16 @@ var StormcloudVideoPlayer = class {
|
|
|
837
1020
|
}
|
|
838
1021
|
if (!this.inAdBreak) return;
|
|
839
1022
|
const remaining = this.getRemainingAdMs();
|
|
840
|
-
|
|
1023
|
+
const shouldContinue = this.shouldContinueAdBreak(remaining);
|
|
1024
|
+
if (shouldContinue && this.adPodQueue.length > 0) {
|
|
841
1025
|
const next = this.adPodQueue.shift();
|
|
842
1026
|
this.currentAdIndex++;
|
|
843
1027
|
this.playSingleAd(next).catch(() => {
|
|
844
1028
|
});
|
|
1029
|
+
} else if (shouldContinue && this.canRequestMoreAds()) {
|
|
1030
|
+
this.requestAdditionalAds().catch(() => {
|
|
1031
|
+
this.handleAdFailure();
|
|
1032
|
+
});
|
|
845
1033
|
} else {
|
|
846
1034
|
this.handleAdFailure();
|
|
847
1035
|
}
|
|
@@ -1332,6 +1520,25 @@ var StormcloudVideoPlayer = class {
|
|
|
1332
1520
|
isShowingAds() {
|
|
1333
1521
|
return this.showAds;
|
|
1334
1522
|
}
|
|
1523
|
+
getAdBreakStats() {
|
|
1524
|
+
const remainingDurationMs = this.currentAdBreakTargetDurationMs != null ? Math.max(
|
|
1525
|
+
0,
|
|
1526
|
+
this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs
|
|
1527
|
+
) : void 0;
|
|
1528
|
+
const estimatedFillRate = this.currentAdBreakTargetDurationMs != null && this.currentAdBreakTargetDurationMs > 0 ? this.cumulativeAdDurationMs / this.currentAdBreakTargetDurationMs * 100 : void 0;
|
|
1529
|
+
return {
|
|
1530
|
+
isInAdBreak: this.inAdBreak,
|
|
1531
|
+
currentAdIndex: this.currentAdIndex,
|
|
1532
|
+
totalAdsInBreak: this.totalAdsInBreak,
|
|
1533
|
+
targetDurationMs: this.currentAdBreakTargetDurationMs,
|
|
1534
|
+
cumulativeDurationMs: this.cumulativeAdDurationMs,
|
|
1535
|
+
estimatedFillRate,
|
|
1536
|
+
remainingDurationMs
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
getPreloadedAdInfo() {
|
|
1540
|
+
return [...this.preloadedAdInfo];
|
|
1541
|
+
}
|
|
1335
1542
|
getStreamType() {
|
|
1336
1543
|
const url = this.config.src.toLowerCase();
|
|
1337
1544
|
if (url.includes(".m3u8") || url.includes("/hls/") || url.includes("application/vnd.apple.mpegurl")) {
|
|
@@ -1346,6 +1553,15 @@ var StormcloudVideoPlayer = class {
|
|
|
1346
1553
|
}
|
|
1347
1554
|
return !!(this.config.allowNativeHls && !(this.config.showCustomControls ?? false));
|
|
1348
1555
|
}
|
|
1556
|
+
shouldContinueLiveStreamDuringAds() {
|
|
1557
|
+
if (this.config.allowNativeHls) {
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
if (!this.isLiveStream) {
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1349
1565
|
async loadDefaultVastFromAdstorm(adstormApiUrl, params) {
|
|
1350
1566
|
const usp = new URLSearchParams(params || {});
|
|
1351
1567
|
const url = `${adstormApiUrl}?${usp.toString()}`;
|
|
@@ -1357,59 +1573,59 @@ var StormcloudVideoPlayer = class {
|
|
|
1357
1573
|
this.apiVastTagUrl = tag;
|
|
1358
1574
|
}
|
|
1359
1575
|
}
|
|
1360
|
-
async handleAdStart(
|
|
1576
|
+
async handleAdStart(marker) {
|
|
1361
1577
|
const scheduled = this.findCurrentOrNextBreak(
|
|
1362
1578
|
this.video.currentTime * 1e3
|
|
1363
1579
|
);
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1580
|
+
let targetDurationMs = this.expectedAdBreakDurationMs;
|
|
1581
|
+
if (!targetDurationMs && scheduled?.durationMs != null) {
|
|
1582
|
+
targetDurationMs = scheduled.durationMs;
|
|
1583
|
+
}
|
|
1584
|
+
if (!targetDurationMs && marker.durationSeconds != null) {
|
|
1585
|
+
targetDurationMs = marker.durationSeconds * 1e3;
|
|
1586
|
+
}
|
|
1587
|
+
this.currentAdBreakTargetDurationMs = targetDurationMs;
|
|
1588
|
+
this.cumulativeAdDurationMs = 0;
|
|
1589
|
+
if (this.config.debugAdTiming) {
|
|
1590
|
+
console.log(
|
|
1591
|
+
"[StormcloudVideoPlayer] Starting ad break with target duration:",
|
|
1592
|
+
{
|
|
1593
|
+
targetDurationMs,
|
|
1594
|
+
scte35Duration: marker.durationSeconds,
|
|
1595
|
+
scheduledDuration: scheduled?.durationMs
|
|
1375
1596
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (this.config.debugAdTiming) {
|
|
1381
|
-
console.log(
|
|
1382
|
-
`[StormcloudVideoPlayer] Using API VAST tag with ${adsNumber} ads:`,
|
|
1383
|
-
vastTagUrl
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
} else if (tags && tags.length > 0) {
|
|
1387
|
-
vastTagUrl = tags[0];
|
|
1388
|
-
const rest = tags.slice(1);
|
|
1389
|
-
this.adPodQueue = rest;
|
|
1390
|
-
this.currentAdIndex = 0;
|
|
1391
|
-
this.totalAdsInBreak = tags.length;
|
|
1392
|
-
if (this.config.debugAdTiming) {
|
|
1393
|
-
console.log(
|
|
1394
|
-
"[StormcloudVideoPlayer] Using scheduled VAST tag:",
|
|
1395
|
-
vastTagUrl
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
} else {
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
const adQueue = await this.buildAdQueueForDuration(targetDurationMs);
|
|
1600
|
+
if (adQueue.length === 0) {
|
|
1399
1601
|
if (this.config.debugAdTiming) {
|
|
1400
|
-
console.log("[StormcloudVideoPlayer] No
|
|
1602
|
+
console.log("[StormcloudVideoPlayer] No ads available for ad break");
|
|
1401
1603
|
}
|
|
1402
1604
|
return;
|
|
1403
1605
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1606
|
+
this.adPodQueue = adQueue.slice(1);
|
|
1607
|
+
this.preloadedAdInfo = await this.getAdInfoForQueue(adQueue);
|
|
1608
|
+
this.currentAdIndex = 0;
|
|
1609
|
+
this.totalAdsInBreak = adQueue.length;
|
|
1610
|
+
this.showAds = true;
|
|
1611
|
+
if (this.config.debugAdTiming) {
|
|
1612
|
+
const totalEstimatedDuration = this.preloadedAdInfo.reduce(
|
|
1613
|
+
(sum, ad) => sum + ad.duration,
|
|
1614
|
+
0
|
|
1615
|
+
);
|
|
1616
|
+
console.log("[StormcloudVideoPlayer] Ad queue built:", {
|
|
1617
|
+
totalAds: adQueue.length,
|
|
1618
|
+
estimatedTotalDuration: totalEstimatedDuration,
|
|
1619
|
+
targetDuration: targetDurationMs ? targetDurationMs / 1e3 : "unknown",
|
|
1620
|
+
fillRate: targetDurationMs ? totalEstimatedDuration * 1e3 / targetDurationMs : "unknown"
|
|
1621
|
+
});
|
|
1408
1622
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1623
|
+
this.currentAdIndex++;
|
|
1624
|
+
await this.playSingleAd(adQueue[0]);
|
|
1625
|
+
if (targetDurationMs != null) {
|
|
1626
|
+
this.expectedAdBreakDurationMs = targetDurationMs;
|
|
1411
1627
|
this.currentAdBreakStartWallClockMs = this.currentAdBreakStartWallClockMs ?? Date.now();
|
|
1412
|
-
this.scheduleAdStopCountdown(
|
|
1628
|
+
this.scheduleAdStopCountdown(targetDurationMs);
|
|
1413
1629
|
}
|
|
1414
1630
|
}
|
|
1415
1631
|
findCurrentOrNextBreak(nowMs) {
|
|
@@ -1526,25 +1742,89 @@ var StormcloudVideoPlayer = class {
|
|
|
1526
1742
|
"[StormcloudVideoPlayer] Handling ad failure - resuming content"
|
|
1527
1743
|
);
|
|
1528
1744
|
}
|
|
1745
|
+
this.endAdBreak();
|
|
1746
|
+
if (this.video.paused) {
|
|
1747
|
+
this.video.play().catch(() => {
|
|
1748
|
+
if (this.config.debugAdTiming) {
|
|
1749
|
+
console.error(
|
|
1750
|
+
"[StormcloudVideoPlayer] Failed to resume video after ad failure"
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
endAdBreak() {
|
|
1757
|
+
if (this.config.debugAdTiming) {
|
|
1758
|
+
const targetDuration = this.currentAdBreakTargetDurationMs ? this.currentAdBreakTargetDurationMs / 1e3 : "unknown";
|
|
1759
|
+
const actualDuration = this.cumulativeAdDurationMs / 1e3;
|
|
1760
|
+
const fillRate = this.currentAdBreakTargetDurationMs ? (this.cumulativeAdDurationMs / this.currentAdBreakTargetDurationMs * 100).toFixed(1) : "unknown";
|
|
1761
|
+
console.log("[StormcloudVideoPlayer] Ad break ended:", {
|
|
1762
|
+
targetDurationSeconds: targetDuration,
|
|
1763
|
+
actualDurationSeconds: actualDuration,
|
|
1764
|
+
fillRate: `${fillRate}%`,
|
|
1765
|
+
totalAdsPlayed: this.currentAdIndex
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1529
1768
|
this.inAdBreak = false;
|
|
1530
1769
|
this.expectedAdBreakDurationMs = void 0;
|
|
1531
1770
|
this.currentAdBreakStartWallClockMs = void 0;
|
|
1771
|
+
this.currentAdBreakTargetDurationMs = void 0;
|
|
1772
|
+
this.cumulativeAdDurationMs = 0;
|
|
1532
1773
|
this.clearAdStartTimer();
|
|
1533
1774
|
this.clearAdStopTimer();
|
|
1534
1775
|
this.clearAdFailsafeTimer();
|
|
1535
1776
|
this.adPodQueue = [];
|
|
1777
|
+
this.preloadedAdInfo = [];
|
|
1536
1778
|
this.showAds = false;
|
|
1537
1779
|
this.currentAdIndex = 0;
|
|
1538
1780
|
this.totalAdsInBreak = 0;
|
|
1539
|
-
|
|
1540
|
-
|
|
1781
|
+
}
|
|
1782
|
+
shouldContinueAdBreak(remainingMs) {
|
|
1783
|
+
if (remainingMs > 500) {
|
|
1784
|
+
return true;
|
|
1785
|
+
}
|
|
1786
|
+
if (this.currentAdBreakTargetDurationMs) {
|
|
1787
|
+
const targetRemainingMs = this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs;
|
|
1788
|
+
const minAdDuration = this.config.minAdDurationMs ?? 5e3;
|
|
1789
|
+
if (targetRemainingMs > minAdDuration) {
|
|
1541
1790
|
if (this.config.debugAdTiming) {
|
|
1542
|
-
console.
|
|
1543
|
-
|
|
1791
|
+
console.log(
|
|
1792
|
+
`[StormcloudVideoPlayer] Target duration not filled, continuing. Remaining: ${targetRemainingMs}ms`
|
|
1544
1793
|
);
|
|
1545
1794
|
}
|
|
1546
|
-
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
canRequestMoreAds() {
|
|
1801
|
+
const maxAdsPerBreak = this.config.maxAdsPerBreak ?? 10;
|
|
1802
|
+
if (this.currentAdIndex >= maxAdsPerBreak) {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
return !!this.apiVastTagUrl;
|
|
1806
|
+
}
|
|
1807
|
+
async requestAdditionalAds() {
|
|
1808
|
+
if (!this.currentAdBreakTargetDurationMs || !this.apiVastTagUrl) {
|
|
1809
|
+
throw new Error(
|
|
1810
|
+
"Cannot request additional ads without target duration and VAST URL"
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
const remainingDurationMs = this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs;
|
|
1814
|
+
const estimatedAdDurationMs = this.getEstimatedAdDuration() * 1e3;
|
|
1815
|
+
if (remainingDurationMs < estimatedAdDurationMs * 0.5) {
|
|
1816
|
+
throw new Error("Not enough time remaining for additional ads");
|
|
1817
|
+
}
|
|
1818
|
+
if (this.config.debugAdTiming) {
|
|
1819
|
+
console.log(
|
|
1820
|
+
`[StormcloudVideoPlayer] Requesting additional ads for remaining ${remainingDurationMs}ms`
|
|
1821
|
+
);
|
|
1547
1822
|
}
|
|
1823
|
+
this.adPodQueue.push(this.apiVastTagUrl);
|
|
1824
|
+
this.totalAdsInBreak++;
|
|
1825
|
+
const next = this.adPodQueue.shift();
|
|
1826
|
+
this.currentAdIndex++;
|
|
1827
|
+
await this.playSingleAd(next);
|
|
1548
1828
|
}
|
|
1549
1829
|
startAdFailsafeTimer() {
|
|
1550
1830
|
this.clearAdFailsafeTimer();
|
|
@@ -1578,6 +1858,109 @@ var StormcloudVideoPlayer = class {
|
|
|
1578
1858
|
}
|
|
1579
1859
|
return [b.vastTagUrl];
|
|
1580
1860
|
}
|
|
1861
|
+
async buildAdQueueForDuration(targetDurationMs) {
|
|
1862
|
+
const adQueue = [];
|
|
1863
|
+
let baseVastTagUrl = this.apiVastTagUrl;
|
|
1864
|
+
if (!baseVastTagUrl) {
|
|
1865
|
+
const scheduled = this.findCurrentOrNextBreak(
|
|
1866
|
+
this.video.currentTime * 1e3
|
|
1867
|
+
);
|
|
1868
|
+
const scheduledTags = this.selectVastTagsForBreak(scheduled);
|
|
1869
|
+
if (scheduledTags && scheduledTags.length > 0) {
|
|
1870
|
+
baseVastTagUrl = scheduledTags[0];
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (!baseVastTagUrl) {
|
|
1874
|
+
return adQueue;
|
|
1875
|
+
}
|
|
1876
|
+
if (!targetDurationMs) {
|
|
1877
|
+
let adsNumber = 1;
|
|
1878
|
+
if (this.vastConfig) {
|
|
1879
|
+
const isHls = this.config.src.includes(".m3u8") || this.config.src.includes("hls");
|
|
1880
|
+
if (isHls && this.vastConfig.cue_tones?.number_ads) {
|
|
1881
|
+
adsNumber = this.vastConfig.cue_tones.number_ads;
|
|
1882
|
+
} else if (!isHls && this.vastConfig.timer_vod?.number_ads) {
|
|
1883
|
+
adsNumber = this.vastConfig.timer_vod.number_ads;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return new Array(adsNumber).fill(baseVastTagUrl);
|
|
1887
|
+
}
|
|
1888
|
+
const targetDurationSeconds = targetDurationMs / 1e3;
|
|
1889
|
+
let cumulativeDurationSeconds = 0;
|
|
1890
|
+
const maxAdsToTry = 10;
|
|
1891
|
+
let adsAdded = 0;
|
|
1892
|
+
if (this.config.debugAdTiming) {
|
|
1893
|
+
console.log(
|
|
1894
|
+
`[StormcloudVideoPlayer] Attempting to fill ${targetDurationSeconds}s with ads`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
while (cumulativeDurationSeconds < targetDurationSeconds && adsAdded < maxAdsToTry) {
|
|
1898
|
+
adQueue.push(baseVastTagUrl);
|
|
1899
|
+
adsAdded++;
|
|
1900
|
+
const estimatedAdDuration = this.getEstimatedAdDuration();
|
|
1901
|
+
cumulativeDurationSeconds += estimatedAdDuration;
|
|
1902
|
+
if (this.config.debugAdTiming) {
|
|
1903
|
+
console.log(
|
|
1904
|
+
`[StormcloudVideoPlayer] Added ad ${adsAdded}, cumulative duration: ${cumulativeDurationSeconds}s`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const remainingDuration = targetDurationSeconds - cumulativeDurationSeconds;
|
|
1908
|
+
const toleranceSeconds = (this.config.adBreakGapToleranceMs ?? 2e3) / 1e3;
|
|
1909
|
+
if (remainingDuration < estimatedAdDuration && remainingDuration >= -toleranceSeconds) {
|
|
1910
|
+
if (this.config.debugAdTiming) {
|
|
1911
|
+
console.log(
|
|
1912
|
+
`[StormcloudVideoPlayer] Within tolerance, adding final ad. Overage: ${-remainingDuration}s`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
if (remainingDuration < estimatedAdDuration && remainingDuration < -toleranceSeconds) {
|
|
1918
|
+
if (this.config.debugAdTiming) {
|
|
1919
|
+
console.log(
|
|
1920
|
+
`[StormcloudVideoPlayer] Would exceed duration by too much, stopping at ${adsAdded} ads`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return adQueue;
|
|
1927
|
+
}
|
|
1928
|
+
getEstimatedAdDuration() {
|
|
1929
|
+
if (this.vastConfig) {
|
|
1930
|
+
return 15;
|
|
1931
|
+
}
|
|
1932
|
+
return 30;
|
|
1933
|
+
}
|
|
1934
|
+
async getAdInfoForQueue(adQueue) {
|
|
1935
|
+
if (this.config.enableAdPreloading === false) {
|
|
1936
|
+
if (this.config.debugAdTiming) {
|
|
1937
|
+
console.log(
|
|
1938
|
+
"[StormcloudVideoPlayer] Ad preloading disabled, using estimates"
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return adQueue.map((vastTagUrl) => ({
|
|
1942
|
+
duration: this.getEstimatedAdDuration(),
|
|
1943
|
+
vastTagUrl,
|
|
1944
|
+
isPreloaded: false
|
|
1945
|
+
}));
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
const adInfos = await this.ima.preloadAds(adQueue);
|
|
1949
|
+
return adInfos;
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
if (this.config.debugAdTiming) {
|
|
1952
|
+
console.warn(
|
|
1953
|
+
"[StormcloudVideoPlayer] Failed to preload ads, using estimates:",
|
|
1954
|
+
error
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
return adQueue.map((vastTagUrl) => ({
|
|
1958
|
+
duration: this.getEstimatedAdDuration(),
|
|
1959
|
+
vastTagUrl,
|
|
1960
|
+
isPreloaded: false
|
|
1961
|
+
}));
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1581
1964
|
getRemainingAdMs() {
|
|
1582
1965
|
if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
|
|
1583
1966
|
return 0;
|
|
@@ -1660,6 +2043,9 @@ var StormcloudVideoPlayer = class {
|
|
|
1660
2043
|
isFullscreen() {
|
|
1661
2044
|
return !!document.fullscreenElement;
|
|
1662
2045
|
}
|
|
2046
|
+
isLive() {
|
|
2047
|
+
return this.isLiveStream;
|
|
2048
|
+
}
|
|
1663
2049
|
get videoElement() {
|
|
1664
2050
|
return this.video;
|
|
1665
2051
|
}
|
|
@@ -1745,6 +2131,7 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
|
|
|
1745
2131
|
const [isLoading, setIsLoading] = import_react.default.useState(true);
|
|
1746
2132
|
const [isBuffering, setIsBuffering] = import_react.default.useState(false);
|
|
1747
2133
|
const [showCenterPlay, setShowCenterPlay] = import_react.default.useState(false);
|
|
2134
|
+
const [showLicenseWarning, setShowLicenseWarning] = import_react.default.useState(false);
|
|
1748
2135
|
const formatTime = (seconds) => {
|
|
1749
2136
|
if (!isFinite(seconds)) return "0:00:00";
|
|
1750
2137
|
const hours = Math.floor(seconds / 3600);
|
|
@@ -1802,6 +2189,15 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
|
|
|
1802
2189
|
if (typeof window === "undefined") return;
|
|
1803
2190
|
const el = videoRef.current;
|
|
1804
2191
|
if (!el || !src) return;
|
|
2192
|
+
if (!licenseKey) {
|
|
2193
|
+
setShowLicenseWarning(true);
|
|
2194
|
+
setIsLoading(false);
|
|
2195
|
+
console.warn(
|
|
2196
|
+
"StormcloudVideoPlayer: License key is required but not provided. Please set the licenseKey prop to use the player."
|
|
2197
|
+
);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
setShowLicenseWarning(false);
|
|
1805
2201
|
if (playerRef.current) {
|
|
1806
2202
|
try {
|
|
1807
2203
|
playerRef.current.destroy();
|
|
@@ -2125,7 +2521,60 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
|
|
|
2125
2521
|
)
|
|
2126
2522
|
}
|
|
2127
2523
|
),
|
|
2128
|
-
|
|
2524
|
+
showLicenseWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2525
|
+
"div",
|
|
2526
|
+
{
|
|
2527
|
+
style: {
|
|
2528
|
+
position: "absolute",
|
|
2529
|
+
top: "50%",
|
|
2530
|
+
left: "50%",
|
|
2531
|
+
transform: "translate(-50%, -50%)",
|
|
2532
|
+
zIndex: 25,
|
|
2533
|
+
background: "linear-gradient(135deg, rgba(220, 38, 38, 0.95) 0%, rgba(185, 28, 28, 0.9) 100%)",
|
|
2534
|
+
color: "white",
|
|
2535
|
+
padding: "24px 32px",
|
|
2536
|
+
borderRadius: "16px",
|
|
2537
|
+
backdropFilter: "blur(20px)",
|
|
2538
|
+
border: "2px solid rgba(255, 255, 255, 0.2)",
|
|
2539
|
+
boxShadow: "0 20px 60px rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.2)",
|
|
2540
|
+
textAlign: "center",
|
|
2541
|
+
maxWidth: "400px",
|
|
2542
|
+
margin: "0 16px"
|
|
2543
|
+
},
|
|
2544
|
+
children: [
|
|
2545
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2546
|
+
"div",
|
|
2547
|
+
{
|
|
2548
|
+
style: {
|
|
2549
|
+
fontSize: "20px",
|
|
2550
|
+
fontWeight: "bold",
|
|
2551
|
+
marginBottom: "12px",
|
|
2552
|
+
color: "#ffffff",
|
|
2553
|
+
textShadow: "0 2px 4px rgba(0, 0, 0, 0.5)"
|
|
2554
|
+
},
|
|
2555
|
+
children: "License Key Required"
|
|
2556
|
+
}
|
|
2557
|
+
),
|
|
2558
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2559
|
+
"div",
|
|
2560
|
+
{
|
|
2561
|
+
style: {
|
|
2562
|
+
fontSize: "14px",
|
|
2563
|
+
lineHeight: "1.5",
|
|
2564
|
+
color: "rgba(255, 255, 255, 0.9)",
|
|
2565
|
+
textShadow: "0 1px 2px rgba(0, 0, 0, 0.3)"
|
|
2566
|
+
},
|
|
2567
|
+
children: [
|
|
2568
|
+
"Please provide a valid license key to use the Stormcloud Video Player.",
|
|
2569
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}),
|
|
2570
|
+
"Contact your administrator for licensing information."
|
|
2571
|
+
]
|
|
2572
|
+
}
|
|
2573
|
+
)
|
|
2574
|
+
]
|
|
2575
|
+
}
|
|
2576
|
+
),
|
|
2577
|
+
showCenterPlay && !isLoading && !isBuffering && !showLicenseWarning && !adStatus.showAds && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2129
2578
|
"div",
|
|
2130
2579
|
{
|
|
2131
2580
|
onClick: handleCenterPlayClick,
|
|
@@ -2176,7 +2625,7 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
|
|
|
2176
2625
|
)
|
|
2177
2626
|
}
|
|
2178
2627
|
),
|
|
2179
|
-
shouldShowEnhancedControls ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2628
|
+
shouldShowEnhancedControls && !showLicenseWarning ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2180
2629
|
"div",
|
|
2181
2630
|
{
|
|
2182
2631
|
style: {
|
|
@@ -2641,7 +3090,7 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
|
|
|
2641
3090
|
)
|
|
2642
3091
|
]
|
|
2643
3092
|
}
|
|
2644
|
-
) }) : showCustomControls && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3093
|
+
) }) : showCustomControls && !showLicenseWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2645
3094
|
"div",
|
|
2646
3095
|
{
|
|
2647
3096
|
style: {
|