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