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.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
- video.play().catch(() => {
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
- video.pause();
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- console.log("[IMA] Pausing video for ad playback");
308
- video.pause();
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
- video.play().catch(() => {
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
- video.play().catch(() => {
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
- if (remaining > 500 && this.adPodQueue.length > 0) {
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.currentAdIndex = 0;
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
- if (remaining > 500 && this.adPodQueue.length > 0) {
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(_marker) {
1517
+ async handleAdStart(marker) {
1302
1518
  const scheduled = this.findCurrentOrNextBreak(
1303
1519
  this.video.currentTime * 1e3
1304
1520
  );
1305
- const tags = this.selectVastTagsForBreak(scheduled);
1306
- let vastTagUrl;
1307
- let adsNumber = 1;
1308
- if (this.apiVastTagUrl) {
1309
- vastTagUrl = this.apiVastTagUrl;
1310
- if (this.vastConfig) {
1311
- const isHls = this.config.src.includes(".m3u8") || this.config.src.includes("hls");
1312
- if (isHls && this.vastConfig.cue_tones?.number_ads) {
1313
- adsNumber = this.vastConfig.cue_tones.number_ads;
1314
- } else if (!isHls && this.vastConfig.timer_vod?.number_ads) {
1315
- adsNumber = this.vastConfig.timer_vod.number_ads;
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
- this.adPodQueue = new Array(adsNumber - 1).fill(vastTagUrl);
1319
- this.currentAdIndex = 0;
1320
- this.totalAdsInBreak = adsNumber;
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 VAST tag available for ad");
1543
+ console.log("[StormcloudVideoPlayer] No ads available for ad break");
1342
1544
  }
1343
1545
  return;
1344
1546
  }
1345
- if (vastTagUrl) {
1346
- this.showAds = true;
1347
- this.currentAdIndex++;
1348
- await this.playSingleAd(vastTagUrl);
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
- if (this.expectedAdBreakDurationMs == null && scheduled?.durationMs != null) {
1351
- this.expectedAdBreakDurationMs = scheduled.durationMs;
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(this.expectedAdBreakDurationMs);
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
- if (this.video.paused) {
1481
- this.video.play().catch(() => {
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.error(
1484
- "[StormcloudVideoPlayer] Failed to resume video after ad failure"
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
- showCenterPlay && !isLoading && !isBuffering && !adStatus.showAds && /* @__PURE__ */ jsx(
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: {