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/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
- video.play().catch(() => {
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
- video.pause();
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- console.log("[IMA] Pausing video for ad playback");
367
- video.pause();
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- if (remaining > 500 && this.adPodQueue.length > 0) {
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.currentAdIndex = 0;
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
- if (remaining > 500 && this.adPodQueue.length > 0) {
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(_marker) {
1576
+ async handleAdStart(marker) {
1361
1577
  const scheduled = this.findCurrentOrNextBreak(
1362
1578
  this.video.currentTime * 1e3
1363
1579
  );
1364
- const tags = this.selectVastTagsForBreak(scheduled);
1365
- let vastTagUrl;
1366
- let adsNumber = 1;
1367
- if (this.apiVastTagUrl) {
1368
- vastTagUrl = this.apiVastTagUrl;
1369
- if (this.vastConfig) {
1370
- const isHls = this.config.src.includes(".m3u8") || this.config.src.includes("hls");
1371
- if (isHls && this.vastConfig.cue_tones?.number_ads) {
1372
- adsNumber = this.vastConfig.cue_tones.number_ads;
1373
- } else if (!isHls && this.vastConfig.timer_vod?.number_ads) {
1374
- adsNumber = this.vastConfig.timer_vod.number_ads;
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
- this.adPodQueue = new Array(adsNumber - 1).fill(vastTagUrl);
1378
- this.currentAdIndex = 0;
1379
- this.totalAdsInBreak = adsNumber;
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 VAST tag available for ad");
1602
+ console.log("[StormcloudVideoPlayer] No ads available for ad break");
1401
1603
  }
1402
1604
  return;
1403
1605
  }
1404
- if (vastTagUrl) {
1405
- this.showAds = true;
1406
- this.currentAdIndex++;
1407
- await this.playSingleAd(vastTagUrl);
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
- if (this.expectedAdBreakDurationMs == null && scheduled?.durationMs != null) {
1410
- this.expectedAdBreakDurationMs = scheduled.durationMs;
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(this.expectedAdBreakDurationMs);
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
- if (this.video.paused) {
1540
- this.video.play().catch(() => {
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.error(
1543
- "[StormcloudVideoPlayer] Failed to resume video after ad failure"
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
- showCenterPlay && !isLoading && !isBuffering && !adStatus.showAds && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
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: {