stormcloud-video-player 0.2.24 → 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.cjs CHANGED
@@ -267,6 +267,8 @@ function createImaController(video, options) {
267
267
  let adPlaying = false;
268
268
  let originalMutedState = false;
269
269
  const listeners = /* @__PURE__ */ new Map();
270
+ const preloadedVast = /* @__PURE__ */ new Map();
271
+ const preloadingVast = /* @__PURE__ */ new Map();
270
272
  function setAdPlayingFlag(isPlaying) {
271
273
  if (isPlaying) {
272
274
  video.dataset.stormcloudAdPlaying = "true";
@@ -357,7 +359,15 @@ function createImaController(video, options) {
357
359
  let adsLoadedReject;
358
360
  function makeAdsRequest(google, vastTagUrl) {
359
361
  const adsRequest = new google.ima.AdsRequest();
360
- adsRequest.adTagUrl = vastTagUrl;
362
+ const preloadedResponse = preloadedVast.get(vastTagUrl);
363
+ if (preloadedResponse) {
364
+ adsRequest.adsResponse = preloadedResponse;
365
+ console.log(
366
+ "[IMA] Using preloaded VAST response for immediate ad request"
367
+ );
368
+ } else {
369
+ adsRequest.adTagUrl = vastTagUrl;
370
+ }
361
371
  const videoWidth = video.offsetWidth || video.clientWidth || 640;
362
372
  const videoHeight = video.offsetHeight || video.clientHeight || 360;
363
373
  adsRequest.linearAdSlotWidth = videoWidth;
@@ -367,6 +377,36 @@ function createImaController(video, options) {
367
377
  adsRequest.vastLoadTimeout = 5e3;
368
378
  console.log(`[IMA] Ads request dimensions: ${videoWidth}x${videoHeight}`);
369
379
  adsLoader.requestAds(adsRequest);
380
+ if (preloadedResponse) {
381
+ preloadedVast.delete(vastTagUrl);
382
+ }
383
+ }
384
+ function ensurePlaceholderContainer() {
385
+ var _a;
386
+ if (adContainerEl) {
387
+ return;
388
+ }
389
+ const container = document.createElement("div");
390
+ container.style.position = "absolute";
391
+ container.style.left = "0";
392
+ container.style.top = "0";
393
+ container.style.right = "0";
394
+ container.style.bottom = "0";
395
+ container.style.display = "none";
396
+ container.style.alignItems = "center";
397
+ container.style.justifyContent = "center";
398
+ container.style.pointerEvents = "none";
399
+ container.style.zIndex = "10";
400
+ container.style.backgroundColor = "#000";
401
+ (_a = video.parentElement) == null ? void 0 : _a.appendChild(container);
402
+ adContainerEl = container;
403
+ }
404
+ async function fetchVastDocument(vastTagUrl) {
405
+ const response = await fetch(vastTagUrl, { mode: "cors" });
406
+ if (!response.ok) {
407
+ throw new Error(`Failed to preload VAST: ${response.status}`);
408
+ }
409
+ return response.text();
370
410
  }
371
411
  function destroyAdsManager() {
372
412
  if (adsManager) {
@@ -382,29 +422,16 @@ function createImaController(video, options) {
382
422
  return {
383
423
  initialize() {
384
424
  ensureImaLoaded().then(() => {
385
- var _a, _b;
425
+ var _a;
386
426
  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;
427
+ ensurePlaceholderContainer();
428
+ if (!adDisplayContainer && adContainerEl) {
402
429
  adDisplayContainer = new google.ima.AdDisplayContainer(
403
- container,
430
+ adContainerEl,
404
431
  video
405
432
  );
406
433
  try {
407
- (_b = adDisplayContainer.initialize) == null ? void 0 : _b.call(adDisplayContainer);
434
+ (_a = adDisplayContainer.initialize) == null ? void 0 : _a.call(adDisplayContainer);
408
435
  } catch {
409
436
  }
410
437
  }
@@ -708,6 +735,32 @@ function createImaController(video, options) {
708
735
  return Promise.reject(error);
709
736
  }
710
737
  },
738
+ async preloadAds(vastTagUrl) {
739
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
740
+ return Promise.resolve();
741
+ }
742
+ if (preloadedVast.has(vastTagUrl)) {
743
+ return Promise.resolve();
744
+ }
745
+ const inflight = preloadingVast.get(vastTagUrl);
746
+ if (inflight) {
747
+ return inflight;
748
+ }
749
+ const preloadPromise = fetchVastDocument(vastTagUrl).then((xml) => {
750
+ preloadedVast.set(vastTagUrl, xml);
751
+ console.log("[IMA] Cached VAST response for preloading:", vastTagUrl);
752
+ }).catch((error) => {
753
+ console.warn("[IMA] Failed to preload VAST response:", error);
754
+ preloadedVast.delete(vastTagUrl);
755
+ }).finally(() => {
756
+ preloadingVast.delete(vastTagUrl);
757
+ });
758
+ preloadingVast.set(vastTagUrl, preloadPromise);
759
+ return preloadPromise;
760
+ },
761
+ hasPreloadedAd(vastTagUrl) {
762
+ return preloadedVast.has(vastTagUrl);
763
+ },
711
764
  async play() {
712
765
  var _a, _b;
713
766
  if (!((_a = window.google) == null ? void 0 : _a.ima) || !adDisplayContainer) {
@@ -783,6 +836,8 @@ function createImaController(video, options) {
783
836
  adContainerEl = void 0;
784
837
  adDisplayContainer = void 0;
785
838
  adsLoader = void 0;
839
+ preloadedVast.clear();
840
+ preloadingVast.clear();
786
841
  },
787
842
  isAdPlaying() {
788
843
  return adPlaying;
@@ -838,6 +893,19 @@ function createImaController(video, options) {
838
893
  }
839
894
  }
840
895
  return 1;
896
+ },
897
+ showPlaceholder() {
898
+ ensurePlaceholderContainer();
899
+ if (adContainerEl) {
900
+ adContainerEl.style.display = "flex";
901
+ adContainerEl.style.pointerEvents = "auto";
902
+ }
903
+ },
904
+ hidePlaceholder() {
905
+ if (adContainerEl) {
906
+ adContainerEl.style.display = "none";
907
+ adContainerEl.style.pointerEvents = "none";
908
+ }
841
909
  }
842
910
  };
843
911
  }
@@ -855,6 +923,8 @@ function createHlsAdPlayer(contentVideo, options) {
855
923
  let adContainerEl;
856
924
  let currentAd;
857
925
  let sessionId;
926
+ const preloadedAds = /* @__PURE__ */ new Map();
927
+ const preloadingAds = /* @__PURE__ */ new Map();
858
928
  let trackingFired = {
859
929
  impression: false,
860
930
  start: false,
@@ -1084,6 +1154,19 @@ function createHlsAdPlayer(contentVideo, options) {
1084
1154
  return null;
1085
1155
  }
1086
1156
  }
1157
+ async function fetchAndParseVastAd(vastTagUrl) {
1158
+ const response = await fetch(vastTagUrl);
1159
+ if (!response.ok) {
1160
+ throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1161
+ }
1162
+ const vastXml = await response.text();
1163
+ console.log("[HlsAdPlayer] VAST XML received");
1164
+ console.log(
1165
+ "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1166
+ vastXml.substring(0, 2e3)
1167
+ );
1168
+ return parseVastXml(vastXml);
1169
+ }
1087
1170
  function createAdVideoElement() {
1088
1171
  const video = document.createElement("video");
1089
1172
  video.style.position = "absolute";
@@ -1235,17 +1318,17 @@ function createHlsAdPlayer(contentVideo, options) {
1235
1318
  }
1236
1319
  try {
1237
1320
  sessionId = generateSessionId();
1238
- const response = await fetch(vastTagUrl);
1239
- if (!response.ok) {
1240
- throw new Error(`Failed to fetch VAST: ${response.statusText}`);
1321
+ let ad;
1322
+ if (preloadedAds.has(vastTagUrl)) {
1323
+ ad = preloadedAds.get(vastTagUrl);
1324
+ preloadedAds.delete(vastTagUrl);
1325
+ console.log(
1326
+ "[HlsAdPlayer] Using preloaded VAST response:",
1327
+ vastTagUrl
1328
+ );
1329
+ } else {
1330
+ ad = await fetchAndParseVastAd(vastTagUrl);
1241
1331
  }
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
1332
  if (!ad) {
1250
1333
  console.warn("[HlsAdPlayer] No ads available from VAST response");
1251
1334
  emit("ad_error");
@@ -1264,6 +1347,37 @@ function createHlsAdPlayer(contentVideo, options) {
1264
1347
  return Promise.reject(error);
1265
1348
  }
1266
1349
  },
1350
+ async preloadAds(vastTagUrl) {
1351
+ if (!vastTagUrl || vastTagUrl.trim() === "") {
1352
+ return Promise.resolve();
1353
+ }
1354
+ if (preloadedAds.has(vastTagUrl)) {
1355
+ return Promise.resolve();
1356
+ }
1357
+ const inflight = preloadingAds.get(vastTagUrl);
1358
+ if (inflight) {
1359
+ return inflight;
1360
+ }
1361
+ const preloadPromise = fetchAndParseVastAd(vastTagUrl).then((ad) => {
1362
+ if (ad) {
1363
+ preloadedAds.set(vastTagUrl, ad);
1364
+ console.log(
1365
+ "[HlsAdPlayer] Cached VAST response for preloading:",
1366
+ vastTagUrl
1367
+ );
1368
+ }
1369
+ }).catch((error) => {
1370
+ console.warn("[HlsAdPlayer] Failed to preload VAST response:", error);
1371
+ preloadedAds.delete(vastTagUrl);
1372
+ }).finally(() => {
1373
+ preloadingAds.delete(vastTagUrl);
1374
+ });
1375
+ preloadingAds.set(vastTagUrl, preloadPromise);
1376
+ return preloadPromise;
1377
+ },
1378
+ hasPreloadedAd(vastTagUrl) {
1379
+ return preloadedAds.has(vastTagUrl);
1380
+ },
1267
1381
  async play() {
1268
1382
  if (!currentAd) {
1269
1383
  console.warn(
@@ -1394,6 +1508,8 @@ function createHlsAdPlayer(contentVideo, options) {
1394
1508
  adContainerEl = void 0;
1395
1509
  currentAd = void 0;
1396
1510
  listeners.clear();
1511
+ preloadedAds.clear();
1512
+ preloadingAds.clear();
1397
1513
  },
1398
1514
  isAdPlaying() {
1399
1515
  return adPlaying;
@@ -1436,6 +1552,35 @@ function createHlsAdPlayer(contentVideo, options) {
1436
1552
  return adVideoElement.volume;
1437
1553
  }
1438
1554
  return 1;
1555
+ },
1556
+ showPlaceholder() {
1557
+ var _a;
1558
+ if (!adContainerEl) {
1559
+ const container = document.createElement("div");
1560
+ container.style.position = "absolute";
1561
+ container.style.left = "0";
1562
+ container.style.top = "0";
1563
+ container.style.right = "0";
1564
+ container.style.bottom = "0";
1565
+ container.style.display = "none";
1566
+ container.style.alignItems = "center";
1567
+ container.style.justifyContent = "center";
1568
+ container.style.pointerEvents = "none";
1569
+ container.style.zIndex = "10";
1570
+ container.style.backgroundColor = "#000";
1571
+ (_a = contentVideo.parentElement) == null ? void 0 : _a.appendChild(container);
1572
+ adContainerEl = container;
1573
+ }
1574
+ if (adContainerEl) {
1575
+ adContainerEl.style.display = "flex";
1576
+ adContainerEl.style.pointerEvents = "auto";
1577
+ }
1578
+ },
1579
+ hidePlaceholder() {
1580
+ if (adContainerEl) {
1581
+ adContainerEl.style.display = "none";
1582
+ adContainerEl.style.pointerEvents = "none";
1583
+ }
1439
1584
  }
1440
1585
  };
1441
1586
  }
@@ -1916,6 +2061,8 @@ var StormcloudVideoPlayer = class {
1916
2061
  this.bufferedSegmentsCount = 0;
1917
2062
  this.shouldAutoplayAfterBuffering = false;
1918
2063
  this.hasInitialBufferCompleted = false;
2064
+ this.adPodAllUrls = [];
2065
+ this.preloadingAdUrls = /* @__PURE__ */ new Set();
1919
2066
  initializePolyfills();
1920
2067
  const browserOverrides = getBrowserConfigOverrides();
1921
2068
  this.config = { ...config, ...browserOverrides };
@@ -2216,6 +2363,7 @@ var StormcloudVideoPlayer = class {
2216
2363
  console.log("[StormcloudVideoPlayer] IMA content_pause event received");
2217
2364
  }
2218
2365
  this.clearAdFailsafeTimer();
2366
+ this.enforceAdHoldState();
2219
2367
  });
2220
2368
  this.ima.on("content_resume", () => {
2221
2369
  if (this.config.debugAdTiming) {
@@ -2240,9 +2388,7 @@ var StormcloudVideoPlayer = class {
2240
2388
  if (remaining > 500 && this.adPodQueue.length > 0) {
2241
2389
  const next = this.adPodQueue.shift();
2242
2390
  this.currentAdIndex++;
2243
- this.video.dataset.stormcloudAdPlaying = "true";
2244
- this.video.muted = true;
2245
- this.video.volume = 0;
2391
+ this.enforceAdHoldState();
2246
2392
  if (this.config.debugAdTiming) {
2247
2393
  console.log(
2248
2394
  `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak}) - IMMEDIATELY starting next ad`
@@ -2854,11 +3000,16 @@ var StormcloudVideoPlayer = class {
2854
3000
  return;
2855
3001
  }
2856
3002
  if (vastTagUrls.length > 0) {
3003
+ this.adPodAllUrls = [...vastTagUrls];
3004
+ this.preloadingAdUrls.clear();
3005
+ this.logQueuedAdUrls(this.adPodAllUrls);
2857
3006
  this.inAdBreak = true;
2858
3007
  this.showAds = true;
2859
3008
  this.currentAdIndex = 0;
2860
3009
  this.totalAdsInBreak = vastTagUrls.length;
2861
3010
  this.adPodQueue = [...vastTagUrls];
3011
+ this.enforceAdHoldState();
3012
+ this.preloadUpcomingAds();
2862
3013
  if (this.config.debugAdTiming) {
2863
3014
  console.log(
2864
3015
  `[StormcloudVideoPlayer] Starting ad pod with ${vastTagUrls.length} ads - will play continuously`
@@ -2929,6 +3080,7 @@ var StormcloudVideoPlayer = class {
2929
3080
  const first = tags[0];
2930
3081
  const rest = tags.slice(1);
2931
3082
  this.adPodQueue = rest;
3083
+ this.enforceAdHoldState();
2932
3084
  await this.playSingleAd(first);
2933
3085
  this.inAdBreak = true;
2934
3086
  this.expectedAdBreakDurationMs = remainingMs;
@@ -3042,6 +3194,12 @@ var StormcloudVideoPlayer = class {
3042
3194
  }
3043
3195
  return;
3044
3196
  }
3197
+ const wasPreloaded = this.ima.hasPreloadedAd(vastTagUrl);
3198
+ if (wasPreloaded && this.config.debugAdTiming) {
3199
+ console.log(
3200
+ `[StormcloudVideoPlayer] IMA SDK preloaded this ad already: ${vastTagUrl}`
3201
+ );
3202
+ }
3045
3203
  if (!this.showAds) {
3046
3204
  if (this.config.debugAdTiming) {
3047
3205
  console.log(
@@ -3062,12 +3220,14 @@ var StormcloudVideoPlayer = class {
3062
3220
  this.startAdFailsafeTimer();
3063
3221
  try {
3064
3222
  await this.ima.requestAds(vastTagUrl);
3223
+ this.preloadUpcomingAds();
3065
3224
  try {
3066
3225
  if (this.config.debugAdTiming) {
3067
3226
  console.log(
3068
3227
  "[StormcloudVideoPlayer] Ad request completed, attempting playback"
3069
3228
  );
3070
3229
  }
3230
+ this.enforceAdHoldState();
3071
3231
  await this.ima.play();
3072
3232
  if (this.config.debugAdTiming) {
3073
3233
  console.log(
@@ -3097,6 +3257,8 @@ var StormcloudVideoPlayer = class {
3097
3257
  "[StormcloudVideoPlayer] Handling ad pod completion - resuming content and hiding ad layer"
3098
3258
  );
3099
3259
  }
3260
+ this.releaseAdHoldState();
3261
+ this.preloadingAdUrls.clear();
3100
3262
  this.inAdBreak = false;
3101
3263
  this.expectedAdBreakDurationMs = void 0;
3102
3264
  this.currentAdBreakStartWallClockMs = void 0;
@@ -3104,6 +3266,7 @@ var StormcloudVideoPlayer = class {
3104
3266
  this.clearAdStopTimer();
3105
3267
  this.clearAdFailsafeTimer();
3106
3268
  this.adPodQueue = [];
3269
+ this.adPodAllUrls = [];
3107
3270
  this.showAds = false;
3108
3271
  this.currentAdIndex = 0;
3109
3272
  this.totalAdsInBreak = 0;
@@ -3184,6 +3347,64 @@ var StormcloudVideoPlayer = class {
3184
3347
  }
3185
3348
  return [b.vastTagUrl];
3186
3349
  }
3350
+ logQueuedAdUrls(urls) {
3351
+ if (!this.config.debugAdTiming) {
3352
+ return;
3353
+ }
3354
+ console.log("[StormcloudVideoPlayer] ALL ad URLs queued:", urls);
3355
+ }
3356
+ enforceAdHoldState() {
3357
+ this.video.dataset.stormcloudAdPlaying = "true";
3358
+ this.video.muted = true;
3359
+ this.video.volume = 0;
3360
+ if (typeof this.ima.showPlaceholder === "function") {
3361
+ this.ima.showPlaceholder();
3362
+ }
3363
+ }
3364
+ releaseAdHoldState() {
3365
+ delete this.video.dataset.stormcloudAdPlaying;
3366
+ if (typeof this.ima.hidePlaceholder === "function") {
3367
+ this.ima.hidePlaceholder();
3368
+ }
3369
+ }
3370
+ preloadUpcomingAds() {
3371
+ if (!this.ima.preloadAds || this.adPodQueue.length === 0) {
3372
+ return;
3373
+ }
3374
+ const upcoming = this.adPodQueue.slice(0, 2);
3375
+ for (const url of upcoming) {
3376
+ if (!url) continue;
3377
+ if (this.ima.hasPreloadedAd(url)) {
3378
+ this.preloadingAdUrls.delete(url);
3379
+ continue;
3380
+ }
3381
+ if (this.preloadingAdUrls.has(url)) {
3382
+ continue;
3383
+ }
3384
+ if (this.config.debugAdTiming) {
3385
+ console.log(
3386
+ `[StormcloudVideoPlayer] Scheduling IMA preload for upcoming ad: ${url}`
3387
+ );
3388
+ }
3389
+ this.preloadingAdUrls.add(url);
3390
+ this.ima.preloadAds(url).then(() => {
3391
+ if (this.config.debugAdTiming) {
3392
+ console.log(
3393
+ `[StormcloudVideoPlayer] IMA preload complete for ad: ${url}`
3394
+ );
3395
+ }
3396
+ }).catch((error) => {
3397
+ if (this.config.debugAdTiming) {
3398
+ console.warn(
3399
+ `[StormcloudVideoPlayer] IMA preload failed for ad: ${url}`,
3400
+ error
3401
+ );
3402
+ }
3403
+ }).finally(() => {
3404
+ this.preloadingAdUrls.delete(url);
3405
+ });
3406
+ }
3407
+ }
3187
3408
  getRemainingAdMs() {
3188
3409
  if (this.expectedAdBreakDurationMs == null || this.currentAdBreakStartWallClockMs == null)
3189
3410
  return 0;
@@ -3335,6 +3556,9 @@ var StormcloudVideoPlayer = class {
3335
3556
  }
3336
3557
  (_a = this.hls) == null ? void 0 : _a.destroy();
3337
3558
  (_b = this.ima) == null ? void 0 : _b.destroy();
3559
+ this.releaseAdHoldState();
3560
+ this.preloadingAdUrls.clear();
3561
+ this.adPodAllUrls = [];
3338
3562
  }
3339
3563
  };
3340
3564