stormcloud-video-player 0.2.35 → 0.3.0

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.
@@ -631,19 +631,10 @@ function createImaController(video, options) {
631
631
  adsManager.addEventListener(
632
632
  AdEvent.CONTENT_PAUSE_REQUESTED,
633
633
  () => {
634
- console.log("[DEBUG-FLOW] \u{1F3AF} CONTENT_PAUSE_REQUESTED - Ad starting");
634
+ console.log("[DEBUG-FLOW] \u{1F3AF} CONTENT_PAUSE_REQUESTED - Ad request accepted");
635
635
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
636
636
  video.pause();
637
637
  }
638
- hideContentVideo();
639
- if (adContainerEl) {
640
- adContainerEl.style.pointerEvents = "auto";
641
- adContainerEl.style.display = "flex";
642
- adContainerEl.style.backgroundColor = "#000";
643
- adContainerEl.offsetHeight;
644
- adContainerEl.style.opacity = "1";
645
- console.log("[DEBUG-LAYER] \u{1F7E1} Ad container VISIBLE");
646
- }
647
638
  adPlaying = true;
648
639
  setAdPlayingFlag(true);
649
640
  emit("content_pause");
@@ -2171,6 +2162,9 @@ var StormcloudVideoPlayer = class {
2171
2162
  this.activeAdRequestToken = null;
2172
2163
  this.adRequestWatchdogToken = null;
2173
2164
  this.adFailsafeToken = null;
2165
+ this.fetchedAdDurations = /* @__PURE__ */ new Map();
2166
+ this.targetAdBreakDurationMs = null;
2167
+ this.isAdaptiveMode = false;
2174
2168
  initializePolyfills();
2175
2169
  const browserOverrides = getBrowserConfigOverrides();
2176
2170
  this.config = { ...config, ...browserOverrides };
@@ -2476,7 +2470,7 @@ var StormcloudVideoPlayer = class {
2476
2470
  this.clearAdRequestWatchdog();
2477
2471
  this.activeAdRequestToken = null;
2478
2472
  this.showAds = true;
2479
- this.enforceAdHoldState();
2473
+ console.log("[DEBUG-LAYER] \u{1F3AC} Layers: Main=hidden, Ad=visible, Placeholder=no");
2480
2474
  });
2481
2475
  this.ima.on("content_resume", () => {
2482
2476
  console.log(`[DEBUG-POD] \u23F8\uFE0F content_resume | ad ${this.currentAdIndex}/${this.totalAdsInBreak}, queue=${this.adPodQueue.length}, remaining=${this.getRemainingAdMs()}ms`);
@@ -2656,6 +2650,12 @@ var StormcloudVideoPlayer = class {
2656
2650
  });
2657
2651
  }
2658
2652
  if (marker.type === "start") {
2653
+ if (this.inAdBreak) {
2654
+ console.log(
2655
+ `[DEBUG-POD] \u26A0\uFE0F SCTE-35 start marker ignored - already in ad break (currentTime: ${this.video.currentTime})`
2656
+ );
2657
+ return;
2658
+ }
2659
2659
  this.inAdBreak = true;
2660
2660
  const durationMs = marker.durationSeconds != null ? marker.durationSeconds * 1e3 : void 0;
2661
2661
  this.expectedAdBreakDurationMs = durationMs;
@@ -2722,6 +2722,9 @@ var StormcloudVideoPlayer = class {
2722
2722
  return;
2723
2723
  }
2724
2724
  if (marker.type === "progress" && this.inAdBreak) {
2725
+ console.log(
2726
+ `[DEBUG-POD] \u{1F4CA} SCTE-35 progress marker (currentTime: ${this.video.currentTime})`
2727
+ );
2725
2728
  if (marker.durationSeconds != null) {
2726
2729
  this.expectedAdBreakDurationMs = marker.durationSeconds * 1e3;
2727
2730
  }
@@ -2733,7 +2736,8 @@ var StormcloudVideoPlayer = class {
2733
2736
  );
2734
2737
  this.scheduleAdStopCountdown(remainingMs);
2735
2738
  }
2736
- if (!this.ima.isAdPlaying()) {
2739
+ if (!this.ima.isAdPlaying() && this.activeAdRequestToken === null) {
2740
+ console.log("[DEBUG-POD] \u{1F4CA} Progress marker: no ad playing, attempting to start");
2737
2741
  const scheduled = this.findCurrentOrNextBreak(
2738
2742
  this.video.currentTime * 1e3
2739
2743
  );
@@ -2742,25 +2746,31 @@ var StormcloudVideoPlayer = class {
2742
2746
  const first = tags[0];
2743
2747
  const rest = tags.slice(1);
2744
2748
  this.adPodQueue = rest;
2745
- if (!this.showAds) {
2746
- this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
2747
- }
2748
2749
  this.playSingleAd(first).catch(() => {
2749
2750
  });
2750
2751
  }
2752
+ } else {
2753
+ console.log(
2754
+ `[DEBUG-POD] \u{1F4CA} Progress marker: ad playing or request active (playing=${this.ima.isAdPlaying()}, token=${this.activeAdRequestToken})`
2755
+ );
2751
2756
  }
2752
2757
  return;
2753
2758
  }
2754
2759
  if (marker.type === "end") {
2760
+ console.log(
2761
+ `[DEBUG-POD] \u{1F3C1} SCTE-35 end marker received (currentTime: ${this.video.currentTime})`
2762
+ );
2755
2763
  this.inAdBreak = false;
2756
2764
  this.expectedAdBreakDurationMs = void 0;
2757
2765
  this.currentAdBreakStartWallClockMs = void 0;
2758
2766
  this.clearAdStartTimer();
2759
2767
  this.clearAdStopTimer();
2760
2768
  if (this.ima.isAdPlaying()) {
2769
+ console.log("[DEBUG-POD] \u{1F6D1} Stopping ad due to SCTE-35 end marker");
2761
2770
  this.ima.stop().catch(() => {
2762
2771
  });
2763
2772
  }
2773
+ this.handleAdPodComplete();
2764
2774
  return;
2765
2775
  }
2766
2776
  }
@@ -2961,7 +2971,7 @@ var StormcloudVideoPlayer = class {
2961
2971
  }
2962
2972
  }
2963
2973
  async fetchAdConfiguration() {
2964
- var _a, _b, _c;
2974
+ var _a, _b, _c, _d, _e, _f, _g;
2965
2975
  const vastMode = this.config.vastMode || "default";
2966
2976
  if (this.config.debugAdTiming) {
2967
2977
  console.log("[StormcloudVideoPlayer] VAST mode:", vastMode);
@@ -3032,6 +3042,16 @@ var StormcloudVideoPlayer = class {
3032
3042
  );
3033
3043
  }
3034
3044
  }
3045
+ const numberAds = (_g = (_f = (_e = (_d = data.response) == null ? void 0 : _d.options) == null ? void 0 : _e.vast) == null ? void 0 : _f.cue_tones) == null ? void 0 : _g.number_ads;
3046
+ if (numberAds != null && numberAds > 0) {
3047
+ this.apiNumberAds = numberAds;
3048
+ if (this.config.debugAdTiming) {
3049
+ console.log(
3050
+ "[StormcloudVideoPlayer] Number of ads per break from API:",
3051
+ this.apiNumberAds
3052
+ );
3053
+ }
3054
+ }
3035
3055
  }
