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