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.js
CHANGED
|
@@ -5,7 +5,7 @@ import React, { useEffect, useRef, useMemo } from "react";
|
|
|
5
5
|
import Hls from "hls.js";
|
|
6
6
|
|
|
7
7
|
// src/sdk/ima.ts
|
|
8
|
-
function createImaController(video) {
|
|
8
|
+
function createImaController(video, options) {
|
|
9
9
|
let adPlaying = false;
|
|
10
10
|
let originalMutedState = false;
|
|
11
11
|
const listeners = /* @__PURE__ */ new Map();
|
|
@@ -68,6 +68,8 @@ function createImaController(video) {
|
|
|
68
68
|
let adsLoadedPromise;
|
|
69
69
|
let adsLoadedResolve;
|
|
70
70
|
let adsLoadedReject;
|
|
71
|
+
let currentAdDuration = 0;
|
|
72
|
+
let preloadedAds = [];
|
|
71
73
|
function makeAdsRequest(google, vastTagUrl) {
|
|
72
74
|
const adsRequest = new google.ima.AdsRequest();
|
|
73
75
|
adsRequest.adTagUrl = vastTagUrl;
|
|
@@ -174,6 +176,7 @@ function createImaController(video) {
|
|
|
174
176
|
} catch {
|
|
175
177
|
}
|
|
176
178
|
adPlaying = false;
|
|
179
|
+
currentAdDuration = 0;
|
|
177
180
|
video.muted = originalMutedState;
|
|
178
181
|
if (adContainerEl)
|
|
179
182
|
adContainerEl.style.pointerEvents = "none";
|
|
@@ -198,8 +201,10 @@ function createImaController(video) {
|
|
|
198
201
|
"[IMA] Max retries reached, emitting ad_error"
|
|
199
202
|
);
|
|
200
203
|
emit("ad_error");
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
205
|
+
video.play().catch(() => {
|
|
206
|
+
});
|
|
207
|
+
}
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
);
|
|
@@ -209,7 +214,14 @@ function createImaController(video) {
|
|
|
209
214
|
console.log("[IMA] Content pause requested");
|
|
210
215
|
originalMutedState = video.muted;
|
|
211
216
|
video.muted = true;
|
|
212
|
-
|
|
217
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
218
|
+
video.pause();
|
|
219
|
+
console.log("[IMA] Video paused (VOD mode)");
|
|
220
|
+
} else {
|
|
221
|
+
console.log(
|
|
222
|
+
"[IMA] Video continues playing but muted (Live mode)"
|
|
223
|
+
);
|
|
224
|
+
}
|
|
213
225
|
adPlaying = true;
|
|
214
226
|
if (adContainerEl)
|
|
215
227
|
adContainerEl.style.pointerEvents = "auto";
|
|
@@ -224,18 +236,47 @@ function createImaController(video) {
|
|
|
224
236
|
video.muted = originalMutedState;
|
|
225
237
|
if (adContainerEl)
|
|
226
238
|
adContainerEl.style.pointerEvents = "none";
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
240
|
+
video.play().catch(() => {
|
|
241
|
+
});
|
|
242
|
+
console.log("[IMA] Video resumed (VOD mode)");
|
|
243
|
+
} else {
|
|
244
|
+
console.log(
|
|
245
|
+
"[IMA] Video unmuted (Live mode - was never paused)"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
229
248
|
emit("content_resume");
|
|
230
249
|
}
|
|
231
250
|
);
|
|
251
|
+
adsManager.addEventListener(AdEvent.STARTED, (adEvent) => {
|
|
252
|
+
console.log("[IMA] Ad started");
|
|
253
|
+
try {
|
|
254
|
+
const ad = adEvent.getAd();
|
|
255
|
+
if (ad && ad.getDuration) {
|
|
256
|
+
currentAdDuration = ad.getDuration();
|
|
257
|
+
console.log(`[IMA] Ad duration: ${currentAdDuration}s`);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn("[IMA] Could not get ad duration:", error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
232
263
|
adsManager.addEventListener(AdEvent.ALL_ADS_COMPLETED, () => {
|
|
233
264
|
console.log("[IMA] All ads completed");
|
|
234
265
|
adPlaying = false;
|
|
266
|
+
currentAdDuration = 0;
|
|
235
267
|
video.muted = originalMutedState;
|
|
236
268
|
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
270
|
+
video.play().catch(() => {
|
|
271
|
+
});
|
|
272
|
+
console.log(
|
|
273
|
+
"[IMA] Video resumed after all ads completed (VOD mode)"
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
console.log(
|
|
277
|
+
"[IMA] Video unmuted after all ads completed (Live mode)"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
239
280
|
emit("all_ads_completed");
|
|
240
281
|
});
|
|
241
282
|
console.log("[IMA] Ads manager event listeners attached");
|
|
@@ -249,8 +290,10 @@ function createImaController(video) {
|
|
|
249
290
|
adPlaying = false;
|
|
250
291
|
video.muted = originalMutedState;
|
|
251
292
|
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
294
|
+
video.play().catch(() => {
|
|
295
|
+
});
|
|
296
|
+
}
|
|
254
297
|
if (adsLoadedReject) {
|
|
255
298
|
adsLoadedReject(new Error("Failed to setup ads manager"));
|
|
256
299
|
adsLoadedReject = void 0;
|
|
@@ -304,8 +347,14 @@ function createImaController(video) {
|
|
|
304
347
|
const height = video.clientHeight || 360;
|
|
305
348
|
console.log(`[IMA] Initializing ads manager (${width}x${height})`);
|
|
306
349
|
adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
|
|
307
|
-
|
|
308
|
-
|
|
350
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
351
|
+
console.log("[IMA] Pausing video for ad playback (VOD mode)");
|
|
352
|
+
video.pause();
|
|
353
|
+
} else {
|
|
354
|
+
console.log(
|
|
355
|
+
"[IMA] Keeping video playing but muted for ad playback (Live mode)"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
309
358
|
adPlaying = true;
|
|
310
359
|
console.log("[IMA] Starting ad playback");
|
|
311
360
|
adsManager.start();
|
|
@@ -313,8 +362,10 @@ function createImaController(video) {
|
|
|
313
362
|
} catch (error) {
|
|
314
363
|
console.error("[IMA] Error starting ad playback:", error);
|
|
315
364
|
adPlaying = false;
|
|
316
|
-
|
|
317
|
-
|
|
365
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
366
|
+
video.play().catch(() => {
|
|
367
|
+
});
|
|
368
|
+
}
|
|
318
369
|
return Promise.reject(error);
|
|
319
370
|
}
|
|
320
371
|
},
|
|
@@ -325,8 +376,13 @@ function createImaController(video) {
|
|
|
325
376
|
adsManager?.stop?.();
|
|
326
377
|
} catch {
|
|
327
378
|
}
|
|
328
|
-
|
|
329
|
-
|
|
379
|
+
if (!options?.continueLiveStreamDuringAds) {
|
|
380
|
+
video.play().catch(() => {
|
|
381
|
+
});
|
|
382
|
+
console.log("[IMA] Video resumed after stop (VOD mode)");
|
|
383
|
+
} else {
|
|
384
|
+
console.log("[IMA] Video unmuted after stop (Live mode)");
|
|
385
|
+
}
|
|
330
386
|
},
|
|
331
387
|
destroy() {
|
|
332
388
|
try {
|
|
@@ -392,6 +448,84 @@ function createImaController(video) {
|
|
|
392
448
|
}
|
|
393
449
|
}
|
|
394
450
|
return 1;
|
|
451
|
+
},
|
|
452
|
+
getAdDuration() {
|
|
453
|
+
return currentAdDuration;
|
|
454
|
+
},
|
|
455
|
+
async preloadAds(vastTagUrls) {
|
|
456
|
+
console.log(`[IMA] Preloading ${vastTagUrls.length} ads`);
|
|
457
|
+
const adInfos = [];
|
|
458
|
+
for (const vastTagUrl of vastTagUrls) {
|
|
459
|
+
try {
|
|
460
|
+
await ensureImaLoaded();
|
|
461
|
+
const google = window.google;
|
|
462
|
+
const tempAdsLoader = new google.ima.AdsLoader(adDisplayContainer);
|
|
463
|
+
const adInfo = await new Promise((resolve, reject) => {
|
|
464
|
+
const timeout = setTimeout(() => {
|
|
465
|
+
reject(new Error("Preload timeout"));
|
|
466
|
+
}, 5e3);
|
|
467
|
+
tempAdsLoader.addEventListener(
|
|
468
|
+
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
|
|
469
|
+
(evt) => {
|
|
470
|
+
clearTimeout(timeout);
|
|
471
|
+
try {
|
|
472
|
+
const tempAdsManager = evt.getAdsManager(video);
|
|
473
|
+
let duration = 30;
|
|
474
|
+
try {
|
|
475
|
+
const ads = tempAdsManager.getCuePoints?.() || [];
|
|
476
|
+
if (ads.length > 0) {
|
|
477
|
+
duration = 15;
|
|
478
|
+
}
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
tempAdsManager.destroy();
|
|
482
|
+
resolve({
|
|
483
|
+
duration,
|
|
484
|
+
vastTagUrl,
|
|
485
|
+
isPreloaded: true
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
clearTimeout(timeout);
|
|
489
|
+
reject(error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
tempAdsLoader.addEventListener(
|
|
494
|
+
google.ima.AdErrorEvent.Type.AD_ERROR,
|
|
495
|
+
(errorEvent) => {
|
|
496
|
+
clearTimeout(timeout);
|
|
497
|
+
console.warn(
|
|
498
|
+
`[IMA] Preload error for ${vastTagUrl}:`,
|
|
499
|
+
errorEvent.getError()
|
|
500
|
+
);
|
|
501
|
+
resolve({
|
|
502
|
+
duration: 30,
|
|
503
|
+
vastTagUrl,
|
|
504
|
+
isPreloaded: false
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
const adsRequest = new google.ima.AdsRequest();
|
|
509
|
+
adsRequest.adTagUrl = vastTagUrl;
|
|
510
|
+
tempAdsLoader.requestAds(adsRequest);
|
|
511
|
+
});
|
|
512
|
+
adInfos.push(adInfo);
|
|
513
|
+
tempAdsLoader.destroy();
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn(`[IMA] Failed to preload ad ${vastTagUrl}:`, error);
|
|
516
|
+
adInfos.push({
|
|
517
|
+
duration: 30,
|
|
518
|
+
vastTagUrl,
|
|
519
|
+
isPreloaded: false
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
preloadedAds = adInfos;
|
|
524
|
+
console.log(
|
|
525
|
+
`[IMA] Preloaded ${adInfos.length} ads with total duration:`,
|
|
526
|
+
adInfos.reduce((sum, ad) => sum + ad.duration, 0)
|
|
527
|
+
);
|
|
528
|
+
return adInfos;
|
|
395
529
|
}
|
|
396
530
|
};
|
|
397
531
|
}
|
|
@@ -623,9 +757,14 @@ var StormcloudVideoPlayer = class {
|
|
|
623
757
|
this.currentAdIndex = 0;
|
|
624
758
|
this.totalAdsInBreak = 0;
|
|
625
759
|
this.showAds = false;
|
|
760
|
+
this.isLiveStream = false;
|
|
761
|
+
this.preloadedAdInfo = [];
|
|
762
|
+
this.cumulativeAdDurationMs = 0;
|
|
626
763
|
this.config = config;
|
|
627
764
|
this.video = config.videoElement;
|
|
628
|
-
this.ima = createImaController(this.video
|
|
765
|
+
this.ima = createImaController(this.video, {
|
|
766
|
+
continueLiveStreamDuringAds: false
|
|
767
|
+
});
|
|
629
768
|
}
|
|
630
769
|
async load() {
|
|
631
770
|
if (!this.attached) {
|
|
@@ -644,6 +783,22 @@ var StormcloudVideoPlayer = class {
|
|
|
644
783
|
this.initializeTracking();
|
|
645
784
|
if (this.shouldUseNativeHls()) {
|
|
646
785
|
this.video.src = this.config.src;
|
|
786
|
+
this.isLiveStream = this.config.lowLatencyMode ?? false;
|
|
787
|
+
if (this.config.debugAdTiming) {
|
|
788
|
+
console.log(
|
|
789
|
+
"[StormcloudVideoPlayer] allowNativeHls: true - VOD mode detected:",
|
|
790
|
+
{
|
|
791
|
+
isLive: this.isLiveStream,
|
|
792
|
+
allowNativeHls: this.config.allowNativeHls,
|
|
793
|
+
adBehavior: "vod (main video pauses during ads)"
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
this.ima.destroy();
|
|
798
|
+
this.ima = createImaController(this.video, {
|
|
799
|
+
continueLiveStreamDuringAds: false
|
|
800
|
+
});
|
|
801
|
+
this.ima.initialize();
|
|
647
802
|
if (this.config.autoplay) {
|
|
648
803
|
await this.video.play().catch(() => {
|
|
649
804
|
});
|
|
@@ -661,7 +816,23 @@ var StormcloudVideoPlayer = class {
|
|
|
661
816
|
this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
662
817
|
this.hls?.loadSource(this.config.src);
|
|
663
818
|
});
|
|
664
|
-
this.hls.on(Hls.Events.MANIFEST_PARSED, async () => {
|
|
819
|
+
this.hls.on(Hls.Events.MANIFEST_PARSED, async (_, data) => {
|
|
820
|
+
this.isLiveStream = this.hls?.levels?.some(
|
|
821
|
+
(level) => level?.details?.live === true || level?.details?.type === "LIVE"
|
|
822
|
+
) ?? false;
|
|
823
|
+
if (this.config.debugAdTiming) {
|
|
824
|
+
const adBehavior = this.shouldContinueLiveStreamDuringAds() ? "live (main video continues muted during ads)" : "vod (main video pauses during ads)";
|
|
825
|
+
console.log("[StormcloudVideoPlayer] Stream type detected:", {
|
|
826
|
+
isLive: this.isLiveStream,
|
|
827
|
+
allowNativeHls: this.config.allowNativeHls,
|
|
828
|
+
adBehavior
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
this.ima.destroy();
|
|
832
|
+
this.ima = createImaController(this.video, {
|
|
833
|
+
continueLiveStreamDuringAds: this.shouldContinueLiveStreamDuringAds()
|
|
834
|
+
});
|
|
835
|
+
this.ima.initialize();
|
|
665
836
|
if (this.config.autoplay) {
|
|
666
837
|
await this.video.play().catch(() => {
|
|
667
838
|
});
|
|
@@ -760,16 +931,28 @@ var StormcloudVideoPlayer = class {
|
|
|
760
931
|
this.ima.initialize();
|
|
761
932
|
this.ima.on("all_ads_completed", () => {
|
|
762
933
|
if (!this.inAdBreak) return;
|
|
934
|
+
const actualAdDuration = this.ima.getAdDuration();
|
|
935
|
+
if (actualAdDuration > 0) {
|
|
936
|
+
this.cumulativeAdDurationMs += actualAdDuration * 1e3;
|
|
937
|
+
if (this.config.debugAdTiming) {
|
|
938
|
+
console.log(
|
|
939
|
+
`[StormcloudVideoPlayer] Ad completed. Duration: ${actualAdDuration}s, Cumulative: ${this.cumulativeAdDurationMs}ms`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
763
943
|
const remaining = this.getRemainingAdMs();
|
|
764
|
-
|
|
944
|
+
const shouldContinue = this.shouldContinueAdBreak(remaining);
|
|
945
|
+
if (shouldContinue && this.adPodQueue.length > 0) {
|
|
765
946
|
const next = this.adPodQueue.shift();
|
|
766
947
|
this.currentAdIndex++;
|
|
767
948
|
this.playSingleAd(next).catch(() => {
|
|
768
949
|
});
|
|
950
|
+
} else if (shouldContinue && this.canRequestMoreAds()) {
|
|
951
|
+
this.requestAdditionalAds().catch(() => {
|
|
952
|
+
this.endAdBreak();
|
|
953
|
+
});
|
|
769
954
|
} else {
|
|
770
|
-
this.
|
|
771
|
-
this.totalAdsInBreak = 0;
|
|
772
|
-
this.showAds = false;
|
|
955
|
+
this.endAdBreak();
|
|
773
956
|
}
|
|
774
957
|
});
|
|
775
958
|
this.ima.on("ad_error", () => {
|
|
@@ -778,11 +961,16 @@ var StormcloudVideoPlayer = class {
|
|
|
778
961
|
}
|
|
779
962
|
if (!this.inAdBreak) return;
|
|
780
963
|
const remaining = this.getRemainingAdMs();
|
|
781
|
-
|
|
964
|
+
const shouldContinue = this.shouldContinueAdBreak(remaining);
|
|
965
|
+
if (shouldContinue && this.adPodQueue.length > 0) {
|
|
782
966
|
const next = this.adPodQueue.shift();
|
|
783
967
|
this.currentAdIndex++;
|
|
784
968
|
this.playSingleAd(next).catch(() => {
|
|
785
969
|
});
|
|
970
|
+
} else if (shouldContinue && this.canRequestMoreAds()) {
|
|
971
|
+
this.requestAdditionalAds().catch(() => {
|
|
972
|
+
this.handleAdFailure();
|
|
973
|
+
});
|
|
786
974
|
} else {
|
|
787
975
|
this.handleAdFailure();
|
|
788
976
|
}
|
|
@@ -1273,6 +1461,25 @@ var StormcloudVideoPlayer = class {
|
|
|
1273
1461
|
isShowingAds() {
|
|
1274
1462
|
return this.showAds;
|
|
1275
1463
|
}
|
|
1464
|
+
getAdBreakStats() {
|
|
1465
|
+
const remainingDurationMs = this.currentAdBreakTargetDurationMs != null ? Math.max(
|
|
1466
|
+
0,
|
|
1467
|
+
this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs
|
|
1468
|
+
) : void 0;
|
|
1469
|
+
const estimatedFillRate = this.currentAdBreakTargetDurationMs != null && this.currentAdBreakTargetDurationMs > 0 ? this.cumulativeAdDurationMs / this.currentAdBreakTargetDurationMs * 100 : void 0;
|
|
1470
|
+
return {
|
|
1471
|
+
isInAdBreak: this.inAdBreak,
|
|
1472
|
+
currentAdIndex: this.currentAdIndex,
|
|
1473
|
+
totalAdsInBreak: this.totalAdsInBreak,
|
|
1474
|
+
targetDurationMs: this.currentAdBreakTargetDurationMs,
|
|
1475
|
+
cumulativeDurationMs: this.cumulativeAdDurationMs,
|
|
1476
|
+
estimatedFillRate,
|
|
1477
|
+
remainingDurationMs
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
getPreloadedAdInfo() {
|
|
1481
|
+
return [...this.preloadedAdInfo];
|
|
1482
|
+
}
|
|
1276
1483
|
getStreamType() {
|
|
1277
1484
|
const url = this.config.src.toLowerCase();
|
|
1278
1485
|
if (url.includes(".m3u8") || url.includes("/hls/") || url.includes("application/vnd.apple.mpegurl")) {
|
|
@@ -1287,6 +1494,15 @@ var StormcloudVideoPlayer = class {
|
|
|
1287
1494
|
}
|
|
1288
1495
|
return !!(this.config.allowNativeHls && !(this.config.showCustomControls ?? false));
|
|
1289
1496
|
}
|
|
1497
|
+
shouldContinueLiveStreamDuringAds() {
|
|
1498
|
+
if (this.config.allowNativeHls) {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
if (!this.isLiveStream) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1290
1506
|
async loadDefaultVastFromAdstorm(adstormApiUrl, params) {
|
|
1291
1507
|
const usp = new URLSearchParams(params || {});
|
|
1292
1508
|
const url = `${adstormApiUrl}?${usp.toString()}`;
|
|
@@ -1298,59 +1514,59 @@ var StormcloudVideoPlayer = class {
|
|
|
1298
1514
|
this.apiVastTagUrl = tag;
|
|
1299
1515
|
}
|
|
1300
1516
|
}
|
|
1301
|
-
async handleAdStart(
|
|
1517
|
+
async handleAdStart(marker) {
|
|
1302
1518
|
const scheduled = this.findCurrentOrNextBreak(
|
|
1303
1519
|
this.video.currentTime * 1e3
|
|
1304
1520
|
);
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1521
|
+
let targetDurationMs = this.expectedAdBreakDurationMs;
|
|
1522
|
+
if (!targetDurationMs && scheduled?.durationMs != null) {
|
|
1523
|
+
targetDurationMs = scheduled.durationMs;
|
|
1524
|
+
}
|
|
1525
|
+
if (!targetDurationMs && marker.durationSeconds != null) {
|
|
1526
|
+
targetDurationMs = marker.durationSeconds * 1e3;
|
|
1527
|
+
}
|
|
1528
|
+
this.currentAdBreakTargetDurationMs = targetDurationMs;
|
|
1529
|
+
this.cumulativeAdDurationMs = 0;
|
|
1530
|
+
if (this.config.debugAdTiming) {
|
|
1531
|
+
console.log(
|
|
1532
|
+
"[StormcloudVideoPlayer] Starting ad break with target duration:",
|
|
1533
|
+
{
|
|
1534
|
+
targetDurationMs,
|
|
1535
|
+
scte35Duration: marker.durationSeconds,
|
|
1536
|
+
scheduledDuration: scheduled?.durationMs
|
|
1316
1537
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (this.config.debugAdTiming) {
|
|
1322
|
-
console.log(
|
|
1323
|
-
`[StormcloudVideoPlayer] Using API VAST tag with ${adsNumber} ads:`,
|
|
1324
|
-
vastTagUrl
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1327
|
-
} else if (tags && tags.length > 0) {
|
|
1328
|
-
vastTagUrl = tags[0];
|
|
1329
|
-
const rest = tags.slice(1);
|
|
1330
|
-
this.adPodQueue = rest;
|
|
1331
|
-
this.currentAdIndex = 0;
|
|
1332
|
-
this.totalAdsInBreak = tags.length;
|
|
1333
|
-
if (this.config.debugAdTiming) {
|
|
1334
|
-
console.log(
|
|
1335
|
-
"[StormcloudVideoPlayer] Using scheduled VAST tag:",
|
|
1336
|
-
vastTagUrl
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
} else {
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
const adQueue = await this.buildAdQueueForDuration(targetDurationMs);
|
|
1541
|
+
if (adQueue.length === 0) {
|
|
1340
1542
|
if (this.config.debugAdTiming) {
|
|
1341
|
-
console.log("[StormcloudVideoPlayer] No
|
|
1543
|
+
console.log("[StormcloudVideoPlayer] No ads available for ad break");
|
|
1342
1544
|
}
|
|
1343
1545
|
return;
|
|
1344
1546
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1547
|
+
this.adPodQueue = adQueue.slice(1);
|
|
1548
|
+
this.preloadedAdInfo = await this.getAdInfoForQueue(adQueue);
|
|
1549
|
+
this.currentAdIndex = 0;
|
|
1550
|
+
this.totalAdsInBreak = adQueue.length;
|
|
1551
|
+
this.showAds = true;
|
|
1552
|
+
if (this.config.debugAdTiming) {
|
|
1553
|
+
const totalEstimatedDuration = this.preloadedAdInfo.reduce(
|
|
1554
|
+
(sum, ad) => sum + ad.duration,
|
|
1555
|
+
0
|
|
1556
|
+
);
|
|
1557
|
+
console.log("[StormcloudVideoPlayer] Ad queue built:", {
|
|
1558
|
+
totalAds: adQueue.length,
|
|
1559
|
+
estimatedTotalDuration: totalEstimatedDuration,
|
|
1560
|
+
targetDuration: targetDurationMs ? targetDurationMs / 1e3 : "unknown",
|
|
1561
|
+
fillRate: targetDurationMs ? totalEstimatedDuration * 1e3 / targetDurationMs : "unknown"
|
|
1562
|
+
});
|
|
1349
1563
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1564
|
+
this.currentAdIndex++;
|
|
1565
|
+
await this.playSingleAd(adQueue[0]);
|
|
1566
|
+
if (targetDurationMs != null) {
|
|
1567
|
+
this.expectedAdBreakDurationMs = targetDurationMs;
|
|
1352
1568
|
this.currentAdBreakStartWallClockMs = this.currentAdBreakStartWallClockMs ?? Date.now();
|
|
1353
|
-
this.scheduleAdStopCountdown(
|
|
1569
|
+
this.scheduleAdStopCountdown(targetDurationMs);
|
|
1354
1570
|
}
|
|
1355
1571
|
}
|
|
1356
1572
|
findCurrentOrNextBreak(nowMs) {
|
|
@@ -1467,25 +1683,89 @@ var StormcloudVideoPlayer = class {
|
|
|
1467
1683
|
"[StormcloudVideoPlayer] Handling ad failure - resuming content"
|
|
1468
1684
|
);
|
|
1469
1685
|
}
|
|
1686
|
+
this.endAdBreak();
|
|
1687
|
+
if (this.video.paused) {
|
|
1688
|
+
this.video.play().catch(() => {
|
|
1689
|
+
if (this.config.debugAdTiming) {
|
|
1690
|
+
console.error(
|
|
1691
|
+
"[StormcloudVideoPlayer] Failed to resume video after ad failure"
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
endAdBreak() {
|
|
1698
|
+
if (this.config.debugAdTiming) {
|
|
1699
|
+
const targetDuration = this.currentAdBreakTargetDurationMs ? this.currentAdBreakTargetDurationMs / 1e3 : "unknown";
|
|
1700
|
+
const actualDuration = this.cumulativeAdDurationMs / 1e3;
|
|
1701
|
+
const fillRate = this.currentAdBreakTargetDurationMs ? (this.cumulativeAdDurationMs / this.currentAdBreakTargetDurationMs * 100).toFixed(1) : "unknown";
|
|
1702
|
+
console.log("[StormcloudVideoPlayer] Ad break ended:", {
|
|
1703
|
+
targetDurationSeconds: targetDuration,
|
|
1704
|
+
actualDurationSeconds: actualDuration,
|
|
1705
|
+
fillRate: `${fillRate}%`,
|
|
1706
|
+
totalAdsPlayed: this.currentAdIndex
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1470
1709
|
this.inAdBreak = false;
|
|
1471
1710
|
this.expectedAdBreakDurationMs = void 0;
|
|
1472
1711
|
this.currentAdBreakStartWallClockMs = void 0;
|
|
1712
|
+
this.currentAdBreakTargetDurationMs = void 0;
|
|
1713
|
+
this.cumulativeAdDurationMs = 0;
|
|
1473
1714
|
this.clearAdStartTimer();
|
|
1474
1715
|
this.clearAdStopTimer();
|
|
1475
1716
|
this.clearAdFailsafeTimer();
|
|
1476
1717
|
this.adPodQueue = [];
|
|
1718
|
+
this.preloadedAdInfo = [];
|
|
1477
1719
|
this.showAds = false;
|
|
1478
1720
|
this.currentAdIndex = 0;
|
|
1479
1721
|
this.totalAdsInBreak = 0;
|
|
1480
|
-
|
|
1481
|
-
|
|
1722
|
+
}
|
|
1723
|
+
shouldContinueAdBreak(remainingMs) {
|
|
1724
|
+
if (remainingMs > 500) {
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
if (this.currentAdBreakTargetDurationMs) {
|
|
1728
|
+
const targetRemainingMs = this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs;
|
|
1729
|
+
const minAdDuration = this.config.minAdDurationMs ?? 5e3;
|
|
1730
|
+
if (targetRemainingMs > minAdDuration) {
|
|
1482
1731
|
if (this.config.debugAdTiming) {
|
|
1483
|
-
console.
|
|
1484
|
-
|
|
1732
|
+
console.log(
|
|
1733
|
+
`[StormcloudVideoPlayer] Target duration not filled, continuing. Remaining: ${targetRemainingMs}ms`
|
|
1485
1734
|
);
|
|
1486
1735
|
}
|
|
1487
|
-
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
canRequestMoreAds() {
|
|
1742
|
+
const maxAdsPerBreak = this.config.maxAdsPerBreak ?? 10;
|
|
1743
|
+
if (this.currentAdIndex >= maxAdsPerBreak) {
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
return !!this.apiVastTagUrl;
|
|
1747
|
+
}
|
|
1748
|
+
async requestAdditionalAds() {
|
|
1749
|
+
if (!this.currentAdBreakTargetDurationMs || !this.apiVastTagUrl) {
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
"Cannot request additional ads without target duration and VAST URL"
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const remainingDurationMs = this.currentAdBreakTargetDurationMs - this.cumulativeAdDurationMs;
|
|
1755
|
+
const estimatedAdDurationMs = this.getEstimatedAdDuration() * 1e3;
|
|
1756
|
+
if (remainingDurationMs < estimatedAdDurationMs * 0.5) {
|
|
1757
|
+
throw new Error("Not enough time remaining for additional ads");
|
|
1758
|
+
}
|
|
1759
|
+
if (this.config.debugAdTiming) {
|
|
1760
|
+
console.log(
|
|
1761
|
+
`[StormcloudVideoPlayer] Requesting additional ads for remaining ${remainingDurationMs}ms`
|
|
1762
|
+
);
|
|
1488
1763
|
}
|
|
1764
|
+
this.adPodQueue.push(this.apiVastTagUrl);
|
|
1765
|
+
this.totalAdsInBreak++;
|
|
1766
|
+
const next = this.adPodQueue.shift();
|
|
1767
|
+
this.currentAdIndex++;
|
|
1768
|
+
await this.playSingleAd(next);
|
|
1489
1769
|
}
|
|
1490
1770
|
startAdFailsafeTimer() {
|
|
1491
1771
|
this.clearAdFailsafeTimer();
|
|
@@ -1519,6 +1799,109 @@ var StormcloudVideoPlayer = class {
|
|
|
1519
1799
|
}
|
|
1520
1800
|
return [b.vastTagUrl];
|
|
1521
1801
|
}
|
|
1802
|
+
async buildAdQueueForDuration(targetDurationMs) {
|
|
1803
|
+
const adQueue = [];
|
|
1804
|
+
let baseVastTagUrl = this.apiVastTagUrl;
|
|
1805
|
+
if (!baseVastTagUrl) {
|
|
1806
|
+
const scheduled = this.findCurrentOrNextBreak(
|
|
1807
|
+
this.video.currentTime * 1e3
|
|
1808
|
+
);
|
|
1809
|
+
const scheduledTags = this.selectVastTagsForBreak(scheduled);
|
|
1810
|
+
if (scheduledTags && scheduledTags.length > 0) {
|
|
1811
|
+
baseVastTagUrl = scheduledTags[0];
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (!baseVastTagUrl) {
|
|
1815
|
+
return adQueue;
|
|
1816
|
+
}
|
|
1817
|
+
if (!targetDurationMs) {
|
|
1818
|
+
let adsNumber = 1;
|
|
1819
|
+
if (this.vastConfig) {
|
|
1820
|
+
const isHls = this.config.src.includes(".m3u8") || this.config.src.includes("hls");
|
|
1821
|
+
if (isHls && this.vastConfig.cue_tones?.number_ads) {
|
|
1822
|
+
adsNumber = this.vastConfig.cue_tones.number_ads;
|
|
1823
|
+
} else if (!isHls && this.vastConfig.timer_vod?.number_ads) {
|
|
1824
|
+
adsNumber = this.vastConfig.timer_vod.number_ads;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return new Array(adsNumber).fill(baseVastTagUrl);
|
|
1828
|
+
}
|
|
1829
|
+
const targetDurationSeconds = targetDurationMs / 1e3;
|
|
1830
|
+
let cumulativeDurationSeconds = 0;
|
|
1831
|
+
const maxAdsToTry = 10;
|
|
1832
|
+
let adsAdded = 0;
|
|
1833
|
+
if (this.config.debugAdTiming) {
|
|
1834
|
+
console.log(
|
|
1835
|
+
`[StormcloudVideoPlayer] Attempting to fill ${targetDurationSeconds}s with ads`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
while (cumulativeDurationSeconds < targetDurationSeconds && adsAdded < maxAdsToTry) {
|
|
1839
|
+
adQueue.push(baseVastTagUrl);
|
|
1840
|
+
adsAdded++;
|
|
1841
|
+
const estimatedAdDuration = this.getEstimatedAdDuration();
|
|
1842
|
+
cumulativeDurationSeconds += estimatedAdDuration;
|
|
1843
|
+
if (this.config.debugAdTiming) {
|
|
1844
|
+
console.log(
|
|
1845
|
+
`[StormcloudVideoPlayer] Added ad ${adsAdded}, cumulative duration: ${cumulativeDurationSeconds}s`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const remainingDuration = targetDurationSeconds - cumulativeDurationSeconds;
|
|
1849
|
+
const toleranceSeconds = (this.config.adBreakGapToleranceMs ?? 2e3) / 1e3;
|
|
1850
|
+
if (remainingDuration < estimatedAdDuration && remainingDuration >= -toleranceSeconds) {
|
|
1851
|
+
if (this.config.debugAdTiming) {
|
|
1852
|
+
console.log(
|
|
1853
|
+
`[StormcloudVideoPlayer] Within tolerance, adding final ad. Overage: ${-remainingDuration}s`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
if (remainingDuration < estimatedAdDuration && remainingDuration < -toleranceSeconds) {
|
|
1859
|
+
if (this.config.debugAdTiming) {
|
|
1860
|
+
console.log(
|
|
1861
|
+
`[StormcloudVideoPlayer] Would exceed duration by too much, stopping at ${adsAdded} ads`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return adQueue;
|
|
1868
|
+
}
|
|
1869
|
+
getEstimatedAdDuration() {
|
|
1870
|
+
if (this.vastConfig) {
|
|
1871
|
+
return 15;
|
|
1872
|
+
}
|
|
1873
|
+
return 30;
|
|
1874
|
+
}
|
|
1875
|
+
async getAdInfoForQueue(adQueue) {
|
|
1876
|
+
if (this.config.enableAdPreloading === false) {
|
|
1877
|
+
if (this.config.debugAdTiming) {
|
|
1878
|
+
console.log(
|
|
1879
|
+
"[StormcloudVideoPlayer] Ad preloading disabled, using estimates"
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
return adQueue.map((vastTagUrl) => ({
|
|
1883
|
+
duration: this.getEstimatedAdDuration(),
|
|
1884
|
+
vastTagUrl,
|
|
1885
|
+
isPreloaded: false
|
|
1886
|
+
}));
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
const adInfos = await this.ima.preloadAds(adQueue);
|
|
1890
|
+
return adInfos;
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
if (this.config.debugAdTiming) {
|
|
1893
|
+
console.warn(
|
|
1894
|
+
"[StormcloudVideoPlayer] Failed to preload ads, using estimates:",
|
|
1895
|
+
error
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
return adQueue.map((vastTagUrl) => ({
|
|
1899
|
+
duration: this.getEstimatedAdDuration(),
|
|
1900
|
+
vastTagUrl,
|
|
1901
|
+
isPreloaded: false
|
|
1902
|
+
}));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1522
1905
|
getRemainingAdMs() {
|
|
1523
1906
|
if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
|
|
1524
1907
|
return 0;
|
|
@@ -1601,6 +1984,9 @@ var StormcloudVideoPlayer = class {
|
|
|
1601
1984
|
isFullscreen() {
|
|
1602
1985
|
return !!document.fullscreenElement;
|
|
1603
1986
|
}
|
|
1987
|
+
isLive() {
|
|
1988
|
+
return this.isLiveStream;
|
|
1989
|
+
}
|
|
1604
1990
|
get videoElement() {
|
|
1605
1991
|
return this.video;
|
|
1606
1992
|
}
|
|
@@ -1695,6 +2081,7 @@ var StormcloudVideoPlayerComponent = React.memo(
|
|
|
1695
2081
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
1696
2082
|
const [isBuffering, setIsBuffering] = React.useState(false);
|
|
1697
2083
|
const [showCenterPlay, setShowCenterPlay] = React.useState(false);
|
|
2084
|
+
const [showLicenseWarning, setShowLicenseWarning] = React.useState(false);
|
|
1698
2085
|
const formatTime = (seconds) => {
|
|
1699
2086
|
if (!isFinite(seconds)) return "0:00:00";
|
|
1700
2087
|
const hours = Math.floor(seconds / 3600);
|
|
@@ -1752,6 +2139,15 @@ var StormcloudVideoPlayerComponent = React.memo(
|
|
|
1752
2139
|
if (typeof window === "undefined") return;
|
|
1753
2140
|
const el = videoRef.current;
|
|
1754
2141
|
if (!el || !src) return;
|
|
2142
|
+
if (!licenseKey) {
|
|
2143
|
+
setShowLicenseWarning(true);
|
|
2144
|
+
setIsLoading(false);
|
|
2145
|
+
console.warn(
|
|
2146
|
+
"StormcloudVideoPlayer: License key is required but not provided. Please set the licenseKey prop to use the player."
|
|
2147
|
+
);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
setShowLicenseWarning(false);
|
|
1755
2151
|
if (playerRef.current) {
|
|
1756
2152
|
try {
|
|
1757
2153
|
playerRef.current.destroy();
|
|
@@ -2075,7 +2471,60 @@ var StormcloudVideoPlayerComponent = React.memo(
|
|
|
2075
2471
|
)
|
|
2076
2472
|
}
|
|
2077
2473
|
),
|
|
2078
|
-
|
|
2474
|
+
showLicenseWarning && /* @__PURE__ */ jsxs(
|
|
2475
|
+
"div",
|
|
2476
|
+
{
|
|
2477
|
+
style: {
|
|
2478
|
+
position: "absolute",
|
|
2479
|
+
top: "50%",
|
|
2480
|
+
left: "50%",
|
|
2481
|
+
transform: "translate(-50%, -50%)",
|
|
2482
|
+
zIndex: 25,
|
|
2483
|
+
background: "linear-gradient(135deg, rgba(220, 38, 38, 0.95) 0%, rgba(185, 28, 28, 0.9) 100%)",
|
|
2484
|
+
color: "white",
|
|
2485
|
+
padding: "24px 32px",
|
|
2486
|
+
borderRadius: "16px",
|
|
2487
|
+
backdropFilter: "blur(20px)",
|
|
2488
|
+
border: "2px solid rgba(255, 255, 255, 0.2)",
|
|
2489
|
+
boxShadow: "0 20px 60px rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.2)",
|
|
2490
|
+
textAlign: "center",
|
|
2491
|
+
maxWidth: "400px",
|
|
2492
|
+
margin: "0 16px"
|
|
2493
|
+
},
|
|
2494
|
+
children: [
|
|
2495
|
+
/* @__PURE__ */ jsx(
|
|
2496
|
+
"div",
|
|
2497
|
+
{
|
|
2498
|
+
style: {
|
|
2499
|
+
fontSize: "20px",
|
|
2500
|
+
fontWeight: "bold",
|
|
2501
|
+
marginBottom: "12px",
|
|
2502
|
+
color: "#ffffff",
|
|
2503
|
+
textShadow: "0 2px 4px rgba(0, 0, 0, 0.5)"
|
|
2504
|
+
},
|
|
2505
|
+
children: "License Key Required"
|
|
2506
|
+
}
|
|
2507
|
+
),
|
|
2508
|
+
/* @__PURE__ */ jsxs(
|
|
2509
|
+
"div",
|
|
2510
|
+
{
|
|
2511
|
+
style: {
|
|
2512
|
+
fontSize: "14px",
|
|
2513
|
+
lineHeight: "1.5",
|
|
2514
|
+
color: "rgba(255, 255, 255, 0.9)",
|
|
2515
|
+
textShadow: "0 1px 2px rgba(0, 0, 0, 0.3)"
|
|
2516
|
+
},
|
|
2517
|
+
children: [
|
|
2518
|
+
"Please provide a valid license key to use the Stormcloud Video Player.",
|
|
2519
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
2520
|
+
"Contact your administrator for licensing information."
|
|
2521
|
+
]
|
|
2522
|
+
}
|
|
2523
|
+
)
|
|
2524
|
+
]
|
|
2525
|
+
}
|
|
2526
|
+
),
|
|
2527
|
+
showCenterPlay && !isLoading && !isBuffering && !showLicenseWarning && !adStatus.showAds && /* @__PURE__ */ jsx(
|
|
2079
2528
|
"div",
|
|
2080
2529
|
{
|
|
2081
2530
|
onClick: handleCenterPlayClick,
|
|
@@ -2126,7 +2575,7 @@ var StormcloudVideoPlayerComponent = React.memo(
|
|
|
2126
2575
|
)
|
|
2127
2576
|
}
|
|
2128
2577
|
),
|
|
2129
|
-
shouldShowEnhancedControls ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
|
|
2578
|
+
shouldShowEnhancedControls && !showLicenseWarning ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
|
|
2130
2579
|
"div",
|
|
2131
2580
|
{
|
|
2132
2581
|
style: {
|
|
@@ -2591,7 +3040,7 @@ var StormcloudVideoPlayerComponent = React.memo(
|
|
|
2591
3040
|
)
|
|
2592
3041
|
]
|
|
2593
3042
|
}
|
|
2594
|
-
) }) : showCustomControls && /* @__PURE__ */ jsxs(
|
|
3043
|
+
) }) : showCustomControls && !showLicenseWarning && /* @__PURE__ */ jsxs(
|
|
2595
3044
|
"div",
|
|
2596
3045
|
{
|
|
2597
3046
|
style: {
|