3036
3056
  getCurrentAdIndex() {
3037
3057
  return this.currentAdIndex;
@@ -3039,6 +3059,27 @@ var StormcloudVideoPlayer = class {
3039
3059
  getTotalAdsInBreak() {
3040
3060
  return this.totalAdsInBreak;
3041
3061
  }
3062
+ generateVastUrlsWithCorrelators(baseUrl, count) {
3063
+ const urls = [];
3064
+ for (let i = 0; i < count; i++) {
3065
+ try {
3066
+ const url = new URL(baseUrl);
3067
+ const timestamp = Date.now();
3068
+ const random = Math.floor(Math.random() * 1e6);
3069
+ const uniqueCorrelator = `${timestamp}${random}${i}`;
3070
+ url.searchParams.set("correlator", uniqueCorrelator);
3071
+ urls.push(url.toString());
3072
+ } catch (error) {
3073
+ console.warn(
3074
+ "[StormcloudVideoPlayer] Failed to parse VAST URL:",
3075
+ baseUrl,
3076
+ error
3077
+ );
3078
+ urls.push(`${baseUrl}${baseUrl.includes("?") ? "&" : "?"}correlator=${Date.now()}${i}`);
3079
+ }
3080
+ }
3081
+ return urls;
3082
+ }
3042
3083
  isAdPlaying() {
3043
3084
  return this.inAdBreak && this.ima.isAdPlaying();
3044
3085
  }
@@ -3077,7 +3118,52 @@ var StormcloudVideoPlayer = class {
3077
3118
  const tags = this.selectVastTagsForBreak(scheduled);
3078
3119
  let vastTagUrls = [];
3079
3120
  if (this.apiVastTagUrl) {
3080
- vastTagUrls = [this.apiVastTagUrl];
3121
+ let numberOfAds = 1;
3122
+ if (this.isLiveStream) {
3123
+ const adBreakDurationMs = _marker.durationSeconds != null ? _marker.durationSeconds * 1e3 : scheduled == null ? void 0 : scheduled.durationMs;
3124
+ if (adBreakDurationMs != null && adBreakDurationMs > 0) {
3125
+ this.isAdaptiveMode = true;
3126
+ this.targetAdBreakDurationMs = adBreakDurationMs;
3127
+ this.fetchedAdDurations.clear();
3128
+ numberOfAds = 2;
3129
+ if (this.config.debugAdTiming) {
3130
+ console.log(
3131
+ `[ADAPTIVE-POD] \u{1F4FA} LIVE MODE (ADAPTIVE): Target duration=${adBreakDurationMs}ms | Starting with ${numberOfAds} ads, will fetch actual durations and add more dynamically`
3132
+ );
3133
+ }
3134
+ } else {
3135
+ if (this.config.debugAdTiming) {
3136
+ console.warn(
3137
+ "[DEBUG-POD] \u26A0\uFE0F LIVE MODE: No duration available, defaulting to 1 ad"
3138
+ );
3139
+ }
3140
+ }
3141
+ } else {
3142
+ this.isAdaptiveMode = false;
3143
+ this.targetAdBreakDurationMs = null;
3144
+ this.fetchedAdDurations.clear();
3145
+ if (this.apiNumberAds && this.apiNumberAds > 1) {
3146
+ numberOfAds = this.apiNumberAds;
3147
+ if (this.config.debugAdTiming) {
3148
+ console.log(
3149
+ `[DEBUG-POD] \u{1F3AC} VOD MODE (FIXED): Using number_ads=${numberOfAds} from API`
3150
+ );
3151
+ }
3152
+ }
3153
+ }
3154
+ if (numberOfAds > 1) {
3155
+ vastTagUrls = this.generateVastUrlsWithCorrelators(
3156
+ this.apiVastTagUrl,
3157
+ numberOfAds
3158
+ );
3159
+ if (this.config.debugAdTiming) {
3160
+ console.log(
3161
+ `[DEBUG-POD] \u{1F504} Generated ${vastTagUrls.length} initial VAST URLs with unique correlators`
3162
+ );
3163
+ }
3164
+ } else {
3165
+ vastTagUrls = [this.apiVastTagUrl];
3166
+ }
3081
3167
  } else if (tags && tags.length > 0) {
3082
3168
  vastTagUrls = tags;
3083
3169
  } else {
@@ -3091,10 +3177,12 @@ var StormcloudVideoPlayer = class {
3091
3177
  this.vastToMediaUrlMap.clear();
3092
3178
  this.preloadedMediaUrls.clear();
3093
3179
  this.preloadingMediaUrls.clear();
3180
+ const currentMuted = this.video.muted;
3181
+ const currentVolume = this.video.volume;
3094
3182
  console.log(
3095
- `[DEBUG-AUDIO] \u{1F4BE} Capturing original state | muted=${this.video.muted}, volume=${this.video.volume}`
3183
+ `[DEBUG-AUDIO] \u{1F4BE} Capturing ORIGINAL state (once) | muted=${currentMuted}, volume=${currentVolume}`
3096
3184
  );
3097
- this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
3185
+ this.ima.updateOriginalMutedState(currentMuted, currentVolume);
3098
3186
  this.inAdBreak = true;
3099
3187
  this.currentAdIndex = 0;
3100
3188
  this.totalAdsInBreak = vastTagUrls.length;
@@ -3308,7 +3396,6 @@ var StormcloudVideoPlayer = class {
3308
3396
  this.clearAdRequestWatchdog();
3309
3397
  this.clearAdFailsafeTimer();
3310
3398
  this.activeAdRequestToken = null;
3311
- this.releaseAdHoldState();
3312
3399
  this.preloadingAdUrls.clear();
3313
3400
  this.vastToMediaUrlMap.clear();
3314
3401
  this.preloadedMediaUrls.clear();
@@ -3325,13 +3412,18 @@ var StormcloudVideoPlayer = class {
3325
3412
  this.totalAdsInBreak = 0;
3326
3413
  this.ima.stop().catch(() => {
3327
3414
  });
3328
- const originalMutedState = this.ima.getOriginalMutedState();
3329
- const originalVolume = typeof this.ima.getOriginalVolume === "function" ? this.ima.getOriginalVolume() : this.video.volume;
3330
- this.video.muted = originalMutedState;
3331
- this.video.volume = originalVolume;
3415
+ const restoredMuted = this.ima.getOriginalMutedState();
3416
+ const restoredVolume = this.ima.getOriginalVolume();
3332
3417
  console.log(
3333
- `[DEBUG-AUDIO] \u{1F50A} Main video restored | muted=${originalMutedState}, volume=${originalVolume}`
3418
+ `[DEBUG-AUDIO] \u{1F50A} Audio restored by IMA | muted=${restoredMuted}, volume=${restoredVolume}`
3334
3419
  );
3420
+ console.log("[DEBUG-LAYER] \u{1F3AC} Layers: Main=visible, Ad=hidden, Placeholder=no");
3421
+ if (this.video.muted !== restoredMuted) {
3422
+ this.video.muted = restoredMuted;
3423
+ }
3424
+ if (Math.abs(this.video.volume - restoredVolume) > 0.01) {
3425
+ this.video.volume = restoredVolume;
3426
+ }
3335
3427
  if (!this.shouldContinueLiveStreamDuringAds() && this.video.paused) {
3336
3428
  console.log("[DEBUG-FLOW] \u25B6\uFE0F Resuming main video playback");
3337
3429
  (_a = this.video.play()) == null ? void 0 : _a.catch((error) => {
@@ -3340,20 +3432,20 @@ var StormcloudVideoPlayer = class {
3340
3432
  }
3341
3433
  }
3342
3434
  handleAdFailure() {
3435
+ console.log("[DEBUG-POD] \u274C handleAdFailure - skipping to next ad or ending break");
3343
3436
  const remaining = this.getRemainingAdMs();
3344
- console.log(
3345
- `[DEBUG-POD] \u274C handleAdFailure | inBreak=${this.inAdBreak}, showAds=${this.showAds}, remaining=${remaining}ms`
3346
- );
3347
- if (remaining > 500 && this.inAdBreak) {
3348
- console.log(
3349
- `[DEBUG-POD] \u23F3 Ad failed but ${remaining}ms remaining - showing placeholder until duration expires`
3350
- );
3351
- this.showAds = true;
3352
- this.ima.showPlaceholder();
3353
- this.enforceAdHoldState();
3354
- return;
3437
+ if (remaining > 500 && this.adPodQueue.length > 0) {
3438
+ const nextPreloaded = this.findNextPreloadedAd();
3439
+ if (nextPreloaded) {
3440
+ this.currentAdIndex++;
3441
+ console.log(`[DEBUG-POD] \u27A1\uFE0F Trying next ad after failure (${this.currentAdIndex}/${this.totalAdsInBreak})`);
3442
+ this.playSingleAd(nextPreloaded).catch(() => {
3443
+ this.handleAdPodComplete();
3444
+ });
3445
+ return;
3446
+ }
3355
3447
  }
3356
- console.log("[DEBUG-POD] \u23F9\uFE0F No remaining time - ending ad break now");
3448
+ console.log("[DEBUG-POD] \u23F9\uFE0F Ending ad break after failure");
3357
3449
  this.handleAdPodComplete();
3358
3450
  }
3359
3451
  startAdRequestWatchdog(token) {
@@ -3426,12 +3518,6 @@ var StormcloudVideoPlayer = class {
3426
3518
  }
3427
3519
  return [b.vastTagUrl];
3428
3520
  }
3429
- logQueuedAdUrls(urls) {
3430
- if (!this.config.debugAdTiming) {
3431
- return;
3432
- }
3433
- console.log("[StormcloudVideoPlayer] ALL ad URLs queued:", urls);
3434
- }
3435
3521
  logAdState(event, extra = {}) {
3436
3522
  if (!this.config.debugAdTiming) {
3437
3523
  return;
@@ -3446,22 +3532,6 @@ var StormcloudVideoPlayer = class {
3446
3532
  ...extra
3447
3533
  });
3448
3534
  }
3449
- enforceAdHoldState() {
3450
- this.video.dataset.stormcloudAdPlaying = "true";
3451
- this.video.muted = true;
3452
- this.video.volume = 0;
3453
- console.log("[DEBUG-LAYER] \u{1F512} Enforced ad hold state (main video muted)");
3454
- if (typeof this.ima.showPlaceholder === "function") {
3455
- this.ima.showPlaceholder();
3456
- }
3457
- }
3458
- releaseAdHoldState() {
3459
- delete this.video.dataset.stormcloudAdPlaying;
3460
- console.log("[DEBUG-LAYER] \u{1F513} Released ad hold state");
3461
- if (typeof this.ima.hidePlaceholder === "function") {
3462
- this.ima.hidePlaceholder();
3463
- }
3464
- }
3465
3535
  async fetchAndParseVastXml(vastTagUrl) {
3466
3536
  try {
3467
3537
  const response = await fetch(vastTagUrl, { mode: "cors" });
@@ -3512,6 +3582,99 @@ var StormcloudVideoPlayer = class {
3512
3582
  }
3513
3583
  return mediaUrls;
3514
3584
  }
3585
+ async fetchVastDuration(vastTagUrl) {
3586
+ var _a;
3587
+ try {
3588
+ const response = await fetch(vastTagUrl, { mode: "cors" });
3589
+ if (!response.ok) {
3590
+ if (this.config.debugAdTiming) {
3591
+ console.warn(
3592
+ `[ADAPTIVE-POD] Failed to fetch VAST: ${response.status}`
3593
+ );
3594
+ }
3595
+ return null;
3596
+ }
3597
+ const xmlText = await response.text();
3598
+ const parser = new DOMParser();
3599
+ const xmlDoc = parser.parseFromString(xmlText, "text/xml");
3600
+ const durationText = (_a = xmlDoc.querySelector("Duration")) == null ? void 0 : _a.textContent;
3601
+ if (!durationText) {
3602
+ if (this.config.debugAdTiming) {
3603
+ console.warn("[ADAPTIVE-POD] No Duration element found in VAST");
3604
+ }
3605
+ return null;
3606
+ }
3607
+ const durationParts = durationText.split(":");
3608
+ const durationSeconds = parseInt(durationParts[0] || "0", 10) * 3600 + parseInt(durationParts[1] || "0", 10) * 60 + parseInt(durationParts[2] || "0", 10);
3609
+ return durationSeconds;
3610
+ } catch (error) {
3611
+ if (this.config.debugAdTiming) {
3612
+ console.warn(
3613
+ `[ADAPTIVE-POD] Error fetching VAST duration from ${vastTagUrl}:`,
3614
+ error
3615
+ );
3616
+ }
3617
+ return null;
3618
+ }
3619
+ }
3620
+ calculateAdditionalAdsNeeded() {
3621
+ if (!this.isAdaptiveMode || this.targetAdBreakDurationMs === null) {
3622
+ return 0;
3623
+ }
3624
+ let totalFetchedDurationMs = 0;
3625
+ for (const duration of this.fetchedAdDurations.values()) {
3626
+ totalFetchedDurationMs += duration * 1e3;
3627
+ }
3628
+ const remainingTimeMs = this.targetAdBreakDurationMs - totalFetchedDurationMs;
3629
+ if (remainingTimeMs <= 0) {
3630
+ if (this.config.debugAdTiming) {
3631
+ console.log(
3632
+ `[ADAPTIVE-POD] \u2705 Target duration reached: ${totalFetchedDurationMs}ms / ${this.targetAdBreakDurationMs}ms`
3633
+ );
3634
+ }
3635
+ return 0;
3636
+ }
3637
+ const fetchedCount = this.fetchedAdDurations.size;
3638
+ const averageDurationMs = fetchedCount > 0 ? totalFetchedDurationMs / fetchedCount : 30 * 1e3;
3639
+ const additionalAds = Math.ceil(remainingTimeMs / averageDurationMs);
3640
+ if (this.config.debugAdTiming) {
3641
+ console.log(
3642
+ `[ADAPTIVE-POD] \u{1F4CA} Need ${additionalAds} more ads | Fetched: ${totalFetchedDurationMs}ms / Target: ${this.targetAdBreakDurationMs}ms | Remaining: ${remainingTimeMs}ms | Avg duration: ${averageDurationMs}ms`
3643
+ );
3644
+ }
3645
+ return additionalAds;
3646
+ }
3647
+ async addAdaptiveAdsToQueue() {
3648
+ if (!this.isAdaptiveMode || !this.apiVastTagUrl) {
3649
+ return;
3650
+ }
3651
+ const additionalAds = this.calculateAdditionalAdsNeeded();
3652
+ if (additionalAds <= 0) {
3653
+ return;
3654
+ }
3655
+ const newUrls = this.generateVastUrlsWithCorrelators(
3656
+ this.apiVastTagUrl,
3657
+ additionalAds
3658
+ );
3659
+ if (this.config.debugAdTiming) {
3660
+ console.log(
3661
+ `[ADAPTIVE-POD] \u{1F504} Adding ${newUrls.length} additional VAST URLs to queue`
3662
+ );
3663
+ }
3664
+ this.adPodAllUrls.push(...newUrls);
3665
+ this.adPodQueue.push(...newUrls);
3666
+ this.totalAdsInBreak += newUrls.length;
3667
+ for (const url of newUrls) {
3668
+ this.preloadSingleAd(url).catch((error) => {
3669
+ if (this.config.debugAdTiming) {
3670
+ console.warn(
3671
+ `[ADAPTIVE-POD] Failed to preload adaptive ad:`,
3672
+ error
3673
+ );
3674
+ }
3675
+ });
3676
+ }
3677
+ }
3515
3678
  async preloadMediaFile(mediaUrl) {
3516
3679
  if (this.preloadedMediaUrls.has(mediaUrl)) {
3517
3680
  return;
@@ -3581,6 +3744,18 @@ var StormcloudVideoPlayer = class {
3581
3744
  async preloadSingleAd(vastTagUrl) {
3582
3745
  if (!vastTagUrl) return;
3583
3746
  try {
3747
+ if (this.isAdaptiveMode && !this.fetchedAdDurations.has(vastTagUrl)) {
3748
+ const duration = await this.fetchVastDuration(vastTagUrl);
3749
+ if (duration !== null) {
3750
+ this.fetchedAdDurations.set(vastTagUrl, duration);
3751
+ if (this.config.debugAdTiming) {
3752
+ console.log(
3753
+ `[ADAPTIVE-POD] \u2713 Fetched ad duration: ${duration}s (${this.fetchedAdDurations.size} ads fetched so far)`
3754
+ );
3755
+ }
3756
+ await this.addAdaptiveAdsToQueue();
3757
+ }
3758
+ }
3584
3759
  if (this.ima.preloadAds && !this.ima.hasPreloadedAd(vastTagUrl)) {
3585
3760
  if (!this.preloadingAdUrls.has(vastTagUrl)) {
3586
3761
  if (this.config.debugAdTiming) {
@@ -3826,7 +4001,6 @@ var StormcloudVideoPlayer = class {
3826
4001
  }
3827
4002
  (_a = this.hls) == null ? void 0 : _a.destroy();
3828
4003
  (_b = this.ima) == null ? void 0 : _b.destroy();
3829
- this.releaseAdHoldState();
3830
4004
  this.preloadingAdUrls.clear();
3831
4005
  this.vastToMediaUrlMap.clear();
3832
4006
  this.preloadedMediaUrls.clear();