stormcloud-video-player 0.2.8 → 0.2.10

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.
@@ -115,9 +115,22 @@ function createImaController(video, options) {
115
115
  'script[data-ima="true"]'
116
116
  );
117
117
  if (existing) {
118
- return new Promise(
119
- (resolve) => existing.addEventListener("load", () => resolve())
120
- );
118
+ if (window.google?.ima) {
119
+ return Promise.resolve();
120
+ }
121
+ return new Promise((resolve, reject) => {
122
+ const timeout = setTimeout(() => {
123
+ reject(new Error("IMA SDK load timeout"));
124
+ }, 1e4);
125
+ existing.addEventListener("load", () => {
126
+ clearTimeout(timeout);
127
+ resolve();
128
+ });
129
+ existing.addEventListener("error", () => {
130
+ clearTimeout(timeout);
131
+ reject(new Error("IMA SDK load failed"));
132
+ });
133
+ });
121
134
  }
122
135
  return new Promise((resolve, reject) => {
123
136
  const script = document.createElement("script");
@@ -146,6 +159,17 @@ function createImaController(video, options) {
146
159
  adsRequest.adTagUrl = vastTagUrl;
147
160
  adsLoader.requestAds(adsRequest);
148
161
  }
162
+ function destroyAdsManager() {
163
+ if (adsManager) {
164
+ try {
165
+ console.log("[IMA] Destroying existing ads manager");
166
+ adsManager.destroy();
167
+ } catch (error) {
168
+ console.warn("[IMA] Error destroying ads manager:", error);
169
+ }
170
+ adsManager = void 0;
171
+ }
172
+ }
149
173
  return {
150
174
  initialize() {
151
175
  ensureImaLoaded().then(() => {
@@ -178,9 +202,22 @@ function createImaController(video, options) {
178
202
  },
179
203
  async requestAds(vastTagUrl) {
180
204
  console.log("[IMA] Requesting ads:", vastTagUrl);
205
+ if (adPlaying) {
206
+ console.warn(
207
+ "[IMA] Cannot request new ads while an ad is playing. Call stop() first."
208
+ );
209
+ return Promise.reject(
210
+ new Error("Ad already playing - cannot request new ads")
211
+ );
212
+ }
213
+ destroyAdsManager();
214
+ adsLoadedReject = void 0;
215
+ adsLoadedResolve = void 0;
216
+ let currentReject;
181
217
  adsLoadedPromise = new Promise((resolve, reject) => {
182
218
  adsLoadedResolve = resolve;
183
219
  adsLoadedReject = reject;
220
+ currentReject = reject;
184
221
  setTimeout(() => {
185
222
  if (adsLoadedReject) {
186
223
  adsLoadedReject(new Error("Ad request timeout"));
@@ -202,7 +239,7 @@ function createImaController(video, options) {
202
239
  container.style.top = "0";
203
240
  container.style.right = "0";
204
241
  container.style.bottom = "0";
205
- container.style.display = "flex";
242
+ container.style.display = "none";
206
243
  container.style.alignItems = "center";
207
244
  container.style.justifyContent = "center";
208
245
  container.style.pointerEvents = "none";
@@ -242,14 +279,14 @@ function createImaController(video, options) {
242
279
  AdErrorEvent.AD_ERROR,
243
280
  (errorEvent) => {
244
281
  console.error("[IMA] Ad error:", errorEvent.getError());
245
- try {
246
- adsManager?.destroy?.();
247
- } catch {
248
- }
282
+ destroyAdsManager();
249
283
  adPlaying = false;
250
284
  video.muted = originalMutedState;
251
- if (adContainerEl)
285
+ if (adContainerEl) {
252
286
  adContainerEl.style.pointerEvents = "none";
287
+ adContainerEl.style.display = "none";
288
+ console.log("[IMA] Ad container hidden after error");
289
+ }
253
290
  if (adsLoadedReject) {
254
291
  adsLoadedReject(new Error("Ad playback error"));
255
292
  adsLoadedReject = void 0;
@@ -282,8 +319,6 @@ function createImaController(video, options) {
282
319
  AdEvent.CONTENT_PAUSE_REQUESTED,
283
320
  () => {
284
321
  console.log("[IMA] Content pause requested");
285
- originalMutedState = video.muted;
286
- video.muted = true;
287
322
  if (!options?.continueLiveStreamDuringAds) {
288
323
  video.pause();
289
324
  console.log("[IMA] Video paused (VOD mode)");
@@ -292,20 +327,34 @@ function createImaController(video, options) {
292
327
  "[IMA] Video continues playing but muted (Live mode)"
293
328
  );
294
329
  }
330
+ video.muted = true;
295
331
  adPlaying = true;
296
- if (adContainerEl)
297
- adContainerEl.style.pointerEvents = "auto";
298
332
  emit("content_pause");
299
333
  }
300
334
  );
335
+ adsManager.addEventListener(AdEvent.STARTED, () => {
336
+ console.log("[IMA] Ad started playing");
337
+ if (adContainerEl) {
338
+ adContainerEl.style.pointerEvents = "auto";
339
+ adContainerEl.style.display = "flex";
340
+ console.log(
341
+ "[IMA] Ad container visibility set to flex with pointer events enabled"
342
+ );
343
+ }
344
+ });
301
345
  adsManager.addEventListener(
302
346
  AdEvent.CONTENT_RESUME_REQUESTED,
303
347
  () => {
304
348
  console.log("[IMA] Content resume requested");
305
349
  adPlaying = false;
306
350
  video.muted = originalMutedState;
307
- if (adContainerEl)
351
+ if (adContainerEl) {
308
352
  adContainerEl.style.pointerEvents = "none";
353
+ adContainerEl.style.display = "none";
354
+ console.log(
355
+ "[IMA] Ad container hidden - pointer events disabled"
356
+ );
357
+ }
309
358
  if (!options?.continueLiveStreamDuringAds) {
310
359
  video.play()?.catch(() => {
311
360
  });
@@ -322,7 +371,13 @@ function createImaController(video, options) {
322
371
  console.log("[IMA] All ads completed");
323
372
  adPlaying = false;
324
373
  video.muted = originalMutedState;
325
- if (adContainerEl) adContainerEl.style.pointerEvents = "none";
374
+ if (adContainerEl) {
375
+ adContainerEl.style.pointerEvents = "none";
376
+ adContainerEl.style.display = "none";
377
+ console.log(
378
+ "[IMA] Ad container hidden after all ads completed"
379
+ );
380
+ }
326
381
  if (!options?.continueLiveStreamDuringAds) {
327
382
  video.play().catch(() => {
328
383
  });
@@ -365,6 +420,13 @@ function createImaController(video, options) {
365
420
  google.ima.AdErrorEvent.Type.AD_ERROR,
366
421
  (adErrorEvent) => {
367
422
  console.error("[IMA] Ads loader error:", adErrorEvent.getError());
423
+ adPlaying = false;
424
+ video.muted = originalMutedState;
425
+ if (adContainerEl) adContainerEl.style.pointerEvents = "none";
426
+ if (!options?.continueLiveStreamDuringAds) {
427
+ video.play().catch(() => {
428
+ });
429
+ }
368
430
  if (adsLoadedReject) {
369
431
  adsLoadedReject(new Error("Ads loader error"));
370
432
  adsLoadedReject = void 0;
@@ -380,11 +442,9 @@ function createImaController(video, options) {
380
442
  return adsLoadedPromise;
381
443
  } catch (error) {
382
444
  console.error("[IMA] Failed to request ads:", error);
383
- if (adsLoadedReject) {
384
- adsLoadedReject(error);
385
- adsLoadedReject = void 0;
386
- adsLoadedResolve = void 0;
387
- }
445
+ currentReject?.(error);
446
+ adsLoadedReject = void 0;
447
+ adsLoadedResolve = void 0;
388
448
  return Promise.reject(error);
389
449
  }
390
450
  },
@@ -404,14 +464,6 @@ function createImaController(video, options) {
404
464
  const height = video.clientHeight || 360;
405
465
  console.log(`[IMA] Initializing ads manager (${width}x${height})`);
406
466
  adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
407
- if (!options?.continueLiveStreamDuringAds) {
408
- console.log("[IMA] Pausing video for ad playback (VOD mode)");
409
- video.pause();
410
- } else {
411
- console.log(
412
- "[IMA] Keeping video playing but muted for ad playback (Live mode)"
413
- );
414
- }
415
467
  adPlaying = true;
416
468
  console.log("[IMA] Starting ad playback");
417
469
  adsManager.start();
@@ -429,10 +481,16 @@ function createImaController(video, options) {
429
481
  async stop() {
430
482
  adPlaying = false;
431
483
  video.muted = originalMutedState;
484
+ if (adContainerEl) {
485
+ adContainerEl.style.pointerEvents = "none";
486
+ adContainerEl.style.display = "none";
487
+ console.log("[IMA] Ad container hidden after stop");
488
+ }
432
489
  try {
433
490
  adsManager?.stop?.();
434
491
  } catch {
435
492
  }
493
+ destroyAdsManager();
436
494
  if (!options?.continueLiveStreamDuringAds) {
437
495
  video.play().catch(() => {
438
496
  });
@@ -442,12 +500,13 @@ function createImaController(video, options) {
442
500
  }
443
501
  },
444
502
  destroy() {
445
- try {
446
- adsManager?.destroy?.();
447
- } catch {
448
- }
503
+ destroyAdsManager();
449
504
  adPlaying = false;
450
505
  video.muted = originalMutedState;
506
+ if (adContainerEl) {
507
+ adContainerEl.style.pointerEvents = "none";
508
+ adContainerEl.style.display = "none";
509
+ }
451
510
  try {
452
511
  adsLoader?.destroy?.();
453
512
  } catch {
@@ -455,6 +514,9 @@ function createImaController(video, options) {
455
514
  if (adContainerEl?.parentElement) {
456
515
  adContainerEl.parentElement.removeChild(adContainerEl);
457
516
  }
517
+ adContainerEl = void 0;
518
+ adDisplayContainer = void 0;
519
+ adsLoader = void 0;
458
520
  },
459
521
  isAdPlaying() {
460
522
  return adPlaying;
@@ -510,6 +572,7 @@ function createImaController(video, options) {
510
572
  }
511
573
 
512
574
  // src/utils/tracking.ts
575
+ var cachedBrowserId = null;
513
576
  function getClientInfo() {
514
577
  const ua = navigator.userAgent;
515
578
  const platform = navigator.platform;
@@ -652,6 +715,9 @@ function getClientInfo() {
652
715
  };
653
716
  }
654
717
  async function getBrowserID(clientInfo) {
718
+ if (cachedBrowserId) {
719
+ return cachedBrowserId;
720
+ }
655
721
  const fingerprintString = JSON.stringify(clientInfo);
656
722
  if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.digest) {
657
723
  try {
@@ -662,6 +728,7 @@ async function getBrowserID(clientInfo) {
662
728
  );
663
729
  const hashArray = Array.from(new Uint8Array(hashBuffer));
664
730
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
731
+ cachedBrowserId = hashHex;
665
732
  return hashHex;
666
733
  } catch (error) {
667
734
  console.warn(
@@ -678,7 +745,8 @@ async function getBrowserID(clientInfo) {
678
745
  const fallbackHash = Math.abs(hash).toString(16).padStart(8, "0");
679
746
  const timestamp = Date.now().toString(16).padStart(12, "0");
680
747
  const random = Math.random().toString(16).substring(2, 14).padStart(12, "0");
681
- return (fallbackHash + timestamp + random).padEnd(64, "0");
748
+ cachedBrowserId = (fallbackHash + timestamp + random).padEnd(64, "0");
749
+ return cachedBrowserId;
682
750
  }
683
751
  async function sendInitialTracking(licenseKey) {
684
752
  try {
@@ -926,17 +994,8 @@ var StormcloudVideoPlayer = class {
926
994
  this.video.muted = !!this.config.muted;
927
995
  this.ima.initialize();
928
996
  this.ima.on("all_ads_completed", () => {
929
- if (!this.inAdBreak) return;
930
- const remaining = this.getRemainingAdMs();
931
- if (remaining > 500 && this.adPodQueue.length > 0) {
932
- const next = this.adPodQueue.shift();
933
- this.currentAdIndex++;
934
- this.playSingleAd(next).catch(() => {
935
- });
936
- } else {
937
- this.currentAdIndex = 0;
938
- this.totalAdsInBreak = 0;
939
- this.showAds = false;
997
+ if (this.config.debugAdTiming) {
998
+ console.log("[StormcloudVideoPlayer] IMA all_ads_completed event received");
940
999
  }
941
1000
  });
942
1001
  this.ima.on("ad_error", () => {
@@ -967,6 +1026,26 @@ var StormcloudVideoPlayer = class {
967
1026
  );
968
1027
  }
969
1028
  this.clearAdFailsafeTimer();
1029
+ if (!this.inAdBreak) return;
1030
+ const remaining = this.getRemainingAdMs();
1031
+ if (remaining > 500 && this.adPodQueue.length > 0) {
1032
+ const next = this.adPodQueue.shift();
1033
+ this.currentAdIndex++;
1034
+ if (this.config.debugAdTiming) {
1035
+ console.log(
1036
+ `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak})`
1037
+ );
1038
+ }
1039
+ this.playSingleAd(next).catch(() => {
1040
+ });
1041
+ } else {
1042
+ if (this.config.debugAdTiming) {
1043
+ console.log("[StormcloudVideoPlayer] Ad pod completed");
1044
+ }
1045
+ this.currentAdIndex = 0;
1046
+ this.totalAdsInBreak = 0;
1047
+ this.showAds = false;
1048
+ }
970
1049
  });
971
1050
  this.video.addEventListener("timeupdate", () => {
972
1051
  this.onTimeUpdate(this.video.currentTime);
@@ -1367,17 +1446,21 @@ var StormcloudVideoPlayer = class {
1367
1446
  return void 0;
1368
1447
  }
1369
1448
  initializeTracking() {
1370
- sendInitialTracking(this.config.licenseKey).catch((error) => {
1449
+ sendInitialTracking(this.config.licenseKey).then(() => {
1450
+ this.heartbeatInterval = window.setInterval(() => {
1451
+ this.sendHeartbeatIfNeeded();
1452
+ }, 5e3);
1453
+ }).catch((error) => {
1371
1454
  if (this.config.debugAdTiming) {
1372
1455
  console.warn(
1373
1456
  "[StormcloudVideoPlayer] Failed to send initial tracking:",
1374
1457
  error
1375
1458
  );
1376
1459
  }
1460
+ this.heartbeatInterval = window.setInterval(() => {
1461
+ this.sendHeartbeatIfNeeded();
1462
+ }, 5e3);
1377
1463
  });
1378
- this.heartbeatInterval = window.setInterval(() => {
1379
- this.sendHeartbeatIfNeeded();
1380
- }, 5e3);
1381
1464
  }
1382
1465
  sendHeartbeatIfNeeded() {
1383
1466
  const now = Date.now();
@@ -1612,9 +1695,21 @@ var StormcloudVideoPlayer = class {
1612
1695
  if (this.config.debugAdTiming) {
1613
1696
  console.log("[StormcloudVideoPlayer] Attempting to play ad:", vastTagUrl);
1614
1697
  }
1698
+ if (this.ima.isAdPlaying()) {
1699
+ if (this.config.debugAdTiming) {
1700
+ console.warn(
1701
+ "[StormcloudVideoPlayer] Ad already playing - skipping new ad request"
1702
+ );
1703
+ }
1704
+ return;
1705
+ }
1706
+ this.ima.updateOriginalMutedState(this.video.muted);
1615
1707
  this.startAdFailsafeTimer();
1616
1708
  try {
1617
1709
  await this.ima.requestAds(vastTagUrl);
1710
+ if (this.config.debugAdTiming) {
1711
+ console.log("[StormcloudVideoPlayer] Ad request successful, starting playback");
1712
+ }
1618
1713
  await this.ima.play();
1619
1714
  if (this.config.debugAdTiming) {
1620
1715
  console.log("[StormcloudVideoPlayer] Ad playback started successfully");
@@ -1642,6 +1737,13 @@ var StormcloudVideoPlayer = class {
1642
1737
  this.showAds = false;
1643
1738
  this.currentAdIndex = 0;
1644
1739
  this.totalAdsInBreak = 0;
1740
+ const originalMutedState = this.ima.getOriginalMutedState();
1741
+ this.video.muted = originalMutedState;
1742
+ if (this.config.debugAdTiming) {
1743
+ console.log(
1744
+ `[StormcloudVideoPlayer] Restored mute state to: ${originalMutedState}`
1745
+ );
1746
+ }
1645
1747
  if (this.video.paused) {
1646
1748
  this.video.play()?.catch(() => {
1647
1749
  if (this.config.debugAdTiming) {