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.
@@ -73,9 +73,22 @@ function createImaController(video, options) {
73
73
  'script[data-ima="true"]'
74
74
  );
75
75
  if (existing) {
76
- return new Promise(
77
- (resolve) => existing.addEventListener("load", () => resolve())
78
- );
76
+ if (window.google?.ima) {
77
+ return Promise.resolve();
78
+ }
79
+ return new Promise((resolve, reject) => {
80
+ const timeout = setTimeout(() => {
81
+ reject(new Error("IMA SDK load timeout"));
82
+ }, 1e4);
83
+ existing.addEventListener("load", () => {
84
+ clearTimeout(timeout);
85
+ resolve();
86
+ });
87
+ existing.addEventListener("error", () => {
88
+ clearTimeout(timeout);
89
+ reject(new Error("IMA SDK load failed"));
90
+ });
91
+ });
79
92
  }
80
93
  return new Promise((resolve, reject) => {
81
94
  const script = document.createElement("script");
@@ -104,6 +117,17 @@ function createImaController(video, options) {
104
117
  adsRequest.adTagUrl = vastTagUrl;
105
118
  adsLoader.requestAds(adsRequest);
106
119
  }
120
+ function destroyAdsManager() {
121
+ if (adsManager) {
122
+ try {
123
+ console.log("[IMA] Destroying existing ads manager");
124
+ adsManager.destroy();
125
+ } catch (error) {
126
+ console.warn("[IMA] Error destroying ads manager:", error);
127
+ }
128
+ adsManager = void 0;
129
+ }
130
+ }
107
131
  return {
108
132
  initialize() {
109
133
  ensureImaLoaded().then(() => {
@@ -136,9 +160,22 @@ function createImaController(video, options) {
136
160
  },
137
161
  async requestAds(vastTagUrl) {
138
162
  console.log("[IMA] Requesting ads:", vastTagUrl);
163
+ if (adPlaying) {
164
+ console.warn(
165
+ "[IMA] Cannot request new ads while an ad is playing. Call stop() first."
166
+ );
167
+ return Promise.reject(
168
+ new Error("Ad already playing - cannot request new ads")
169
+ );
170
+ }
171
+ destroyAdsManager();
172
+ adsLoadedReject = void 0;
173
+ adsLoadedResolve = void 0;
174
+ let currentReject;
139
175
  adsLoadedPromise = new Promise((resolve, reject) => {
140
176
  adsLoadedResolve = resolve;
141
177
  adsLoadedReject = reject;
178
+ currentReject = reject;
142
179
  setTimeout(() => {
143
180
  if (adsLoadedReject) {
144
181
  adsLoadedReject(new Error("Ad request timeout"));
@@ -160,7 +197,7 @@ function createImaController(video, options) {
160
197
  container.style.top = "0";
161
198
  container.style.right = "0";
162
199
  container.style.bottom = "0";
163
- container.style.display = "flex";
200
+ container.style.display = "none";
164
201
  container.style.alignItems = "center";
165
202
  container.style.justifyContent = "center";
166
203
  container.style.pointerEvents = "none";
@@ -200,14 +237,14 @@ function createImaController(video, options) {
200
237
  AdErrorEvent.AD_ERROR,
201
238
  (errorEvent) => {
202
239
  console.error("[IMA] Ad error:", errorEvent.getError());
203
- try {
204
- adsManager?.destroy?.();
205
- } catch {
206
- }
240
+ destroyAdsManager();
207
241
  adPlaying = false;
208
242
  video.muted = originalMutedState;
209
- if (adContainerEl)
243
+ if (adContainerEl) {
210
244
  adContainerEl.style.pointerEvents = "none";
245
+ adContainerEl.style.display = "none";
246
+ console.log("[IMA] Ad container hidden after error");
247
+ }
211
248
  if (adsLoadedReject) {
212
249
  adsLoadedReject(new Error("Ad playback error"));
213
250
  adsLoadedReject = void 0;
@@ -240,8 +277,6 @@ function createImaController(video, options) {
240
277
  AdEvent.CONTENT_PAUSE_REQUESTED,
241
278
  () => {
242
279
  console.log("[IMA] Content pause requested");
243
- originalMutedState = video.muted;
244
- video.muted = true;
245
280
  if (!options?.continueLiveStreamDuringAds) {
246
281
  video.pause();
247
282
  console.log("[IMA] Video paused (VOD mode)");
@@ -250,20 +285,34 @@ function createImaController(video, options) {
250
285
  "[IMA] Video continues playing but muted (Live mode)"
251
286
  );
252
287
  }
288
+ video.muted = true;
253
289
  adPlaying = true;
254
- if (adContainerEl)
255
- adContainerEl.style.pointerEvents = "auto";
256
290
  emit("content_pause");
257
291
  }
258
292
  );
293
+ adsManager.addEventListener(AdEvent.STARTED, () => {
294
+ console.log("[IMA] Ad started playing");
295
+ if (adContainerEl) {
296
+ adContainerEl.style.pointerEvents = "auto";
297
+ adContainerEl.style.display = "flex";
298
+ console.log(
299
+ "[IMA] Ad container visibility set to flex with pointer events enabled"
300
+ );
301
+ }
302
+ });
259
303
  adsManager.addEventListener(
260
304
  AdEvent.CONTENT_RESUME_REQUESTED,
261
305
  () => {
262
306
  console.log("[IMA] Content resume requested");
263
307
  adPlaying = false;
264
308
  video.muted = originalMutedState;
265
- if (adContainerEl)
309
+ if (adContainerEl) {
266
310
  adContainerEl.style.pointerEvents = "none";
311
+ adContainerEl.style.display = "none";
312
+ console.log(
313
+ "[IMA] Ad container hidden - pointer events disabled"
314
+ );
315
+ }
267
316
  if (!options?.continueLiveStreamDuringAds) {
268
317
  video.play()?.catch(() => {
269
318
  });
@@ -280,7 +329,13 @@ function createImaController(video, options) {
280
329
  console.log("[IMA] All ads completed");
281
330
  adPlaying = false;
282
331
  video.muted = originalMutedState;
283
- if (adContainerEl) adContainerEl.style.pointerEvents = "none";
332
+ if (adContainerEl) {
333
+ adContainerEl.style.pointerEvents = "none";
334
+ adContainerEl.style.display = "none";
335
+ console.log(
336
+ "[IMA] Ad container hidden after all ads completed"
337
+ );
338
+ }
284
339
  if (!options?.continueLiveStreamDuringAds) {
285
340
  video.play().catch(() => {
286
341
  });
@@ -323,6 +378,13 @@ function createImaController(video, options) {
323
378
  google.ima.AdErrorEvent.Type.AD_ERROR,
324
379
  (adErrorEvent) => {
325
380
  console.error("[IMA] Ads loader error:", adErrorEvent.getError());
381
+ adPlaying = false;
382
+ video.muted = originalMutedState;
383
+ if (adContainerEl) adContainerEl.style.pointerEvents = "none";
384
+ if (!options?.continueLiveStreamDuringAds) {
385
+ video.play().catch(() => {
386
+ });
387
+ }
326
388
  if (adsLoadedReject) {
327
389
  adsLoadedReject(new Error("Ads loader error"));
328
390
  adsLoadedReject = void 0;
@@ -338,11 +400,9 @@ function createImaController(video, options) {
338
400
  return adsLoadedPromise;
339
401
  } catch (error) {
340
402
  console.error("[IMA] Failed to request ads:", error);
341
- if (adsLoadedReject) {
342
- adsLoadedReject(error);
343
- adsLoadedReject = void 0;
344
- adsLoadedResolve = void 0;
345
- }
403
+ currentReject?.(error);
404
+ adsLoadedReject = void 0;
405
+ adsLoadedResolve = void 0;
346
406
  return Promise.reject(error);
347
407
  }
348
408
  },
@@ -362,14 +422,6 @@ function createImaController(video, options) {
362
422
  const height = video.clientHeight || 360;
363
423
  console.log(`[IMA] Initializing ads manager (${width}x${height})`);
364
424
  adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
365
- if (!options?.continueLiveStreamDuringAds) {
366
- console.log("[IMA] Pausing video for ad playback (VOD mode)");
367
- video.pause();
368
- } else {
369
- console.log(
370
- "[IMA] Keeping video playing but muted for ad playback (Live mode)"
371
- );
372
- }
373
425
  adPlaying = true;
374
426
  console.log("[IMA] Starting ad playback");
375
427
  adsManager.start();
@@ -387,10 +439,16 @@ function createImaController(video, options) {
387
439
  async stop() {
388
440
  adPlaying = false;
389
441
  video.muted = originalMutedState;
442
+ if (adContainerEl) {
443
+ adContainerEl.style.pointerEvents = "none";
444
+ adContainerEl.style.display = "none";
445
+ console.log("[IMA] Ad container hidden after stop");
446
+ }
390
447
  try {
391
448
  adsManager?.stop?.();
392
449
  } catch {
393
450
  }
451
+ destroyAdsManager();
394
452
  if (!options?.continueLiveStreamDuringAds) {
395
453
  video.play().catch(() => {
396
454
  });
@@ -400,12 +458,13 @@ function createImaController(video, options) {
400
458
  }
401
459
  },
402
460
  destroy() {
403
- try {
404
- adsManager?.destroy?.();
405
- } catch {
406
- }
461
+ destroyAdsManager();
407
462
  adPlaying = false;
408
463
  video.muted = originalMutedState;
464
+ if (adContainerEl) {
465
+ adContainerEl.style.pointerEvents = "none";
466
+ adContainerEl.style.display = "none";
467
+ }
409
468
  try {
410
469
  adsLoader?.destroy?.();
411
470
  } catch {
@@ -413,6 +472,9 @@ function createImaController(video, options) {
413
472
  if (adContainerEl?.parentElement) {
414
473
  adContainerEl.parentElement.removeChild(adContainerEl);
415
474
  }
475
+ adContainerEl = void 0;
476
+ adDisplayContainer = void 0;
477
+ adsLoader = void 0;
416
478
  },
417
479
  isAdPlaying() {
418
480
  return adPlaying;
@@ -468,6 +530,7 @@ function createImaController(video, options) {
468
530
  }
469
531
 
470
532
  // src/utils/tracking.ts
533
+ var cachedBrowserId = null;
471
534
  function getClientInfo() {
472
535
  const ua = navigator.userAgent;
473
536
  const platform = navigator.platform;
@@ -610,6 +673,9 @@ function getClientInfo() {
610
673
  };
611
674
  }
612
675
  async function getBrowserID(clientInfo) {
676
+ if (cachedBrowserId) {
677
+ return cachedBrowserId;
678
+ }
613
679
  const fingerprintString = JSON.stringify(clientInfo);
614
680
  if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.digest) {
615
681
  try {
@@ -620,6 +686,7 @@ async function getBrowserID(clientInfo) {
620
686
  );
621
687
  const hashArray = Array.from(new Uint8Array(hashBuffer));
622
688
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
689
+ cachedBrowserId = hashHex;
623
690
  return hashHex;
624
691
  } catch (error) {
625
692
  console.warn(
@@ -636,7 +703,8 @@ async function getBrowserID(clientInfo) {
636
703
  const fallbackHash = Math.abs(hash).toString(16).padStart(8, "0");
637
704
  const timestamp = Date.now().toString(16).padStart(12, "0");
638
705
  const random = Math.random().toString(16).substring(2, 14).padStart(12, "0");
639
- return (fallbackHash + timestamp + random).padEnd(64, "0");
706
+ cachedBrowserId = (fallbackHash + timestamp + random).padEnd(64, "0");
707
+ return cachedBrowserId;
640
708
  }
641
709
  async function sendInitialTracking(licenseKey) {
642
710
  try {
@@ -884,17 +952,8 @@ var StormcloudVideoPlayer = class {
884
952
  this.video.muted = !!this.config.muted;
885
953
  this.ima.initialize();
886
954
  this.ima.on("all_ads_completed", () => {
887
- if (!this.inAdBreak) return;
888
- const remaining = this.getRemainingAdMs();
889
- if (remaining > 500 && this.adPodQueue.length > 0) {
890
- const next = this.adPodQueue.shift();
891
- this.currentAdIndex++;
892
- this.playSingleAd(next).catch(() => {
893
- });
894
- } else {
895
- this.currentAdIndex = 0;
896
- this.totalAdsInBreak = 0;
897
- this.showAds = false;
955
+ if (this.config.debugAdTiming) {
956
+ console.log("[StormcloudVideoPlayer] IMA all_ads_completed event received");
898
957
  }
899
958
  });
900
959
  this.ima.on("ad_error", () => {
@@ -925,6 +984,26 @@ var StormcloudVideoPlayer = class {
925
984
  );
926
985
  }
927
986
  this.clearAdFailsafeTimer();
987
+ if (!this.inAdBreak) return;
988
+ const remaining = this.getRemainingAdMs();
989
+ if (remaining > 500 && this.adPodQueue.length > 0) {
990
+ const next = this.adPodQueue.shift();
991
+ this.currentAdIndex++;
992
+ if (this.config.debugAdTiming) {
993
+ console.log(
994
+ `[StormcloudVideoPlayer] Playing next ad in pod (${this.currentAdIndex}/${this.totalAdsInBreak})`
995
+ );
996
+ }
997
+ this.playSingleAd(next).catch(() => {
998
+ });
999
+ } else {
1000
+ if (this.config.debugAdTiming) {
1001
+ console.log("[StormcloudVideoPlayer] Ad pod completed");
1002
+ }
1003
+ this.currentAdIndex = 0;
1004
+ this.totalAdsInBreak = 0;
1005
+ this.showAds = false;
1006
+ }
928
1007
  });
929
1008
  this.video.addEventListener("timeupdate", () => {
930
1009
  this.onTimeUpdate(this.video.currentTime);
@@ -1325,17 +1404,21 @@ var StormcloudVideoPlayer = class {
1325
1404
  return void 0;
1326
1405
  }
1327
1406
  initializeTracking() {
1328
- sendInitialTracking(this.config.licenseKey).catch((error) => {
1407
+ sendInitialTracking(this.config.licenseKey).then(() => {
1408
+ this.heartbeatInterval = window.setInterval(() => {
1409
+ this.sendHeartbeatIfNeeded();
1410
+ }, 5e3);
1411
+ }).catch((error) => {
1329
1412
  if (this.config.debugAdTiming) {
1330
1413
  console.warn(
1331
1414
  "[StormcloudVideoPlayer] Failed to send initial tracking:",
1332
1415
  error
1333
1416
  );
1334
1417
  }
1418
+ this.heartbeatInterval = window.setInterval(() => {
1419
+ this.sendHeartbeatIfNeeded();
1420
+ }, 5e3);
1335
1421
  });
1336
- this.heartbeatInterval = window.setInterval(() => {
1337
- this.sendHeartbeatIfNeeded();
1338
- }, 5e3);
1339
1422
  }
1340
1423
  sendHeartbeatIfNeeded() {
1341
1424
  const now = Date.now();
@@ -1570,9 +1653,21 @@ var StormcloudVideoPlayer = class {
1570
1653
  if (this.config.debugAdTiming) {
1571
1654
  console.log("[StormcloudVideoPlayer] Attempting to play ad:", vastTagUrl);
1572
1655
  }
1656
+ if (this.ima.isAdPlaying()) {
1657
+ if (this.config.debugAdTiming) {
1658
+ console.warn(
1659
+ "[StormcloudVideoPlayer] Ad already playing - skipping new ad request"
1660
+ );
1661
+ }
1662
+ return;
1663
+ }
1664
+ this.ima.updateOriginalMutedState(this.video.muted);
1573
1665
  this.startAdFailsafeTimer();
1574
1666
  try {
1575
1667
  await this.ima.requestAds(vastTagUrl);
1668
+ if (this.config.debugAdTiming) {
1669
+ console.log("[StormcloudVideoPlayer] Ad request successful, starting playback");
1670
+ }
1576
1671
  await this.ima.play();
1577
1672
  if (this.config.debugAdTiming) {
1578
1673
  console.log("[StormcloudVideoPlayer] Ad playback started successfully");
@@ -1600,6 +1695,13 @@ var StormcloudVideoPlayer = class {
1600
1695
  this.showAds = false;
1601
1696
  this.currentAdIndex = 0;
1602
1697
  this.totalAdsInBreak = 0;
1698
+ const originalMutedState = this.ima.getOriginalMutedState();
1699
+ this.video.muted = originalMutedState;
1700
+ if (this.config.debugAdTiming) {
1701
+ console.log(
1702
+ `[StormcloudVideoPlayer] Restored mute state to: ${originalMutedState}`
1703
+ );
1704
+ }
1603
1705
  if (this.video.paused) {
1604
1706
  this.video.play()?.catch(() => {
1605
1707
  if (this.config.debugAdTiming) {