stormcloud-video-player 0.2.23 → 0.2.25

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
@@ -39,6 +39,8 @@ interface StormcloudVideoPlayerConfig {
39
39
  interface ImaController {
40
40
  initialize: () => void;
41
41
  requestAds: (vastTagUrl: string) => Promise<void>;
42
+ preloadAds: (vastTagUrl: string) => Promise<void>;
43
+ hasPreloadedAd: (vastTagUrl: string) => boolean;
42
44
  play: () => Promise<void>;
43
45
  stop: () => Promise<void>;
44
46
  destroy: () => void;
@@ -50,6 +52,8 @@ interface ImaController {
50
52
  getOriginalMutedState: () => boolean;
51
53
  setAdVolume: (volume: number) => void;
52
54
  getAdVolume: () => number;
55
+ showPlaceholder: () => void;
56
+ hidePlaceholder: () => void;
53
57
  }
54
58
  interface ImaControllerOptions {
55
59
  maxRetries?: number;
@@ -122,6 +126,8 @@ declare class StormcloudVideoPlayer {
122
126
  private bufferedSegmentsCount;
123
127
  private shouldAutoplayAfterBuffering;
124
128
  private hasInitialBufferCompleted;
129
+ private adPodAllUrls;
130
+ private preloadingAdUrls;
125
131
  constructor(config: StormcloudVideoPlayerConfig);
126
132
  private createAdPlayer;
127
133
  load(): Promise<void>;
@@ -148,6 +154,7 @@ declare class StormcloudVideoPlayer {
148
154
  shouldShowNativeControls(): boolean;
149
155
  private shouldContinueLiveStreamDuringAds;
150
156
  private handleAdStart;
157
+ private playAdPod;
151
158
  private findCurrentOrNextBreak;
152
159
  private onTimeUpdate;
153
160
  private handleMidAdJoin;
@@ -163,6 +170,10 @@ declare class StormcloudVideoPlayer {
163
170
  private startAdFailsafeTimer;
164
171
  private clearAdFailsafeTimer;
165
172
  private selectVastTagsForBreak;
173
+ private logQueuedAdUrls;
174
+ private enforceAdHoldState;
175
+ private releaseAdHoldState;
176
+ private preloadUpcomingAds;
166
177
  private getRemainingAdMs;
167
178
  private findBreakForTime;
168
179
  toggleMute(): void;
package/lib/index.d.ts CHANGED
@@ -39,6 +39,8 @@ interface StormcloudVideoPlayerConfig {
39
39
  interface ImaController {
40
40
  initialize: () => void;
41
41
  requestAds: (vastTagUrl: string) => Promise<void>;
42
+ preloadAds: (vastTagUrl: string) => Promise<void>;
43
+ hasPreloadedAd: (vastTagUrl: string) => boolean;
42
44
  play: () => Promise<void>;
43
45
  stop: () => Promise<void>;
44
46
  destroy: () => void;
@@ -50,6 +52,8 @@ interface ImaController {
50
52
  getOriginalMutedState: () => boolean;
51
53
  setAdVolume: (volume: number) => void;
52
54
  getAdVolume: () => number;
55
+ showPlaceholder: () => void;
56
+ hidePlaceholder: () => void;
53
57
  }
54
58
  interface ImaControllerOptions {
55
59
  maxRetries?: number;
@@ -122,6 +126,8 @@ declare class StormcloudVideoPlayer {
122
126
  private bufferedSegmentsCount;
123
127
  private shouldAutoplayAfterBuffering;
124
128
  private hasInitialBufferCompleted;
129
+ private adPodAllUrls;
130
+ private preloadingAdUrls;
125
131
  constructor(config: StormcloudVideoPlayerConfig);
126
132
  private createAdPlayer;
127
133
  load(): Promise<void>;
@@ -148,6 +154,7 @@ declare class StormcloudVideoPlayer {
148
154
  shouldShowNativeControls(): boolean;
149
155
  private shouldContinueLiveStreamDuringAds;
150
156
  private handleAdStart;
157
+ private playAdPod;
151
158
  private findCurrentOrNextBreak;
152
159
  private onTimeUpdate;
153
160
  private handleMidAdJoin;
@@ -163,6 +170,10 @@ declare class StormcloudVideoPlayer {
163
170
  private startAdFailsafeTimer;
164
171
  private clearAdFailsafeTimer;
165
172
  private selectVastTagsForBreak;
173
+ private logQueuedAdUrls;
174
+ private enforceAdHoldState;
175
+ private releaseAdHoldState;
176
+ private preloadUpcomingAds;
166
177
  private getRemainingAdMs;
167
178
  private findBreakForTime;
168
179
  toggleMute(): void;
package/lib/index.js CHANGED
@@ -198,6 +198,8 @@ function createImaController(video, options) {
198
198
  let adPlaying = false;
199
199
  let originalMutedState = false;
200
200
  const listeners = /* @__PURE__ */ new Map();
201
+ const preloadedVast = /* @__PURE__ */ new Map();
202
+ const preloadingVast = /* @__PURE__ */ new Map();
201
203
  function setAdPlayingFlag(isPlaying) {
202
204
  if (isPlaying) {
203
205
  video.dataset.stormcloudAdPlaying = "true";
@@ -288,7 +290,15 @@ function createImaController(video, options) {
288
290
  let adsLoadedReject;
289
291
  function makeAdsRequest(google, vastTagUrl) {
290
292
  const adsRequest = new google.ima.AdsRequest();
291
- adsRequest.adTagUrl = vastTagUrl;
293
+ const preloadedResponse = preloadedVast.get(vastTagUrl);
294
+ if (preloadedResponse) {
295
+ adsRequest.adsResponse = preloadedResponse;
296
+ console.log(
297
+ "[IMA] Using preloaded VAST response for immediate ad request"
298
+ );
299
+ } else {
300
+ adsRequest.adTagUrl = vastTagUrl;
301
+ }
292
302
  const videoWidth = video.offsetWidth || video.clientWidth || 640;
293
303
  const videoHeight = video.offsetHeight || video.clientHeight || 360;
294
304
  adsRequest.linearAdSlotWidth = videoWidth;
@@ -298,6 +308,36 @@ function createImaController(video, options) {
298
308
  adsRequest.vastLoadTimeout = 5e3;
299
309
  console.log(`[IMA] Ads request dimensions: ${videoWidth}x${videoHeight}`);
300
310
  adsLoader.requestAds(adsRequest);
311
+ if (preloadedResponse) {
312
+ preloadedVast.delete(vastTagUrl);
313
+ }
314
+ }
315
+ function ensurePlaceholderContainer() {
316
+ var _a;
317
+ if (adContainerEl) {
318
+ return;
319
+ }
320
+ const container = document.createElement("div");
321
+ container.style.position = "absolute";
322
+ container.style.left = "0";
323
+ container.style.top = "0";
324
+ container.style.right = "0";
325
+ container.style.bottom = "0";
326
+ container.style.display = "none";
327
+ container.style.alignItems = "center";
328
+ container.style.justifyContent = "center";
329
+ container.style.pointerEvents = "none";
330
+ container.style.zIndex = "10";
331
+ container.style.backgroundColor = "#000";
332
+ (_a = video.parentElement) == null ? void 0 : _a.appendChild(container);
333
+ adContainerEl = container;
334
+ }
335
+ async function fetchVastDocument(vastTagUrl) {
336
+ const response = await fetch(vastTagUrl, { mode: "cors" });
337
+ if (!response.ok) {
338
+ throw new Error(`Failed to preload VAST: ${response.status}`);
339
+ }
340
+ return response.text();
301
341
  }
302
342
  function destroyAdsManager() {
303
343
  if (adsManager) {
@@ -313,29 +353,16 @@ function createImaController(video, options) {
313
353
  return {
314
354
  initialize() {
315
355
  ensureImaLoaded().then(() => {
316
- var _a, _b;
356
+ var _a;
317
357
  const google = window.google;
318
- if (!adDisplayContainer) {
319
- const container = document.createElement("div");
320
- container.style.position = "absolute";
321
- container.style.left = "0";
322
- container.style.top = "0";
323
- container.style.right = "0";
324
- container.style.bottom = "0";
325
- container.style.display = "none";
326
- container.style.alignItems = "center";
327
- container.style.justifyContent = "center";
328
- container.style.pointerEvents = "none";
329
- container.style.zIndex = "10";
330
- container.style.backgroundColor = "#000";
331
- (_a = video.parentElement) == null ? void 0 : _a.appendChild(container);
332
- adContainerEl = container;
358
+ ensurePlaceholderContainer();
359
+ if (!adDisplayContainer && adContainerEl) {
333
360
  adDisplayContainer = new google.ima.AdDisplayContainer(
334
- container,
361
+ adContainerEl,
335
362
  video
336
363
  );
337
364
  try {
338
- (_b = adDisplayContainer.initialize) == null ? void 0 : _b.call(adDisplayContainer);
365
+ (_a = adDisplayContainer.initialize) == null ? void 0 : _a.call(adDisplayContainer);
339
366
  } catch {
340
367
  }
341
368
  }
@@ -437,9 +464,13 @@ function createImaController(video, options) {
437
464
  adsLoader.addEventListener(
438
465
  google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
439
466
  (evt) => {
440
- console.log("[IMA] Ads manager loaded");
467
+ console.log(
468
+ "[IMA] Ads manager loaded - enabling preloading for continuous playback"
469
+ );
441
470
  try {
442
- adsManager = evt.getAdsManager(video);
471
+ const adsRenderingSettings = new google.ima.AdsRenderingSettings();
472
+ adsRenderingSettings.enablePreloading = true;
473
+ adsManager = evt.getAdsManager(video, adsRenderingSettings);
443
474
  const AdEvent = google.ima.AdEvent.Type;
444
475
  const AdErrorEvent = google.ima.AdErrorEvent.Type;
445
476
  adsManager.addEventListener(
@@ -635,6 +666,32 @@ function createImaController(video, options) {
635
666
  return Promise.reject(error);
636
667
  }
637
668
  },
669
+ async preloadAds(vastTagUrl) {
670
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
671
+ return Promise.resolve();
672
+ }
673
+ if (preloadedVast.has(vastTagUrl)) {
674
+ return Promise.resolve();
675
+ }
676
+ const inflight = preloadingVast.get(vastTagUrl);
677
+ if (inflight) {
678
+ return inflight;
679
+ }
680
+ const preloadPromise = fetchVastDocument(vastTagUrl).then((xml) => {
681
+ preloadedVast.set(vastTagUrl, xml);
682
+ console.log("[IMA] Cached VAST response for preloading:", vastTagUrl);
683
+ }).catch((error) => {
684
+ console.warn("[IMA] Failed to preload VAST response:", error);
685
+ preloadedVast.delete(vastTagUrl);
686
+ }).finally(() => {
687
+ preloadingVast.delete(vastTagUrl);
688
+ });
689
+ preloadingVast.set(vastTagUrl, preloadPromise);
690
+ return preloadPromise;
691
+ },
692
+ hasPreloadedAd(vastTagUrl) {
693
+ return preloadedVast.has(vastTagUrl);
694
+ },
638
695
  async play() {
639
696
  var _a, _b;
640
697
  if (!((_a = window.google) == null ? void 0 : _a.ima) || !adDisplayContainer) {
@@ -710,6 +767,8 @@ function createImaController(video, options) {
710
767
  adContainerEl = void 0;
711
768
  adDisplayContainer = void 0;
712
769
  adsLoader = void 0;
770
+ preloadedVast.clear();
771
+ preloadingVast.clear();
713
772
  },
714
773
  isAdPlaying() {
715
774
  return adPlaying;
@@ -765,6 +824,19 @@ function createImaController(video, options) {
765
824
  }
766
825
  }
767
826
  return 1;
827
+ },
828
+ showPlaceholder() {
829
+ ensurePlaceholderContainer();
830
+ if (adContainerEl) {
831
+ adContainerEl.style.display = "flex";
832
+ adContainerEl.style.pointerEvents = "auto";
833
+ }
834
+ },
835
+ hidePlaceholder() {
836
+ if (adContainerEl) {
837
+ adContainerEl.style.display = "none";
838
+ adContainerEl.style.pointerEvents = "none";
839
+ }
768
840
  }
769
841
  };
770
842
  }
@@ -782,6 +854,8 @@ function createHlsAdPlayer(contentVideo, options) {
782
854
  let adContainerEl;
783
855
  let currentAd;
784
856
  let sessionId;
857
+ const preloadedAds = /* @__PURE__ */ new Map();
858
+ const preloadingAds = /* @__PURE__ */ new Map();
785
859
  let trackingFired = {
786
860
  impression: false,
787
861
  start: false,
@@ -1011,6 +1085,19 @@ function createHlsAdPlayer(contentVideo, options) {
1011
1085
  return null;
1012
1086
  }
1013
1087
  }
1088
+ async function fetchAndParseVastAd(vastTagUrl) {
1089
+ const response = await fetch(vastTagUrl);
1090
+ if (!response.ok) {
1091
+ throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1092
+ }
1093
+ const vastXml = await response.text();
1094
+ console.log("[HlsAdPlayer] VAST XML received");
1095
+ console.log(
1096
+ "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1097
+ vastXml.substring(0, 2e3)
1098
+ );
1099
+ return parseVastXml(vastXml);
1100
+ }
1014
1101
  function createAdVideoElement() {
1015
1102
  const video = document.createElement("video");
1016
1103
  video.style.position = "absolute";
@@ -1162,17 +1249,17 @@ function createHlsAdPlayer(contentVideo, options) {
1162
1249
  }
1163
1250
  try {
1164
1251
  sessionId = generateSessionId();
1165
- const response = await fetch(vastTagUrl);
1166
- if (!response.ok) {
1167
- throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1252
+ let ad;
1253
+ if (preloadedAds.has(vastTagUrl)) {
1254
+ ad = preloadedAds.get(vastTagUrl);
1255
+ preloadedAds.delete(vastTagUrl);
1256
+ console.log(
1257
+ "[HlsAdPlayer] Using preloaded VAST response:",
1258
+ vastTagUrl
1259
+ );
1260
+ } else {
1261
+ ad = await fetchAndParseVastAd(vastTagUrl);
1168
1262
  }
1169
- const vastXml = await response.text();
1170
- console.log("[HlsAdPlayer] VAST XML received");
1171
- console.log(
1172
- "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1173
- vastXml.substring(0, 2e3)
1174
- );
1175
- const ad = parseVastXml(vastXml);
1176
1263
  if (!ad) {
1177
1264
  console.warn("[HlsAdPlayer] No ads available from VAST response");
1178
1265
  emit("ad_error");
@@ -1191,6 +1278,37 @@ function createHlsAdPlayer(contentVideo, options) {
1191
1278
  return Promise.reject(error);
1192
1279
  }
1193
1280
  },
1281
+ async preloadAds(vastTagUrl) {
1282
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
1283
+ return Promise.resolve();
1284
+ }
1285
+ if (preloadedAds.has(vastTagUrl)) {
1286
+ return Promise.resolve();
1287
+ }
1288
+ const inflight = preloadingAds.get(vastTagUrl);
1289
+ if (inflight) {
1290
+ return inflight;
1291
+ }
1292
+ const preloadPromise = fetchAndParseVastAd(vastTagUrl).then((ad) => {
1293
+ if (ad) {
1294
+ preloadedAds.set(vastTagUrl, ad);
1295
+ console.log(
1296
+ "[HlsAdPlayer] Cached VAST response for preloading:",
1297
+ vastTagUrl
1298
+ );
1299
+ }
1300
+ }).catch((error) => {
1301
+ console.warn("[HlsAdPlayer] Failed to preload VAST response:", error);
1302
+ preloadedAds.delete(vastTagUrl);
1303
+ }).finally(() => {
1304
+ preloadingAds.delete(vastTagUrl);
1305
+ });
1306
+ preloadingAds.set(vastTagUrl, preloadPromise);
1307
+ return preloadPromise;
1308
+ },
1309
+ hasPreloadedAd(vastTagUrl) {
1310
+ return preloadedAds.has(vastTagUrl);
1311
+ },
1194
1312
  async play() {
1195
1313
  if (!currentAd) {
1196
1314
  console.warn(
@@ -1321,6 +1439,8 @@ function createHlsAdPlayer(contentVideo, options) {
1321
1439
  adContainerEl = void 0;
1322
1440
  currentAd = void 0;
1323
1441
  listeners.clear();
1442
+ preloadedAds.clear();
1443
+ preloadingAds.clear();
1324
1444
  },
1325
1445
  isAdPlaying() {
1326
1446
  return adPlaying;
@@ -1363,6 +1483,35 @@ function createHlsAdPlayer(contentVideo, options) {
1363
1483
  return adVideoElement.volume;
1364
1484
  }
1365
1485
  return 1;
1486
+ },
1487
+ showPlaceholder() {
1488
+ var _a;
1489
+ if (!adContainerEl) {
1490
+ const container = document.createElement("div");
1491
+ container.style.position = "absolute";
1492
+ container.style.left = "0";
1493
+ container.style.top = "0";
1494
+ container.style.right = "0";
1495
+ container.style.bottom = "0";
1496
+ container.style.display = "none";
1497
+ container.style.alignItems = "center";
1498
+ container.style.justifyContent = "center";
1499
+ container.style.pointerEvents = "none";
1500
+ container.style.zIndex = "10";
1501
+ container.style.backgroundColor = "#000";
1502
+ (_a = contentVideo.parentElement) == null ? void 0 : _a.appendChild(container);
1503
+ adContainerEl = container;
1504
+ }
1505
+ if (adContainerEl) {
1506
+ adContainerEl.style.display = "flex";
1507
+ adContainerEl.style.pointerEvents = "auto";
1508
+ }
1509
+ },
1510
+ hidePlaceholder() {
1511
+ if (adContainerEl) {
1512
+ adContainerEl.style.display = "none";
1513
+ adContainerEl.style.pointerEvents = "none";
1514
+ }
1366
1515
  }
1367
1516
  };
1368
1517
  }
@@ -1843,6 +1992,8 @@ var StormcloudVideoPlayer = class {
1843
1992
  this.bufferedSegmentsCount = 0;
1844
1993
  this.shouldAutoplayAfterBuffering = false;
1845
1994
  this.hasInitialBufferCompleted = false;
1995
+ this.adPodAllUrls = [];
1996
+ this.preloadingAdUrls = /* @__PURE__ */ new Set();
1846
1997
  initializePolyfills();
1847
1998
  const browserOverrides = getBrowserConfigOverrides();
1848
1999
  this.config = { ...config, ...browserOverrides };
@@ -2143,6 +2294,7 @@ var StormcloudVideoPlayer = class {
2143
2294
  console.log("[StormcloudVideoPlayer] IMA content_pause event received");
2144
2295
  }
2145
2296
  this.clearAdFailsafeTimer();
2297
+ this.enforceAdHoldState();
2146
2298
  });
2147
2299
  this.ima.on("content_resume", () => {
2148
2300
  if (this.config.debugAdTiming) {
@@ -2167,12 +2319,10 @@ var StormcloudVideoPlayer = class {
2167
2319
  if (remaining > 500 && this.adPodQueue.length > 0) {
2168
2320
  const next = this.adPodQueue.shift();
2169
2321
  this.currentAdIndex++;
2170
- this.video.dataset.stormcloudAdPlaying = "true";
2171
- this.video.muted = true;
2172
- this.video.volume = 0;
2322
+ this.enforceAdHoldState();
2173
2323
  if (this.config.debugAdTiming) {
2174
2324
  console.log(
2175
- `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak}) - main video stays muted, ad layer stays visible`
2325
+ `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak}) - IMMEDIATELY starting next ad`
2176
2326
  );
2177
2327
  }
2178
2328
  this.playSingleAd(next).catch(() => {
@@ -2757,25 +2907,21 @@ var StormcloudVideoPlayer = class {
2757
2907
  this.video.currentTime * 1e3
2758
2908
  );
2759
2909
  const tags = this.selectVastTagsForBreak(scheduled);
2760
- let vastTagUrl;
2910
+ let vastTagUrls = [];
2761
2911
  if (this.apiVastTagUrl) {
2762
- vastTagUrl = this.apiVastTagUrl;
2763
- this.adPodQueue = [];
2764
- this.currentAdIndex = 0;
2765
- this.totalAdsInBreak = 1;
2912
+ vastTagUrls = [this.apiVastTagUrl];
2766
2913
  if (this.config.debugAdTiming) {
2767
- console.log("[StormcloudVideoPlayer] Using VAST endpoint:", vastTagUrl);
2914
+ console.log(
2915
+ "[StormcloudVideoPlayer] Using VAST endpoint:",
2916
+ this.apiVastTagUrl
2917
+ );
2768
2918
  }
2769
2919
  } else if (tags && tags.length > 0) {
2770
- vastTagUrl = tags[0];
2771
- const rest = tags.slice(1);
2772
- this.adPodQueue = rest;
2773
- this.currentAdIndex = 0;
2774
- this.totalAdsInBreak = tags.length;
2920
+ vastTagUrls = tags;
2775
2921
  if (this.config.debugAdTiming) {
2776
2922
  console.log(
2777
- "[StormcloudVideoPlayer] Using scheduled VAST tag:",
2778
- vastTagUrl
2923
+ "[StormcloudVideoPlayer] Using scheduled VAST tags (count: " + tags.length + "):",
2924
+ tags
2779
2925
  );
2780
2926
  }
2781
2927
  } else {
@@ -2784,16 +2930,28 @@ var StormcloudVideoPlayer = class {
2784
2930
  }
2785
2931
  return;
2786
2932
  }
2787
- if (vastTagUrl) {
2933
+ if (vastTagUrls.length > 0) {
2934
+ this.adPodAllUrls = [...vastTagUrls];
2935
+ this.preloadingAdUrls.clear();
2936
+ this.logQueuedAdUrls(this.adPodAllUrls);
2788
2937
  this.inAdBreak = true;
2789
2938
  this.showAds = true;
2790
- this.currentAdIndex++;
2939
+ this.currentAdIndex = 0;
2940
+ this.totalAdsInBreak = vastTagUrls.length;
2941
+ this.adPodQueue = [...vastTagUrls];
2942
+ this.enforceAdHoldState();
2943
+ this.preloadUpcomingAds();
2944
+ if (this.config.debugAdTiming) {
2945
+ console.log(
2946
+ `[StormcloudVideoPlayer] Starting ad pod with ${vastTagUrls.length} ads - will play continuously`
2947
+ );
2948
+ }
2791
2949
  try {
2792
- await this.playSingleAd(vastTagUrl);
2950
+ await this.playAdPod();
2793
2951
  } catch (error) {
2794
2952
  if (this.config.debugAdTiming) {
2795
2953
  console.error(
2796
- "[StormcloudVideoPlayer] Ad playback failed in handleAdStart:",
2954
+ "[StormcloudVideoPlayer] Ad pod playback failed:",
2797
2955
  error
2798
2956
  );
2799
2957
  }
@@ -2806,6 +2964,22 @@ var StormcloudVideoPlayer = class {
2806
2964
  this.scheduleAdStopCountdown(this.expectedAdBreakDurationMs);
2807
2965
  }
2808
2966
  }
2967
+ async playAdPod() {
2968
+ if (this.adPodQueue.length === 0) {
2969
+ if (this.config.debugAdTiming) {
2970
+ console.log("[StormcloudVideoPlayer] No ads in pod to play");
2971
+ }
2972
+ return;
2973
+ }
2974
+ const firstAd = this.adPodQueue.shift();
2975
+ this.currentAdIndex++;
2976
+ if (this.config.debugAdTiming) {
2977
+ console.log(
2978
+ `[StormcloudVideoPlayer] Playing ad ${this.currentAdIndex}/${this.totalAdsInBreak}`
2979
+ );
2980
+ }
2981
+ await this.playSingleAd(firstAd);
2982
+ }
2809
2983
  findCurrentOrNextBreak(nowMs) {
2810
2984
  var _a;
2811
2985
  const schedule = [];
@@ -2837,6 +3011,7 @@ var StormcloudVideoPlayer = class {
2837
3011
  const first = tags[0];
2838
3012
  const rest = tags.slice(1);
2839
3013
  this.adPodQueue = rest;
3014
+ this.enforceAdHoldState();
2840
3015
  await this.playSingleAd(first);
2841
3016
  this.inAdBreak = true;
2842
3017
  this.expectedAdBreakDurationMs = remainingMs;
@@ -2950,6 +3125,12 @@ var StormcloudVideoPlayer = class {
2950
3125
  }
2951
3126
  return;
2952
3127
  }
3128
+ const wasPreloaded = this.ima.hasPreloadedAd(vastTagUrl);
3129
+ if (wasPreloaded && this.config.debugAdTiming) {
3130
+ console.log(
3131
+ `[StormcloudVideoPlayer] IMA SDK preloaded this ad already: ${vastTagUrl}`
3132
+ );
3133
+ }
2953
3134
  if (!this.showAds) {
2954
3135
  if (this.config.debugAdTiming) {
2955
3136
  console.log(
@@ -2970,12 +3151,14 @@ var StormcloudVideoPlayer = class {
2970
3151
  this.startAdFailsafeTimer();
2971
3152
  try {
2972
3153
  await this.ima.requestAds(vastTagUrl);
3154
+ this.preloadUpcomingAds();
2973
3155
  try {
2974
3156
  if (this.config.debugAdTiming) {
2975
3157
  console.log(
2976
3158
  "[StormcloudVideoPlayer] Ad request completed, attempting playback"
2977
3159
  );
2978
3160
  }
3161
+ this.enforceAdHoldState();
2979
3162
  await this.ima.play();
2980
3163
  if (this.config.debugAdTiming) {
2981
3164
  console.log(
@@ -3005,6 +3188,8 @@ var StormcloudVideoPlayer = class {
3005
3188
  "[StormcloudVideoPlayer] Handling ad pod completion - resuming content and hiding ad layer"
3006
3189
  );
3007
3190
  }
3191
+ this.releaseAdHoldState();
3192
+ this.preloadingAdUrls.clear();
3008
3193
  this.inAdBreak = false;
3009
3194
  this.expectedAdBreakDurationMs = void 0;
3010
3195
  this.currentAdBreakStartWallClockMs = void 0;
@@ -3012,6 +3197,7 @@ var StormcloudVideoPlayer = class {
3012
3197
  this.clearAdStopTimer();
3013
3198
  this.clearAdFailsafeTimer();
3014
3199
  this.adPodQueue = [];
3200
+ this.adPodAllUrls = [];
3015
3201
  this.showAds = false;
3016
3202
  this.currentAdIndex = 0;
3017
3203
  this.totalAdsInBreak = 0;
@@ -3092,6 +3278,64 @@ var StormcloudVideoPlayer = class {
3092
3278
  }
3093
3279
  return [b.vastTagUrl];
3094
3280
  }
3281
+ logQueuedAdUrls(urls) {
3282
+ if (!this.config.debugAdTiming) {
3283
+ return;
3284
+ }
3285
+ console.log("[StormcloudVideoPlayer] ALL ad URLs queued:", urls);
3286
+ }
3287
+ enforceAdHoldState() {
3288
+ this.video.dataset.stormcloudAdPlaying = "true";
3289
+ this.video.muted = true;
3290
+ this.video.volume = 0;
3291
+ if (typeof this.ima.showPlaceholder === "function") {
3292
+ this.ima.showPlaceholder();
3293
+ }
3294
+ }
3295
+ releaseAdHoldState() {
3296
+ delete this.video.dataset.stormcloudAdPlaying;
3297
+ if (typeof this.ima.hidePlaceholder === "function") {
3298
+ this.ima.hidePlaceholder();
3299
+ }
3300
+ }
3301
+ preloadUpcomingAds() {
3302
+ if (!this.ima.preloadAds || this.adPodQueue.length === 0) {
3303
+ return;
3304
+ }
3305
+ const upcoming = this.adPodQueue.slice(0, 2);
3306
+ for (const url of upcoming) {
3307
+ if (!url) continue;
3308
+ if (this.ima.hasPreloadedAd(url)) {
3309
+ this.preloadingAdUrls.delete(url);
3310
+ continue;
3311
+ }
3312
+ if (this.preloadingAdUrls.has(url)) {
3313
+ continue;
3314
+ }
3315
+ if (this.config.debugAdTiming) {
3316
+ console.log(
3317
+ `[StormcloudVideoPlayer] Scheduling IMA preload for upcoming ad: ${url}`
3318
+ );
3319
+ }
3320
+ this.preloadingAdUrls.add(url);
3321
+ this.ima.preloadAds(url).then(() => {
3322
+ if (this.config.debugAdTiming) {
3323
+ console.log(
3324
+ `[StormcloudVideoPlayer] IMA preload complete for ad: ${url}`
3325
+ );
3326
+ }
3327
+ }).catch((error) => {
3328
+ if (this.config.debugAdTiming) {
3329
+ console.warn(
3330
+ `[StormcloudVideoPlayer] IMA preload failed for ad: ${url}`,
3331
+ error
3332
+ );
3333
+ }
3334
+ }).finally(() => {
3335
+ this.preloadingAdUrls.delete(url);
3336
+ });
3337
+ }
3338
+ }
3095
3339
  getRemainingAdMs() {
3096
3340
  if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
3097
3341
  return 0;
@@ -3243,6 +3487,9 @@ var StormcloudVideoPlayer = class {
3243
3487
  }
3244
3488
  (_a = this.hls) == null ? void 0 : _a.destroy();
3245
3489
  (_b = this.ima) == null ? void 0 : _b.destroy();
3490
+ this.releaseAdHoldState();
3491
+ this.preloadingAdUrls.clear();
3492
+ this.adPodAllUrls = [];
3246
3493
  }
3247
3494
  };
3248
3495