stormcloud-video-player 0.2.24 → 0.2.26

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.cjs CHANGED
@@ -266,7 +266,10 @@ function supportsFeature(feature) {
266
266
  function createImaController(video, options) {
267
267
  let adPlaying = false;
268
268
  let originalMutedState = false;
269
+ let originalVolume = typeof video.volume === "number" && !Number.isNaN(video.volume) ? Math.max(0, Math.min(1, video.volume)) : 1;
269
270
  const listeners = /* @__PURE__ */ new Map();
271
+ const preloadedVast = /* @__PURE__ */ new Map();
272
+ const preloadingVast = /* @__PURE__ */ new Map();
270
273
  function setAdPlayingFlag(isPlaying) {
271
274
  if (isPlaying) {
272
275
  video.dataset.stormcloudAdPlaying = "true";
@@ -357,16 +360,70 @@ function createImaController(video, options) {
357
360
  let adsLoadedReject;
358
361
  function makeAdsRequest(google, vastTagUrl) {
359
362
  const adsRequest = new google.ima.AdsRequest();
360
- adsRequest.adTagUrl = vastTagUrl;
363
+ const preloadedResponse = preloadedVast.get(vastTagUrl);
364
+ if (preloadedResponse) {
365
+ adsRequest.adsResponse = preloadedResponse;
366
+ console.log(
367
+ "[IMA] Using preloaded VAST response for immediate ad request"
368
+ );
369
+ } else {
370
+ adsRequest.adTagUrl = vastTagUrl;
371
+ }
361
372
  const videoWidth = video.offsetWidth || video.clientWidth || 640;
362
373
  const videoHeight = video.offsetHeight || video.clientHeight || 360;
363
374
  adsRequest.linearAdSlotWidth = videoWidth;
364
375
  adsRequest.linearAdSlotHeight = videoHeight;
365
376
  adsRequest.nonLinearAdSlotWidth = videoWidth;
366
377
  adsRequest.nonLinearAdSlotHeight = videoHeight;
378
+ if (typeof adsRequest.setAdWillAutoPlay === "function") {
379
+ try {
380
+ const willAutoPlay = !video.paused || video.autoplay;
381
+ adsRequest.setAdWillAutoPlay(willAutoPlay);
382
+ } catch (error) {
383
+ console.warn("[IMA] Failed to call setAdWillAutoPlay:", error);
384
+ }
385
+ }
386
+ if (typeof adsRequest.setAdWillPlayMuted === "function") {
387
+ try {
388
+ const willPlayMuted = video.muted || video.volume === 0;
389
+ adsRequest.setAdWillPlayMuted(willPlayMuted);
390
+ } catch (error) {
391
+ console.warn("[IMA] Failed to call setAdWillPlayMuted:", error);
392
+ }
393
+ }
367
394
  adsRequest.vastLoadTimeout = 5e3;
368
395
  console.log(`[IMA] Ads request dimensions: ${videoWidth}x${videoHeight}`);
369
396
  adsLoader.requestAds(adsRequest);
397
+ if (preloadedResponse) {
398
+ preloadedVast.delete(vastTagUrl);
399
+ }
400
+ }
401
+ function ensurePlaceholderContainer() {
402
+ var _a;
403
+ if (adContainerEl) {
404
+ return;
405
+ }
406
+ const container = document.createElement("div");
407
+ container.style.position = "absolute";
408
+ container.style.left = "0";
409
+ container.style.top = "0";
410
+ container.style.right = "0";
411
+ container.style.bottom = "0";
412
+ container.style.display = "none";
413
+ container.style.alignItems = "center";
414
+ container.style.justifyContent = "center";
415
+ container.style.pointerEvents = "none";
416
+ container.style.zIndex = "10";
417
+ container.style.backgroundColor = "#000";
418
+ (_a = video.parentElement) == null ? void 0 : _a.appendChild(container);
419
+ adContainerEl = container;
420
+ }
421
+ async function fetchVastDocument(vastTagUrl) {
422
+ const response = await fetch(vastTagUrl, { mode: "cors" });
423
+ if (!response.ok) {
424
+ throw new Error(`Failed to preload VAST: ${response.status}`);
425
+ }
426
+ return response.text();
370
427
  }
371
428
  function destroyAdsManager() {
372
429
  if (adsManager) {
@@ -382,29 +439,16 @@ function createImaController(video, options) {
382
439
  return {
383
440
  initialize() {
384
441
  ensureImaLoaded().then(() => {
385
- var _a, _b;
442
+ var _a;
386
443
  const google = window.google;
387
- if (!adDisplayContainer) {
388
- const container = document.createElement("div");
389
- container.style.position = "absolute";
390
- container.style.left = "0";
391
- container.style.top = "0";
392
- container.style.right = "0";
393
- container.style.bottom = "0";
394
- container.style.display = "none";
395
- container.style.alignItems = "center";
396
- container.style.justifyContent = "center";
397
- container.style.pointerEvents = "none";
398
- container.style.zIndex = "10";
399
- container.style.backgroundColor = "#000";
400
- (_a = video.parentElement) == null ? void 0 : _a.appendChild(container);
401
- adContainerEl = container;
444
+ ensurePlaceholderContainer();
445
+ if (!adDisplayContainer && adContainerEl) {
402
446
  adDisplayContainer = new google.ima.AdDisplayContainer(
403
- container,
447
+ adContainerEl,
404
448
  video
405
449
  );
406
450
  try {
407
- (_b = adDisplayContainer.initialize) == null ? void 0 : _b.call(adDisplayContainer);
451
+ (_a = adDisplayContainer.initialize) == null ? void 0 : _a.call(adDisplayContainer);
408
452
  } catch {
409
453
  }
410
454
  }
@@ -708,6 +752,32 @@ function createImaController(video, options) {
708
752
  return Promise.reject(error);
709
753
  }
710
754
  },
755
+ async preloadAds(vastTagUrl) {
756
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
757
+ return Promise.resolve();
758
+ }
759
+ if (preloadedVast.has(vastTagUrl)) {
760
+ return Promise.resolve();
761
+ }
762
+ const inflight = preloadingVast.get(vastTagUrl);
763
+ if (inflight) {
764
+ return inflight;
765
+ }
766
+ const preloadPromise = fetchVastDocument(vastTagUrl).then((xml) => {
767
+ preloadedVast.set(vastTagUrl, xml);
768
+ console.log("[IMA] Cached VAST response for preloading:", vastTagUrl);
769
+ }).catch((error) => {
770
+ console.warn("[IMA] Failed to preload VAST response:", error);
771
+ preloadedVast.delete(vastTagUrl);
772
+ }).finally(() => {
773
+ preloadingVast.delete(vastTagUrl);
774
+ });
775
+ preloadingVast.set(vastTagUrl, preloadPromise);
776
+ return preloadPromise;
777
+ },
778
+ hasPreloadedAd(vastTagUrl) {
779
+ return preloadedVast.has(vastTagUrl);
780
+ },
711
781
  async play() {
712
782
  var _a, _b;
713
783
  if (!((_a = window.google) == null ? void 0 : _a.ima) || !adDisplayContainer) {
@@ -726,7 +796,7 @@ function createImaController(video, options) {
726
796
  console.log(`[IMA] Initializing ads manager (${width}x${height})`);
727
797
  adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
728
798
  adPlaying = true;
729
- const adVolume = originalMutedState ? 0 : video.volume;
799
+ const adVolume = originalMutedState ? 0 : originalVolume;
730
800
  try {
731
801
  adsManager.setVolume(adVolume);
732
802
  console.log(`[IMA] Set ad volume to ${adVolume}`);
@@ -768,6 +838,7 @@ function createImaController(video, options) {
768
838
  destroyAdsManager();
769
839
  adPlaying = false;
770
840
  video.muted = originalMutedState;
841
+ video.volume = originalVolume;
771
842
  setAdPlayingFlag(false);
772
843
  if (adContainerEl) {
773
844
  adContainerEl.style.pointerEvents = "none";
@@ -783,6 +854,8 @@ function createImaController(video, options) {
783
854
  adContainerEl = void 0;
784
855
  adDisplayContainer = void 0;
785
856
  adsLoader = void 0;
857
+ preloadedVast.clear();
858
+ preloadingVast.clear();
786
859
  },
787
860
  isAdPlaying() {
788
861
  return adPlaying;
@@ -810,15 +883,20 @@ function createImaController(video, options) {
810
883
  var _a;
811
884
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
812
885
  },
813
- updateOriginalMutedState(muted) {
886
+ updateOriginalMutedState(muted, volume) {
887
+ const nextVolume = typeof volume === "number" && !Number.isNaN(volume) ? Math.max(0, Math.min(1, volume)) : originalVolume;
814
888
  console.log(
815
- `[IMA] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
889
+ `[IMA] updateOriginalMutedState called: { muted: ${originalMutedState} -> ${muted}, volume: ${originalVolume} -> ${nextVolume} }`
816
890
  );
817
891
  originalMutedState = muted;
892
+ originalVolume = nextVolume;
818
893
  },
819
894
  getOriginalMutedState() {
820
895
  return originalMutedState;
821
896
  },
897
+ getOriginalVolume() {
898
+ return originalVolume;
899
+ },
822
900
  setAdVolume(volume) {
823
901
  if (adsManager && adPlaying) {
824
902
  try {
@@ -838,6 +916,19 @@ function createImaController(video, options) {
838
916
  }
839
917
  }
840
918
  return 1;
919
+ },
920
+ showPlaceholder() {
921
+ ensurePlaceholderContainer();
922
+ if (adContainerEl) {
923
+ adContainerEl.style.display = "flex";
924
+ adContainerEl.style.pointerEvents = "auto";
925
+ }
926
+ },
927
+ hidePlaceholder() {
928
+ if (adContainerEl) {
929
+ adContainerEl.style.display = "none";
930
+ adContainerEl.style.pointerEvents = "none";
931
+ }
841
932
  }
842
933
  };
843
934
  }
@@ -847,6 +938,7 @@ var import_hls = __toESM(require("hls.js"), 1);
847
938
  function createHlsAdPlayer(contentVideo, options) {
848
939
  let adPlaying = false;
849
940
  let originalMutedState = false;
941
+ let originalVolume = Math.max(0, Math.min(1, contentVideo.volume || 1));
850
942
  const listeners = /* @__PURE__ */ new Map();
851
943
  const licenseKey = options == null ? void 0 : options.licenseKey;
852
944
  const mainHlsInstance = options == null ? void 0 : options.mainHlsInstance;
@@ -855,6 +947,8 @@ function createHlsAdPlayer(contentVideo, options) {
855
947
  let adContainerEl;
856
948
  let currentAd;
857
949
  let sessionId;
950
+ const preloadedAds = /* @__PURE__ */ new Map();
951
+ const preloadingAds = /* @__PURE__ */ new Map();
858
952
  let trackingFired = {
859
953
  impression: false,
860
954
  start: false,
@@ -1084,6 +1178,19 @@ function createHlsAdPlayer(contentVideo, options) {
1084
1178
  return null;
1085
1179
  }
1086
1180
  }
1181
+ async function fetchAndParseVastAd(vastTagUrl) {
1182
+ const response = await fetch(vastTagUrl);
1183
+ if (!response.ok) {
1184
+ throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1185
+ }
1186
+ const vastXml = await response.text();
1187
+ console.log("[HlsAdPlayer] VAST XML received");
1188
+ console.log(
1189
+ "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1190
+ vastXml.substring(0, 2e3)
1191
+ );
1192
+ return parseVastXml(vastXml);
1193
+ }
1087
1194
  function createAdVideoElement() {
1088
1195
  const video = document.createElement("video");
1089
1196
  video.style.position = "absolute";
@@ -1189,6 +1296,7 @@ function createHlsAdPlayer(contentVideo, options) {
1189
1296
  setAdPlayingFlag(false);
1190
1297
  const previousMutedState = contentVideo.muted;
1191
1298
  contentVideo.muted = originalMutedState;
1299
+ contentVideo.volume = originalMutedState ? 0 : originalVolume;
1192
1300
  console.log(
1193
1301
  `[HlsAdPlayer] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
1194
1302
  );
@@ -1235,17 +1343,17 @@ function createHlsAdPlayer(contentVideo, options) {
1235
1343
  }
1236
1344
  try {
1237
1345
  sessionId = generateSessionId();
1238
- const response = await fetch(vastTagUrl);
1239
- if (!response.ok) {
1240
- throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1346
+ let ad;
1347
+ if (preloadedAds.has(vastTagUrl)) {
1348
+ ad = preloadedAds.get(vastTagUrl);
1349
+ preloadedAds.delete(vastTagUrl);
1350
+ console.log(
1351
+ "[HlsAdPlayer] Using preloaded VAST response:",
1352
+ vastTagUrl
1353
+ );
1354
+ } else {
1355
+ ad = await fetchAndParseVastAd(vastTagUrl);
1241
1356
  }
1242
- const vastXml = await response.text();
1243
- console.log("[HlsAdPlayer] VAST XML received");
1244
- console.log(
1245
- "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1246
- vastXml.substring(0, 2e3)
1247
- );
1248
- const ad = parseVastXml(vastXml);
1249
1357
  if (!ad) {
1250
1358
  console.warn("[HlsAdPlayer] No ads available from VAST response");
1251
1359
  emit("ad_error");
@@ -1264,6 +1372,37 @@ function createHlsAdPlayer(contentVideo, options) {
1264
1372
  return Promise.reject(error);
1265
1373
  }
1266
1374
  },
1375
+ async preloadAds(vastTagUrl) {
1376
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
1377
+ return Promise.resolve();
1378
+ }
1379
+ if (preloadedAds.has(vastTagUrl)) {
1380
+ return Promise.resolve();
1381
+ }
1382
+ const inflight = preloadingAds.get(vastTagUrl);
1383
+ if (inflight) {
1384
+ return inflight;
1385
+ }
1386
+ const preloadPromise = fetchAndParseVastAd(vastTagUrl).then((ad) => {
1387
+ if (ad) {
1388
+ preloadedAds.set(vastTagUrl, ad);
1389
+ console.log(
1390
+ "[HlsAdPlayer] Cached VAST response for preloading:",
1391
+ vastTagUrl
1392
+ );
1393
+ }
1394
+ }).catch((error) => {
1395
+ console.warn("[HlsAdPlayer] Failed to preload VAST response:", error);
1396
+ preloadedAds.delete(vastTagUrl);
1397
+ }).finally(() => {
1398
+ preloadingAds.delete(vastTagUrl);
1399
+ });
1400
+ preloadingAds.set(vastTagUrl, preloadPromise);
1401
+ return preloadPromise;
1402
+ },
1403
+ hasPreloadedAd(vastTagUrl) {
1404
+ return preloadedAds.has(vastTagUrl);
1405
+ },
1267
1406
  async play() {
1268
1407
  if (!currentAd) {
1269
1408
  console.warn(
@@ -1287,6 +1426,10 @@ function createHlsAdPlayer(contentVideo, options) {
1287
1426
  complete: false
1288
1427
  };
1289
1428
  const contentVolume = contentVideo.volume;
1429
+ originalVolume = Math.max(
1430
+ 0,
1431
+ Math.min(1, contentVolume || originalVolume)
1432
+ );
1290
1433
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
1291
1434
  contentVideo.pause();
1292
1435
  console.log("[HlsAdPlayer] Content paused (VOD mode)");
@@ -1299,7 +1442,7 @@ function createHlsAdPlayer(contentVideo, options) {
1299
1442
  adPlaying = true;
1300
1443
  setAdPlayingFlag(true);
1301
1444
  if (adVideoElement) {
1302
- const adVolume = originalMutedState ? 0 : contentVolume;
1445
+ const adVolume = originalMutedState ? 0 : originalVolume;
1303
1446
  adVideoElement.volume = Math.max(0, Math.min(1, adVolume));
1304
1447
  adVideoElement.muted = false;
1305
1448
  console.log(
@@ -1378,6 +1521,7 @@ function createHlsAdPlayer(contentVideo, options) {
1378
1521
  adPlaying = false;
1379
1522
  setAdPlayingFlag(false);
1380
1523
  contentVideo.muted = originalMutedState;
1524
+ contentVideo.volume = originalMutedState ? 0 : originalVolume;
1381
1525
  if (adHls) {
1382
1526
  adHls.destroy();
1383
1527
  adHls = void 0;
@@ -1394,6 +1538,8 @@ function createHlsAdPlayer(contentVideo, options) {
1394
1538
  adContainerEl = void 0;
1395
1539
  currentAd = void 0;
1396
1540
  listeners.clear();
1541
+ preloadedAds.clear();
1542
+ preloadingAds.clear();
1397
1543
  },
1398
1544
  isAdPlaying() {
1399
1545
  return adPlaying;
@@ -1417,15 +1563,20 @@ function createHlsAdPlayer(contentVideo, options) {
1417
1563
  var _a;
1418
1564
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
1419
1565
  },
1420
- updateOriginalMutedState(muted) {
1566
+ updateOriginalMutedState(muted, volume) {
1567
+ const nextVolume = typeof volume === "number" && !Number.isNaN(volume) ? Math.max(0, Math.min(1, volume)) : originalVolume;
1421
1568
  console.log(
1422
- `[HlsAdPlayer] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
1569
+ `[HlsAdPlayer] updateOriginalMutedState called: { muted: ${originalMutedState} -> ${muted}, volume: ${originalVolume} -> ${nextVolume} }`
1423
1570
  );
1424
1571
  originalMutedState = muted;
1572
+ originalVolume = nextVolume;
1425
1573
  },
1426
1574
  getOriginalMutedState() {
1427
1575
  return originalMutedState;
1428
1576
  },
1577
+ getOriginalVolume() {
1578
+ return originalVolume;
1579
+ },
1429
1580
  setAdVolume(volume) {
1430
1581
  if (adVideoElement && adPlaying) {
1431
1582
  adVideoElement.volume = Math.max(0, Math.min(1, volume));
@@ -1436,6 +1587,35 @@ function createHlsAdPlayer(contentVideo, options) {
1436
1587
  return adVideoElement.volume;
1437
1588
  }
1438
1589
  return 1;
1590
+ },
1591
+ showPlaceholder() {
1592
+ var _a;
1593
+ if (!adContainerEl) {
1594
+ const container = document.createElement("div");
1595
+ container.style.position = "absolute";
1596
+ container.style.left = "0";
1597
+ container.style.top = "0";
1598
+ container.style.right = "0";
1599
+ container.style.bottom = "0";
1600
+ container.style.display = "none";
1601
+ container.style.alignItems = "center";
1602
+ container.style.justifyContent = "center";
1603
+ container.style.pointerEvents = "none";
1604
+ container.style.zIndex = "10";
1605
+ container.style.backgroundColor = "#000";
1606
+ (_a = contentVideo.parentElement) == null ? void 0 : _a.appendChild(container);
1607
+ adContainerEl = container;
1608
+ }
1609
+ if (adContainerEl) {
1610
+ adContainerEl.style.display = "flex";
1611
+ adContainerEl.style.pointerEvents = "auto";
1612
+ }
1613
+ },
1614
+ hidePlaceholder() {
1615
+ if (adContainerEl) {
1616
+ adContainerEl.style.display = "none";
1617
+ adContainerEl.style.pointerEvents = "none";
1618
+ }
1439
1619
  }
1440
1620
  };
1441
1621
  }
@@ -1916,6 +2096,8 @@ var StormcloudVideoPlayer = class {
1916
2096
  this.bufferedSegmentsCount = 0;
1917
2097
  this.shouldAutoplayAfterBuffering = false;
1918
2098
  this.hasInitialBufferCompleted = false;
2099
+ this.adPodAllUrls = [];
2100
+ this.preloadingAdUrls = /* @__PURE__ */ new Set();
1919
2101
  initializePolyfills();
1920
2102
  const browserOverrides = getBrowserConfigOverrides();
1921
2103
  this.config = { ...config, ...browserOverrides };
@@ -2175,7 +2357,7 @@ var StormcloudVideoPlayer = class {
2175
2357
  this.video.autoplay = !!this.config.autoplay;
2176
2358
  this.video.muted = !!this.config.muted;
2177
2359
  this.ima.initialize();
2178
- this.ima.updateOriginalMutedState(this.video.muted);
2360
+ this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
2179
2361
  this.ima.on("all_ads_completed", () => {
2180
2362
  if (this.config.debugAdTiming) {
2181
2363
  console.log(
@@ -2216,6 +2398,7 @@ var StormcloudVideoPlayer = class {
2216
2398
  console.log("[StormcloudVideoPlayer] IMA content_pause event received");
2217
2399
  }
2218
2400
  this.clearAdFailsafeTimer();
2401
+ this.enforceAdHoldState();
2219
2402
  });
2220
2403
  this.ima.on("content_resume", () => {
2221
2404
  if (this.config.debugAdTiming) {
@@ -2240,9 +2423,7 @@ var StormcloudVideoPlayer = class {
2240
2423
  if (remaining > 500 && this.adPodQueue.length > 0) {
2241
2424
  const next = this.adPodQueue.shift();
2242
2425
  this.currentAdIndex++;
2243
- this.video.dataset.stormcloudAdPlaying = "true";
2244
- this.video.muted = true;
2245
- this.video.volume = 0;
2426
+ this.enforceAdHoldState();
2246
2427
  if (this.config.debugAdTiming) {
2247
2428
  console.log(
2248
2429
  `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak}) - IMMEDIATELY starting next ad`
@@ -2854,11 +3035,16 @@ var StormcloudVideoPlayer = class {
2854
3035
  return;
2855
3036
  }
2856
3037
  if (vastTagUrls.length > 0) {
3038
+ this.adPodAllUrls = [...vastTagUrls];
3039
+ this.preloadingAdUrls.clear();
3040
+ this.logQueuedAdUrls(this.adPodAllUrls);
2857
3041
  this.inAdBreak = true;
2858
3042
  this.showAds = true;
2859
3043
  this.currentAdIndex = 0;
2860
3044
  this.totalAdsInBreak = vastTagUrls.length;
2861
3045
  this.adPodQueue = [...vastTagUrls];
3046
+ this.enforceAdHoldState();
3047
+ this.preloadUpcomingAds();
2862
3048
  if (this.config.debugAdTiming) {
2863
3049
  console.log(
2864
3050
  `[StormcloudVideoPlayer] Starting ad pod with ${vastTagUrls.length} ads - will play continuously`
@@ -2929,6 +3115,7 @@ var StormcloudVideoPlayer = class {
2929
3115
  const first = tags[0];
2930
3116
  const rest = tags.slice(1);
2931
3117
  this.adPodQueue = rest;
3118
+ this.enforceAdHoldState();
2932
3119
  await this.playSingleAd(first);
2933
3120
  this.inAdBreak = true;
2934
3121
  this.expectedAdBreakDurationMs = remainingMs;
@@ -3042,6 +3229,12 @@ var StormcloudVideoPlayer = class {
3042
3229
  }
3043
3230
  return;
3044
3231
  }
3232
+ const wasPreloaded = this.ima.hasPreloadedAd(vastTagUrl);
3233
+ if (wasPreloaded && this.config.debugAdTiming) {
3234
+ console.log(
3235
+ `[StormcloudVideoPlayer] IMA SDK preloaded this ad already: ${vastTagUrl}`
3236
+ );
3237
+ }
3045
3238
  if (!this.showAds) {
3046
3239
  if (this.config.debugAdTiming) {
3047
3240
  console.log(
@@ -3053,7 +3246,7 @@ var StormcloudVideoPlayer = class {
3053
3246
  }
3054
3247
  );
3055
3248
  }
3056
- this.ima.updateOriginalMutedState(this.video.muted);
3249
+ this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
3057
3250
  } else if (this.config.debugAdTiming) {
3058
3251
  console.log(
3059
3252
  `[StormcloudVideoPlayer] Keeping existing original mute state (currently showing ads)`
@@ -3062,12 +3255,14 @@ var StormcloudVideoPlayer = class {
3062
3255
  this.startAdFailsafeTimer();
3063
3256
  try {
3064
3257
  await this.ima.requestAds(vastTagUrl);
3258
+ this.preloadUpcomingAds();
3065
3259
  try {
3066
3260
  if (this.config.debugAdTiming) {
3067
3261
  console.log(
3068
3262
  "[StormcloudVideoPlayer] Ad request completed, attempting playback"
3069
3263
  );
3070
3264
  }
3265
+ this.enforceAdHoldState();
3071
3266
  await this.ima.play();
3072
3267
  if (this.config.debugAdTiming) {
3073
3268
  console.log(
@@ -3097,6 +3292,8 @@ var StormcloudVideoPlayer = class {
3097
3292
  "[StormcloudVideoPlayer] Handling ad pod completion - resuming content and hiding ad layer"
3098
3293
  );
3099
3294
  }
3295
+ this.releaseAdHoldState();
3296
+ this.preloadingAdUrls.clear();
3100
3297
  this.inAdBreak = false;
3101
3298
  this.expectedAdBreakDurationMs = void 0;
3102
3299
  this.currentAdBreakStartWallClockMs = void 0;
@@ -3104,14 +3301,16 @@ var StormcloudVideoPlayer = class {
3104
3301
  this.clearAdStopTimer();
3105
3302
  this.clearAdFailsafeTimer();
3106
3303
  this.adPodQueue = [];
3304
+ this.adPodAllUrls = [];
3107
3305
  this.showAds = false;
3108
3306
  this.currentAdIndex = 0;
3109
3307
  this.totalAdsInBreak = 0;
3110
3308
  this.ima.stop().catch(() => {
3111
3309
  });
3112
3310
  const originalMutedState = this.ima.getOriginalMutedState();
3311
+ const originalVolume = typeof this.ima.getOriginalVolume === "function" ? this.ima.getOriginalVolume() : this.video.volume;
3113
3312
  this.video.muted = originalMutedState;
3114
- this.video.volume = originalMutedState ? 0 : 1;
3313
+ this.video.volume = originalVolume;
3115
3314
  if (this.config.debugAdTiming) {
3116
3315
  console.log(
3117
3316
  `[StormcloudVideoPlayer] Restored main video - muted: ${originalMutedState}, volume: ${this.video.volume}`
@@ -3184,6 +3383,64 @@ var StormcloudVideoPlayer = class {
3184
3383
  }
3185
3384
  return [b.vastTagUrl];
3186
3385
  }
3386
+ logQueuedAdUrls(urls) {
3387
+ if (!this.config.debugAdTiming) {
3388
+ return;
3389
+ }
3390
+ console.log("[StormcloudVideoPlayer] ALL ad URLs queued:", urls);
3391
+ }
3392
+ enforceAdHoldState() {
3393
+ this.video.dataset.stormcloudAdPlaying = "true";
3394
+ this.video.muted = true;
3395
+ this.video.volume = 0;
3396
+ if (typeof this.ima.showPlaceholder === "function") {
3397
+ this.ima.showPlaceholder();
3398
+ }
3399
+ }
3400
+ releaseAdHoldState() {
3401
+ delete this.video.dataset.stormcloudAdPlaying;
3402
+ if (typeof this.ima.hidePlaceholder === "function") {
3403
+ this.ima.hidePlaceholder();
3404
+ }
3405
+ }
3406
+ preloadUpcomingAds() {
3407
+ if (!this.ima.preloadAds || this.adPodQueue.length === 0) {
3408
+ return;
3409
+ }
3410
+ const upcoming = this.adPodQueue.slice(0, 2);
3411
+ for (const url of upcoming) {
3412
+ if (!url) continue;
3413
+ if (this.ima.hasPreloadedAd(url)) {
3414
+ this.preloadingAdUrls.delete(url);
3415
+ continue;
3416
+ }
3417
+ if (this.preloadingAdUrls.has(url)) {
3418
+ continue;
3419
+ }
3420
+ if (this.config.debugAdTiming) {
3421
+ console.log(
3422
+ `[StormcloudVideoPlayer] Scheduling IMA preload for upcoming ad: ${url}`
3423
+ );
3424
+ }
3425
+ this.preloadingAdUrls.add(url);
3426
+ this.ima.preloadAds(url).then(() => {
3427
+ if (this.config.debugAdTiming) {
3428
+ console.log(
3429
+ `[StormcloudVideoPlayer] IMA preload complete for ad: ${url}`
3430
+ );
3431
+ }
3432
+ }).catch((error) => {
3433
+ if (this.config.debugAdTiming) {
3434
+ console.warn(
3435
+ `[StormcloudVideoPlayer] IMA preload failed for ad: ${url}`,
3436
+ error
3437
+ );
3438
+ }
3439
+ }).finally(() => {
3440
+ this.preloadingAdUrls.delete(url);
3441
+ });
3442
+ }
3443
+ }
3187
3444
  getRemainingAdMs() {
3188
3445
  if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
3189
3446
  return 0;
@@ -3204,7 +3461,7 @@ var StormcloudVideoPlayer = class {
3204
3461
  if (this.ima.isAdPlaying()) {
3205
3462
  const currentPerceptualState = this.isMuted();
3206
3463
  const newMutedState = !currentPerceptualState;
3207
- this.ima.updateOriginalMutedState(newMutedState);
3464
+ this.ima.updateOriginalMutedState(newMutedState, this.video.volume);
3208
3465
  this.ima.setAdVolume(newMutedState ? 0 : 1);
3209
3466
  if (this.config.debugAdTiming) {
3210
3467
  console.log(
@@ -3214,7 +3471,7 @@ var StormcloudVideoPlayer = class {
3214
3471
  }
3215
3472
  } else {
3216
3473
  this.video.muted = !this.video.muted;
3217
- this.ima.updateOriginalMutedState(this.video.muted);
3474
+ this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
3218
3475
  if (this.config.debugAdTiming) {
3219
3476
  console.log("[StormcloudVideoPlayer] Muted:", this.video.muted);
3220
3477
  }
@@ -3286,7 +3543,7 @@ var StormcloudVideoPlayer = class {
3286
3543
  }
3287
3544
  this.video.muted = muted;
3288
3545
  if (adPlaying) {
3289
- this.ima.updateOriginalMutedState(muted);
3546
+ this.ima.updateOriginalMutedState(muted, this.video.volume);
3290
3547
  this.ima.setAdVolume(muted ? 0 : 1);
3291
3548
  if (this.config.debugAdTiming) {
3292
3549
  console.log("[StormcloudVideoPlayer] setMuted applied during ad", {
@@ -3295,7 +3552,7 @@ var StormcloudVideoPlayer = class {
3295
3552
  }
3296
3553
  return;
3297
3554
  }
3298
- this.ima.updateOriginalMutedState(muted);
3555
+ this.ima.updateOriginalMutedState(muted, this.video.volume);
3299
3556
  if (this.config.debugAdTiming) {
3300
3557
  console.log("[StormcloudVideoPlayer] setMuted called:", muted);
3301
3558
  }
@@ -3335,6 +3592,9 @@ var StormcloudVideoPlayer = class {
3335
3592
  }
3336
3593
  (_a = this.hls) == null ? void 0 : _a.destroy();
3337
3594
  (_b = this.ima) == null ? void 0 : _b.destroy();
3595
+ this.releaseAdHoldState();
3596
+ this.preloadingAdUrls.clear();
3597
+ this.adPodAllUrls = [];
3338
3598
  }
3339
3599
  };
3340
3600