stormcloud-video-player 0.2.17 → 0.2.19

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,13 @@ function createImaController(video, options) {
267
267
  let adPlaying = false;
268
268
  let originalMutedState = false;
269
269
  const listeners = /* @__PURE__ */ new Map();
270
+ function setAdPlayingFlag(isPlaying) {
271
+ if (isPlaying) {
272
+ video.dataset.stormcloudAdPlaying = "true";
273
+ } else {
274
+ delete video.dataset.stormcloudAdPlaying;
275
+ }
276
+ }
270
277
  function emit(event, payload) {
271
278
  const set = listeners.get(event);
272
279
  if (!set) return;
@@ -509,7 +516,12 @@ function createImaController(video, options) {
509
516
  console.error("[IMA] Ad error:", errorEvent.getError());
510
517
  destroyAdsManager();
511
518
  adPlaying = false;
519
+ const previousMutedState = video.muted;
512
520
  video.muted = originalMutedState;
521
+ setAdPlayingFlag(false);
522
+ console.log(
523
+ `[IMA] Restored mute state after ad error: ${previousMutedState} -> ${originalMutedState}`
524
+ );
513
525
  if (adContainerEl) {
514
526
  adContainerEl.style.pointerEvents = "none";
515
527
  adContainerEl.style.display = "none";
@@ -562,11 +574,13 @@ function createImaController(video, options) {
562
574
  }
563
575
  video.muted = true;
564
576
  adPlaying = true;
577
+ setAdPlayingFlag(true);
565
578
  emit("content_pause");
566
579
  }
567
580
  );
568
581
  adsManager.addEventListener(AdEvent.STARTED, () => {
569
582
  console.log("[IMA] Ad started playing");
583
+ setAdPlayingFlag(true);
570
584
  if (adContainerEl) {
571
585
  adContainerEl.style.pointerEvents = "auto";
572
586
  adContainerEl.style.display = "flex";
@@ -582,6 +596,7 @@ function createImaController(video, options) {
582
596
  console.log("[IMA] Content resume requested");
583
597
  adPlaying = false;
584
598
  video.muted = originalMutedState;
599
+ setAdPlayingFlag(false);
585
600
  if (adContainerEl) {
586
601
  adContainerEl.style.pointerEvents = "none";
587
602
  adContainerEl.style.display = "none";
@@ -605,6 +620,7 @@ function createImaController(video, options) {
605
620
  console.log("[IMA] All ads completed");
606
621
  adPlaying = false;
607
622
  video.muted = originalMutedState;
623
+ setAdPlayingFlag(false);
608
624
  if (adContainerEl) {
609
625
  adContainerEl.style.pointerEvents = "none";
610
626
  adContainerEl.style.display = "none";
@@ -635,6 +651,7 @@ function createImaController(video, options) {
635
651
  console.error("[IMA] Error setting up ads manager:", e);
636
652
  adPlaying = false;
637
653
  video.muted = originalMutedState;
654
+ setAdPlayingFlag(false);
638
655
  if (adContainerEl) {
639
656
  adContainerEl.style.pointerEvents = "none";
640
657
  adContainerEl.style.display = "none";
@@ -664,7 +681,12 @@ function createImaController(video, options) {
664
681
  (adErrorEvent) => {
665
682
  console.error("[IMA] Ads loader error:", adErrorEvent.getError());
666
683
  adPlaying = false;
684
+ const previousMutedState = video.muted;
667
685
  video.muted = originalMutedState;
686
+ setAdPlayingFlag(false);
687
+ console.log(
688
+ `[IMA] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
689
+ );
668
690
  if (adContainerEl) {
669
691
  adContainerEl.style.pointerEvents = "none";
670
692
  adContainerEl.style.display = "none";
@@ -716,12 +738,20 @@ function createImaController(video, options) {
716
738
  console.log(`[IMA] Initializing ads manager (${width}x${height})`);
717
739
  adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
718
740
  adPlaying = true;
741
+ const adVolume = originalMutedState ? 0 : video.volume;
742
+ try {
743
+ adsManager.setVolume(adVolume);
744
+ console.log(`[IMA] Set ad volume to ${adVolume}`);
745
+ } catch (error) {
746
+ console.warn("[IMA] Failed to set ad volume:", error);
747
+ }
719
748
  console.log("[IMA] Starting ad playback");
720
749
  adsManager.start();
721
750
  return Promise.resolve();
722
751
  } catch (error) {
723
752
  console.error("[IMA] Error starting ad playback:", error);
724
753
  adPlaying = false;
754
+ setAdPlayingFlag(false);
725
755
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
726
756
  (_b = video.play()) == null ? void 0 : _b.catch(() => {
727
757
  });
@@ -733,6 +763,7 @@ function createImaController(video, options) {
733
763
  var _a;
734
764
  adPlaying = false;
735
765
  video.muted = originalMutedState;
766
+ setAdPlayingFlag(false);
736
767
  if (adContainerEl) {
737
768
  adContainerEl.style.pointerEvents = "none";
738
769
  adContainerEl.style.display = "none";
@@ -756,6 +787,7 @@ function createImaController(video, options) {
756
787
  destroyAdsManager();
757
788
  adPlaying = false;
758
789
  video.muted = originalMutedState;
790
+ setAdPlayingFlag(false);
759
791
  if (adContainerEl) {
760
792
  adContainerEl.style.pointerEvents = "none";
761
793
  adContainerEl.style.display = "none";
@@ -798,6 +830,9 @@ function createImaController(video, options) {
798
830
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
799
831
  },
800
832
  updateOriginalMutedState(muted) {
833
+ console.log(
834
+ `[IMA] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
835
+ );
801
836
  originalMutedState = muted;
802
837
  },
803
838
  getOriginalMutedState() {
@@ -854,7 +889,10 @@ function createHlsAdPlayer(contentVideo, options) {
854
889
  try {
855
890
  fn(payload);
856
891
  } catch (error) {
857
- console.warn(`[HlsAdPlayer] Error in event listener for ${event}:`, error);
892
+ console.warn(
893
+ `[HlsAdPlayer] Error in event listener for ${event}:`,
894
+ error
895
+ );
858
896
  }
859
897
  }
860
898
  }
@@ -917,7 +955,9 @@ function createHlsAdPlayer(contentVideo, options) {
917
955
  }
918
956
  const mainQuality = getMainStreamQuality();
919
957
  if (!mainQuality) {
920
- console.log("[HlsAdPlayer] No main stream quality info, using first media file");
958
+ console.log(
959
+ "[HlsAdPlayer] No main stream quality info, using first media file"
960
+ );
921
961
  return firstFile;
922
962
  }
923
963
  console.log("[HlsAdPlayer] Main stream quality:", mainQuality);
@@ -953,7 +993,10 @@ function createHlsAdPlayer(contentVideo, options) {
953
993
  const xmlDoc = parser.parseFromString(xmlString, "text/xml");
954
994
  const parserError = xmlDoc.querySelector("parsererror");
955
995
  if (parserError) {
956
- console.error("[HlsAdPlayer] XML parsing error (malformed VAST XML):", parserError.textContent);
996
+ console.error(
997
+ "[HlsAdPlayer] XML parsing error (malformed VAST XML):",
998
+ parserError.textContent
999
+ );
957
1000
  return null;
958
1001
  }
959
1002
  const adElement = xmlDoc.querySelector("Ad");
@@ -969,17 +1012,23 @@ function createHlsAdPlayer(contentVideo, options) {
969
1012
  const duration = parseInt(durationParts[0] || "0", 10) * 3600 + parseInt(durationParts[1] || "0", 10) * 60 + parseInt(durationParts[2] || "0", 10);
970
1013
  const mediaFileElements = xmlDoc.querySelectorAll("MediaFile");
971
1014
  const mediaFiles = [];
972
- console.log(`[HlsAdPlayer] Found ${mediaFileElements.length} MediaFile element(s) in VAST XML`);
1015
+ console.log(
1016
+ `[HlsAdPlayer] Found ${mediaFileElements.length} MediaFile element(s) in VAST XML`
1017
+ );
973
1018
  mediaFileElements.forEach((mf, index) => {
974
1019
  var _a2;
975
1020
  const type = mf.getAttribute("type") || "";
976
1021
  const url = ((_a2 = mf.textContent) == null ? void 0 : _a2.trim()) || "";
977
1022
  const width = mf.getAttribute("width") || "";
978
1023
  const height = mf.getAttribute("height") || "";
979
- console.log(`[HlsAdPlayer] MediaFile ${index}: type="${type}", url="${url}", width="${width}", height="${height}"`);
1024
+ console.log(
1025
+ `[HlsAdPlayer] MediaFile ${index}: type="${type}", url="${url}", width="${width}", height="${height}"`
1026
+ );
980
1027
  if (type === "application/x-mpegURL" || type.includes("m3u8")) {
981
1028
  if (!url) {
982
- console.warn(`[HlsAdPlayer] MediaFile ${index} has HLS type but empty URL`);
1029
+ console.warn(
1030
+ `[HlsAdPlayer] MediaFile ${index} has HLS type but empty URL`
1031
+ );
983
1032
  return;
984
1033
  }
985
1034
  const bitrateAttr = mf.getAttribute("bitrate");
@@ -993,12 +1042,16 @@ function createHlsAdPlayer(contentVideo, options) {
993
1042
  });
994
1043
  console.log(`[HlsAdPlayer] Added HLS MediaFile: ${url}`);
995
1044
  } else {
996
- console.log(`[HlsAdPlayer] MediaFile ${index} ignored (type="${type}" is not HLS)`);
1045
+ console.log(
1046
+ `[HlsAdPlayer] MediaFile ${index} ignored (type="${type}" is not HLS)`
1047
+ );
997
1048
  }
998
1049
  });
999
1050
  if (mediaFiles.length === 0) {
1000
1051
  if (isNoAdAvailable) {
1001
- console.warn("[HlsAdPlayer] No ads available (VAST response indicates no ads)");
1052
+ console.warn(
1053
+ "[HlsAdPlayer] No ads available (VAST response indicates no ads)"
1054
+ );
1002
1055
  } else {
1003
1056
  console.warn("[HlsAdPlayer] No HLS media files found in VAST XML");
1004
1057
  }
@@ -1061,6 +1114,10 @@ function createHlsAdPlayer(contentVideo, options) {
1061
1114
  video.style.backgroundColor = "#000";
1062
1115
  video.playsInline = true;
1063
1116
  video.muted = false;
1117
+ video.volume = 1;
1118
+ console.log(
1119
+ `[HlsAdPlayer] Created ad video element with volume ${video.volume}`
1120
+ );
1064
1121
  return video;
1065
1122
  }
1066
1123
  function setupAdEventListeners() {
@@ -1120,10 +1177,22 @@ function createHlsAdPlayer(contentVideo, options) {
1120
1177
  }
1121
1178
  });
1122
1179
  }
1180
+ function setAdPlayingFlag(isPlaying) {
1181
+ if (isPlaying) {
1182
+ contentVideo.dataset.stormcloudAdPlaying = "true";
1183
+ } else {
1184
+ delete contentVideo.dataset.stormcloudAdPlaying;
1185
+ }
1186
+ }
1123
1187
  function handleAdComplete() {
1124
1188
  console.log("[HlsAdPlayer] Handling ad completion");
1125
1189
  adPlaying = false;
1190
+ setAdPlayingFlag(false);
1191
+ const previousMutedState = contentVideo.muted;
1126
1192
  contentVideo.muted = originalMutedState;
1193
+ console.log(
1194
+ `[HlsAdPlayer] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
1195
+ );
1127
1196
  if (adContainerEl) {
1128
1197
  adContainerEl.style.display = "none";
1129
1198
  adContainerEl.style.pointerEvents = "none";
@@ -1141,7 +1210,12 @@ function createHlsAdPlayer(contentVideo, options) {
1141
1210
  function handleAdError() {
1142
1211
  console.log("[HlsAdPlayer] Handling ad error");
1143
1212
  adPlaying = false;
1213
+ setAdPlayingFlag(false);
1214
+ const previousMutedState = contentVideo.muted;
1144
1215
  contentVideo.muted = originalMutedState;
1216
+ console.log(
1217
+ `[HlsAdPlayer] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
1218
+ );
1145
1219
  if (adContainerEl) {
1146
1220
  adContainerEl.style.display = "none";
1147
1221
  adContainerEl.style.pointerEvents = "none";
@@ -1178,7 +1252,9 @@ function createHlsAdPlayer(contentVideo, options) {
1178
1252
  async requestAds(vastTagUrl) {
1179
1253
  console.log("[HlsAdPlayer] Requesting ads:", vastTagUrl);
1180
1254
  if (adPlaying) {
1181
- console.warn("[HlsAdPlayer] Cannot request new ads while an ad is playing");
1255
+ console.warn(
1256
+ "[HlsAdPlayer] Cannot request new ads while an ad is playing"
1257
+ );
1182
1258
  return Promise.reject(new Error("Ad already playing"));
1183
1259
  }
1184
1260
  try {
@@ -1189,14 +1265,20 @@ function createHlsAdPlayer(contentVideo, options) {
1189
1265
  }
1190
1266
  const vastXml = await response.text();
1191
1267
  console.log("[HlsAdPlayer] VAST XML received");
1192
- console.log("[HlsAdPlayer] VAST XML content (first 2000 chars):", vastXml.substring(0, 2e3));
1268
+ console.log(
1269
+ "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1270
+ vastXml.substring(0, 2e3)
1271
+ );
1193
1272
  const ad = parseVastXml(vastXml);
1194
1273
  if (!ad) {
1195
1274
  console.warn("[HlsAdPlayer] No ads available from VAST response");
1275
+ emit("ad_error");
1196
1276
  return Promise.resolve();
1197
1277
  }
1198
1278
  currentAd = ad;
1199
- console.log(`[HlsAdPlayer] Ad parsed: ${ad.title}, duration: ${ad.duration}s`);
1279
+ console.log(
1280
+ `[HlsAdPlayer] Ad parsed: ${ad.title}, duration: ${ad.duration}s`
1281
+ );
1200
1282
  fireTrackingPixels(ad.trackingUrls.impression);
1201
1283
  trackingFired.impression = true;
1202
1284
  return Promise.resolve();
@@ -1208,7 +1290,9 @@ function createHlsAdPlayer(contentVideo, options) {
1208
1290
  },
1209
1291
  async play() {
1210
1292
  if (!currentAd) {
1211
- console.warn("[HlsAdPlayer] Cannot play: No ad loaded (no ads available)");
1293
+ console.warn(
1294
+ "[HlsAdPlayer] Cannot play: No ad loaded (no ads available)"
1295
+ );
1212
1296
  return Promise.reject(new Error("No ad loaded"));
1213
1297
  }
1214
1298
  console.log("[HlsAdPlayer] Starting ad playback");
@@ -1226,6 +1310,7 @@ function createHlsAdPlayer(contentVideo, options) {
1226
1310
  thirdQuartile: false,
1227
1311
  complete: false
1228
1312
  };
1313
+ const contentVolume = contentVideo.volume;
1229
1314
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
1230
1315
  contentVideo.pause();
1231
1316
  console.log("[HlsAdPlayer] Content paused (VOD mode)");
@@ -1234,6 +1319,15 @@ function createHlsAdPlayer(contentVideo, options) {
1234
1319
  }
1235
1320
  contentVideo.muted = true;
1236
1321
  adPlaying = true;
1322
+ setAdPlayingFlag(true);
1323
+ if (adVideoElement) {
1324
+ const adVolume = originalMutedState ? 0 : contentVolume;
1325
+ adVideoElement.volume = Math.max(0, Math.min(1, adVolume));
1326
+ adVideoElement.muted = false;
1327
+ console.log(
1328
+ `[HlsAdPlayer] Set ad video volume to ${adVideoElement.volume}, muted: ${adVideoElement.muted}, originalMutedState: ${originalMutedState}, contentVolume: ${contentVolume}`
1329
+ );
1330
+ }
1237
1331
  if (adContainerEl) {
1238
1332
  adContainerEl.style.display = "flex";
1239
1333
  adContainerEl.style.pointerEvents = "auto";
@@ -1286,6 +1380,7 @@ function createHlsAdPlayer(contentVideo, options) {
1286
1380
  async stop() {
1287
1381
  console.log("[HlsAdPlayer] Stopping ad");
1288
1382
  adPlaying = false;
1383
+ setAdPlayingFlag(false);
1289
1384
  contentVideo.muted = originalMutedState;
1290
1385
  if (adContainerEl) {
1291
1386
  adContainerEl.style.display = "none";
@@ -1308,6 +1403,7 @@ function createHlsAdPlayer(contentVideo, options) {
1308
1403
  destroy() {
1309
1404
  console.log("[HlsAdPlayer] Destroying");
1310
1405
  adPlaying = false;
1406
+ setAdPlayingFlag(false);
1311
1407
  contentVideo.muted = originalMutedState;
1312
1408
  if (adHls) {
1313
1409
  adHls.destroy();
@@ -1349,6 +1445,9 @@ function createHlsAdPlayer(contentVideo, options) {
1349
1445
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
1350
1446
  },
1351
1447
  updateOriginalMutedState(muted) {
1448
+ console.log(
1449
+ `[HlsAdPlayer] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
1450
+ );
1352
1451
  originalMutedState = muted;
1353
1452
  },
1354
1453
  getOriginalMutedState() {
@@ -1841,6 +1940,9 @@ var StormcloudVideoPlayer = class {
1841
1940
  this.isLiveStream = false;
1842
1941
  this.nativeHlsMode = false;
1843
1942
  this.videoSrcProtection = null;
1943
+ this.bufferedSegmentsCount = 0;
1944
+ this.shouldAutoplayAfterBuffering = false;
1945
+ this.hasInitialBufferCompleted = false;
1844
1946
  initializePolyfills();
1845
1947
  const browserOverrides = getBrowserConfigOverrides();
1846
1948
  this.config = { ...config, ...browserOverrides };
@@ -1927,14 +2029,22 @@ var StormcloudVideoPlayer = class {
1927
2029
  liveDurationInfinity: true,
1928
2030
  lowLatencyMode: !!this.config.lowLatencyMode,
1929
2031
  maxLiveSyncPlaybackRate: this.config.lowLatencyMode ? 1.5 : 1,
1930
- ...this.config.lowLatencyMode ? { liveSyncDuration: 2 } : {}
2032
+ ...this.config.lowLatencyMode ? { liveSyncDuration: 2 } : {},
2033
+ maxBufferLength: 30,
2034
+ maxMaxBufferLength: 600,
2035
+ maxBufferSize: 60 * 1e3 * 1e3,
2036
+ maxBufferHole: 0.5,
2037
+ highBufferWatchdogPeriod: 2,
2038
+ nudgeOffset: 0.1,
2039
+ nudgeMaxRetry: 3,
2040
+ startPosition: -1
1931
2041
  });
1932
2042
  this.hls.on(import_hls2.default.Events.MEDIA_ATTACHED, () => {
1933
2043
  var _a2;
1934
2044
  (_a2 = this.hls) == null ? void 0 : _a2.loadSource(this.config.src);
1935
2045
  });
1936
2046
  this.hls.on(import_hls2.default.Events.MANIFEST_PARSED, async (_, data) => {
1937
- var _a2, _b2, _c, _d;
2047
+ var _a2, _b2, _c, _d, _e;
1938
2048
  this.isLiveStream = (_c = (_b2 = (_a2 = this.hls) == null ? void 0 : _a2.levels) == null ? void 0 : _b2.some(
1939
2049
  (level) => {
1940
2050
  var _a3, _b3;
@@ -1952,9 +2062,51 @@ var StormcloudVideoPlayer = class {
1952
2062
  this.ima.destroy();
1953
2063
  this.ima = this.createAdPlayer(this.shouldContinueLiveStreamDuringAds());
1954
2064
  this.ima.initialize();
1955
- if (this.config.autoplay) {
1956
- await ((_d = this.video.play()) == null ? void 0 : _d.catch(() => {
1957
- }));
2065
+ this.bufferedSegmentsCount = 0;
2066
+ this.hasInitialBufferCompleted = false;
2067
+ this.shouldAutoplayAfterBuffering = !!this.config.autoplay;
2068
+ const minSegments = (_d = this.config.minSegmentsBeforePlay) != null ? _d : 2;
2069
+ if (this.config.debugAdTiming) {
2070
+ console.log(
2071
+ "[StormcloudVideoPlayer] Waiting for",
2072
+ minSegments,
2073
+ "segments to buffer before playback"
2074
+ );
2075
+ }
2076
+ if (minSegments === 0 || !this.config.autoplay) {
2077
+ this.hasInitialBufferCompleted = true;
2078
+ if (this.config.autoplay) {
2079
+ await ((_e = this.video.play()) == null ? void 0 : _e.catch(() => {
2080
+ }));
2081
+ }
2082
+ }
2083
+ });
2084
+ this.hls.on(import_hls2.default.Events.FRAG_BUFFERED, async (_evt, data) => {
2085
+ var _a2, _b2;
2086
+ if (this.hasInitialBufferCompleted) {
2087
+ return;
2088
+ }
2089
+ this.bufferedSegmentsCount++;
2090
+ const minSegments = (_a2 = this.config.minSegmentsBeforePlay) != null ? _a2 : 2;
2091
+ if (this.config.debugAdTiming) {
2092
+ console.log(
2093
+ `[StormcloudVideoPlayer] Buffered segment ${this.bufferedSegmentsCount}/${minSegments}`
2094
+ );
2095
+ }
2096
+ if (this.bufferedSegmentsCount >= minSegments) {
2097
+ this.hasInitialBufferCompleted = true;
2098
+ if (this.shouldAutoplayAfterBuffering) {
2099
+ if (this.config.debugAdTiming) {
2100
+ console.log(
2101
+ `[StormcloudVideoPlayer] Initial buffer complete (${this.bufferedSegmentsCount} segments). Starting playback.`
2102
+ );
2103
+ }
2104
+ await ((_b2 = this.video.play()) == null ? void 0 : _b2.catch((err) => {
2105
+ if (this.config.debugAdTiming) {
2106
+ console.warn("[StormcloudVideoPlayer] Autoplay failed:", err);
2107
+ }
2108
+ }));
2109
+ }
1958
2110
  }
1959
2111
  });
1960
2112
  this.hls.on(import_hls2.default.Events.FRAG_PARSING_METADATA, (_evt, data) => {
@@ -2050,6 +2202,7 @@ var StormcloudVideoPlayer = class {
2050
2202
  this.video.autoplay = !!this.config.autoplay;
2051
2203
  this.video.muted = !!this.config.muted;
2052
2204
  this.ima.initialize();
2205
+ this.ima.updateOriginalMutedState(this.video.muted);
2053
2206
  this.ima.on("all_ads_completed", () => {
2054
2207
  if (this.config.debugAdTiming) {
2055
2208
  console.log(
@@ -2787,14 +2940,66 @@ var StormcloudVideoPlayer = class {
2787
2940
  }
2788
2941
  }
2789
2942
  ensureAdStoppedByTimer() {
2943
+ var _a, _b;
2790
2944
  if (!this.inAdBreak) return;
2945
+ this.adStopTimerId = void 0;
2946
+ const adPlaying = this.ima.isAdPlaying();
2947
+ const pendingAds = this.adPodQueue.length > 0;
2948
+ const checkIntervalMs = Math.max(
2949
+ 250,
2950
+ Math.floor((_a = this.config.adBreakCheckIntervalMs) != null ? _a : 1e3)
2951
+ );
2952
+ const maxExtensionMsConfig = this.config.maxAdBreakExtensionMs;
2953
+ const maxExtensionMs = typeof maxExtensionMsConfig === "number" && maxExtensionMsConfig > 0 ? maxExtensionMsConfig : 6e4;
2954
+ let elapsedSinceStartMs = 0;
2955
+ if (this.currentAdBreakStartWallClockMs != null) {
2956
+ elapsedSinceStartMs = Date.now() - this.currentAdBreakStartWallClockMs;
2957
+ }
2958
+ const expectedDurationMs = (_b = this.expectedAdBreakDurationMs) != null ? _b : 0;
2959
+ const overrunMs = Math.max(0, elapsedSinceStartMs - expectedDurationMs);
2960
+ const shouldExtendAdBreak = (adPlaying || pendingAds || this.showAds) && overrunMs < maxExtensionMs;
2961
+ if (shouldExtendAdBreak) {
2962
+ if (this.config.debugAdTiming) {
2963
+ console.log(
2964
+ "[StormcloudVideoPlayer] Extending ad break beyond scheduled duration",
2965
+ {
2966
+ adPlaying,
2967
+ pendingAds,
2968
+ showAds: this.showAds,
2969
+ overrunMs,
2970
+ checkIntervalMs,
2971
+ maxExtensionMs
2972
+ }
2973
+ );
2974
+ }
2975
+ this.scheduleAdStopCountdown(checkIntervalMs);
2976
+ return;
2977
+ }
2978
+ if (this.config.debugAdTiming) {
2979
+ console.log("[StormcloudVideoPlayer] Ending ad break via timer", {
2980
+ adPlaying,
2981
+ pendingAds,
2982
+ showAds: this.showAds,
2983
+ overrunMs,
2984
+ maxExtensionMs
2985
+ });
2986
+ }
2791
2987
  this.inAdBreak = false;
2792
2988
  this.expectedAdBreakDurationMs = void 0;
2793
2989
  this.currentAdBreakStartWallClockMs = void 0;
2794
- this.adStopTimerId = void 0;
2795
- if (this.ima.isAdPlaying()) {
2990
+ this.showAds = false;
2991
+ this.adPodQueue = [];
2992
+ this.currentAdIndex = 0;
2993
+ this.totalAdsInBreak = 0;
2994
+ this.clearAdFailsafeTimer();
2995
+ if (adPlaying) {
2796
2996
  this.ima.stop().catch(() => {
2797
2997
  });
2998
+ return;
2999
+ }
3000
+ const originalMutedState = this.ima.getOriginalMutedState();
3001
+ if (this.video.muted !== originalMutedState) {
3002
+ this.video.muted = originalMutedState;
2798
3003
  }
2799
3004
  }
2800
3005
  scheduleAdStartIn(delayMs) {
@@ -2834,7 +3039,23 @@ var StormcloudVideoPlayer = class {
2834
3039
  }
2835
3040
  return;
2836
3041
  }
2837
- this.ima.updateOriginalMutedState(this.video.muted);
3042
+ if (!this.showAds) {
3043
+ if (this.config.debugAdTiming) {
3044
+ console.log(
3045
+ `[StormcloudVideoPlayer] Capturing original state before ad request:`,
3046
+ {
3047
+ videoMuted: this.video.muted,
3048
+ videoVolume: this.video.volume,
3049
+ showAds: this.showAds
3050
+ }
3051
+ );
3052
+ }
3053
+ this.ima.updateOriginalMutedState(this.video.muted);
3054
+ } else if (this.config.debugAdTiming) {
3055
+ console.log(
3056
+ `[StormcloudVideoPlayer] Keeping existing original mute state (currently showing ads)`
3057
+ );
3058
+ }
2838
3059
  this.startAdFailsafeTimer();
2839
3060
  try {
2840
3061
  await this.ima.requestAds(vastTagUrl);
@@ -2889,11 +3110,12 @@ var StormcloudVideoPlayer = class {
2889
3110
  this.showAds = false;
2890
3111
  this.currentAdIndex = 0;
2891
3112
  this.totalAdsInBreak = 0;
3113
+ const currentMutedState = this.video.muted;
2892
3114
  const originalMutedState = this.ima.getOriginalMutedState();
2893
3115
  this.video.muted = originalMutedState;
2894
3116
  if (this.config.debugAdTiming) {
2895
3117
  console.log(
2896
- `[StormcloudVideoPlayer] Restored mute state to: ${originalMutedState}`
3118
+ `[StormcloudVideoPlayer] Restored mute state: ${currentMutedState} -> ${originalMutedState}`
2897
3119
  );
2898
3120
  }
2899
3121
  if (this.video.paused) {
@@ -2985,6 +3207,7 @@ var StormcloudVideoPlayer = class {
2985
3207
  }
2986
3208
  } else {
2987
3209
  this.video.muted = !this.video.muted;
3210
+ this.ima.updateOriginalMutedState(this.video.muted);
2988
3211
  if (this.config.debugAdTiming) {
2989
3212
  console.log("[StormcloudVideoPlayer] Muted:", this.video.muted);
2990
3213
  }
@@ -3029,11 +3252,47 @@ var StormcloudVideoPlayer = class {
3029
3252
  }
3030
3253
  isMuted() {
3031
3254
  if (this.ima.isAdPlaying()) {
3032
- const adVolume = this.ima.getAdVolume();
3033
- return adVolume === 0;
3255
+ if (this.config.debugAdTiming) {
3256
+ console.log(
3257
+ "[StormcloudVideoPlayer] isMuted() override during ad playback -> false"
3258
+ );
3259
+ }
3260
+ return false;
3261
+ }
3262
+ if (this.config.debugAdTiming) {
3263
+ console.log(
3264
+ `[StormcloudVideoPlayer] isMuted() no ad playing: video.muted=${this.video.muted}`
3265
+ );
3034
3266
  }
3035
3267
  return this.video.muted;
3036
3268
  }
3269
+ setMuted(muted) {
3270
+ const adPlaying = this.ima.isAdPlaying();
3271
+ if (adPlaying && muted === this.video.muted) {
3272
+ if (this.config.debugAdTiming) {
3273
+ console.log(
3274
+ "[StormcloudVideoPlayer] setMuted reflective update during ad ignored",
3275
+ { muted }
3276
+ );
3277
+ }
3278
+ return;
3279
+ }
3280
+ this.video.muted = muted;
3281
+ if (adPlaying) {
3282
+ this.ima.updateOriginalMutedState(muted);
3283
+ this.ima.setAdVolume(muted ? 0 : 1);
3284
+ if (this.config.debugAdTiming) {
3285
+ console.log("[StormcloudVideoPlayer] setMuted applied during ad", {
3286
+ muted
3287
+ });
3288
+ }
3289
+ return;
3290
+ }
3291
+ this.ima.updateOriginalMutedState(muted);
3292
+ if (this.config.debugAdTiming) {
3293
+ console.log("[StormcloudVideoPlayer] setMuted called:", muted);
3294
+ }
3295
+ }
3037
3296
  isFullscreen() {
3038
3297
  return !!document.fullscreenElement;
3039
3298
  }
@@ -3112,10 +3371,12 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3112
3371
  vastMode,
3113
3372
  vastTagUrl,
3114
3373
  adPlayerType,
3374
+ minSegmentsBeforePlay,
3115
3375
  ...restVideoAttrs
3116
3376
  } = props;
3117
3377
  const videoRef = (0, import_react.useRef)(null);
3118
3378
  const playerRef = (0, import_react.useRef)(null);
3379
+ const bufferingTimeoutRef = (0, import_react.useRef)(null);
3119
3380
  const [adStatus, setAdStatus] = import_react.default.useState({ showAds: false, currentIndex: 0, totalAds: 0 });
3120
3381
  const [shouldShowNativeControls, setShouldShowNativeControls] = import_react.default.useState(true);
3121
3382
  const [isMuted, setIsMuted] = import_react.default.useState(false);
@@ -3237,6 +3498,9 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3237
3498
  return;
3238
3499
  }
3239
3500
  setShowLicenseWarning(false);
3501
+ if (debugAdTiming) {
3502
+ console.log("[StormcloudUI] Initializing player, isLoading=true");
3503
+ }
3240
3504
  if (playerRef.current) {
3241
3505
  try {
3242
3506
  playerRef.current.destroy();
@@ -3267,17 +3531,25 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3267
3531
  if (vastMode !== void 0) cfg.vastMode = vastMode;
3268
3532
  if (vastTagUrl !== void 0) cfg.vastTagUrl = vastTagUrl;
3269
3533
  if (adPlayerType !== void 0) cfg.adPlayerType = adPlayerType;
3534
+ if (minSegmentsBeforePlay !== void 0)
3535
+ cfg.minSegmentsBeforePlay = minSegmentsBeforePlay;
3270
3536
  const player = new StormcloudVideoPlayer(cfg);
3271
3537
  playerRef.current = player;
3272
3538
  player.load().then(() => {
3273
3539
  const showNative = player.shouldShowNativeControls();
3274
3540
  setShouldShowNativeControls(showNative);
3541
+ if (debugAdTiming) {
3542
+ console.log(
3543
+ "[StormcloudUI] Player loaded successfully, waiting for video ready"
3544
+ );
3545
+ }
3275
3546
  onReady == null ? void 0 : onReady(player);
3276
3547
  }).catch((error) => {
3277
3548
  console.error(
3278
3549
  "StormcloudVideoPlayer: Failed to load player:",
3279
3550
  error
3280
3551
  );
3552
+ setIsLoading(false);
3281
3553
  onReady == null ? void 0 : onReady(player);
3282
3554
  });
3283
3555
  return () => {
@@ -3294,8 +3566,8 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3294
3566
  if (autoplay !== void 0 && playerRef.current.videoElement) {
3295
3567
  playerRef.current.videoElement.autoplay = autoplay;
3296
3568
  }
3297
- if (muted !== void 0 && playerRef.current.videoElement) {
3298
- playerRef.current.videoElement.muted = muted;
3569
+ if (muted !== void 0) {
3570
+ playerRef.current.setMuted(muted);
3299
3571
  }
3300
3572
  } catch (error) {
3301
3573
  console.warn("Failed to update player properties:", error);
@@ -3376,26 +3648,108 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3376
3648
  (0, import_react.useEffect)(() => {
3377
3649
  if (!videoRef.current) return;
3378
3650
  const handleLoadedMetadata = () => {
3651
+ var _a;
3379
3652
  if (videoRef.current) {
3380
3653
  const video2 = videoRef.current;
3381
3654
  void video2.offsetHeight;
3382
3655
  }
3656
+ if (debugAdTiming) {
3657
+ console.log(
3658
+ "[StormcloudUI] Video event: loadedmetadata, readyState:",
3659
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3660
+ );
3661
+ }
3662
+ };
3663
+ const handleLoadedData = () => {
3664
+ var _a;
3665
+ if (debugAdTiming) {
3666
+ console.log(
3667
+ "[StormcloudUI] Video event: loadeddata, readyState:",
3668
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3669
+ );
3670
+ }
3383
3671
  };
3384
3672
  const handleLoadStart = () => {
3385
- setIsLoading(true);
3386
- setIsBuffering(false);
3673
+ var _a;
3674
+ if (debugAdTiming) {
3675
+ console.log(
3676
+ "[StormcloudUI] Video event: loadstart, readyState:",
3677
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3678
+ );
3679
+ }
3387
3680
  };
3388
3681
  const handleCanPlay = () => {
3682
+ var _a;
3683
+ setIsLoading(false);
3684
+ if (bufferingTimeoutRef.current) {
3685
+ clearTimeout(bufferingTimeoutRef.current);
3686
+ bufferingTimeoutRef.current = null;
3687
+ }
3688
+ setIsBuffering(false);
3689
+ if (debugAdTiming) {
3690
+ console.log(
3691
+ "[StormcloudUI] Video event: canplay, readyState:",
3692
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3693
+ "- clearing loading state, isLoading=false"
3694
+ );
3695
+ }
3696
+ };
3697
+ const handleCanPlayThrough = () => {
3698
+ var _a;
3389
3699
  setIsLoading(false);
3700
+ if (bufferingTimeoutRef.current) {
3701
+ clearTimeout(bufferingTimeoutRef.current);
3702
+ bufferingTimeoutRef.current = null;
3703
+ }
3390
3704
  setIsBuffering(false);
3705
+ if (debugAdTiming) {
3706
+ console.log(
3707
+ "[StormcloudUI] Video event: canplaythrough, readyState:",
3708
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3709
+ "- clearing loading state, isLoading=false"
3710
+ );
3711
+ }
3391
3712
  };
3392
3713
  const handleWaiting = () => {
3393
- setIsBuffering(true);
3714
+ var _a;
3715
+ if (bufferingTimeoutRef.current) {
3716
+ clearTimeout(bufferingTimeoutRef.current);
3717
+ }
3718
+ bufferingTimeoutRef.current = window.setTimeout(() => {
3719
+ var _a2;
3720
+ setIsBuffering(true);
3721
+ if (debugAdTiming) {
3722
+ console.log(
3723
+ "[StormcloudUI] Video buffering detected (after 300ms delay), readyState:",
3724
+ (_a2 = videoRef.current) == null ? void 0 : _a2.readyState,
3725
+ "- showing spinner, isBuffering=true"
3726
+ );
3727
+ }
3728
+ }, 300);
3729
+ if (debugAdTiming) {
3730
+ console.log(
3731
+ "[StormcloudUI] Video event: waiting, readyState:",
3732
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3733
+ "- buffering delay started (300ms)"
3734
+ );
3735
+ }
3394
3736
  };
3395
3737
  const handlePlaying = () => {
3738
+ var _a;
3396
3739
  setIsLoading(false);
3740
+ if (bufferingTimeoutRef.current) {
3741
+ clearTimeout(bufferingTimeoutRef.current);
3742
+ bufferingTimeoutRef.current = null;
3743
+ }
3397
3744
  setIsBuffering(false);
3398
3745
  setShowCenterPlay(false);
3746
+ if (debugAdTiming) {
3747
+ console.log(
3748
+ "[StormcloudUI] Video event: playing, readyState:",
3749
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3750
+ "- playback started, isLoading=false, isBuffering=false"
3751
+ );
3752
+ }
3399
3753
  };
3400
3754
  const handlePause = () => {
3401
3755
  if (playerRef.current && !playerRef.current.isShowingAds()) {
@@ -3410,8 +3764,9 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3410
3764
  const video = videoRef.current;
3411
3765
  video.addEventListener("loadstart", handleLoadStart);
3412
3766
  video.addEventListener("loadedmetadata", handleLoadedMetadata);
3413
- video.addEventListener("loadeddata", handleLoadedMetadata);
3767
+ video.addEventListener("loadeddata", handleLoadedData);
3414
3768
  video.addEventListener("canplay", handleCanPlay);
3769
+ video.addEventListener("canplaythrough", handleCanPlayThrough);
3415
3770
  video.addEventListener("waiting", handleWaiting);
3416
3771
  video.addEventListener("playing", handlePlaying);
3417
3772
  video.addEventListener("pause", handlePause);
@@ -3420,16 +3775,21 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3420
3775
  setShowCenterPlay(true);
3421
3776
  }
3422
3777
  return () => {
3778
+ if (bufferingTimeoutRef.current) {
3779
+ clearTimeout(bufferingTimeoutRef.current);
3780
+ bufferingTimeoutRef.current = null;
3781
+ }
3423
3782
  video.removeEventListener("loadstart", handleLoadStart);
3424
3783
  video.removeEventListener("loadedmetadata", handleLoadedMetadata);
3425
- video.removeEventListener("loadeddata", handleLoadedMetadata);
3784
+ video.removeEventListener("loadeddata", handleLoadedData);
3426
3785
  video.removeEventListener("canplay", handleCanPlay);
3786
+ video.removeEventListener("canplaythrough", handleCanPlayThrough);
3427
3787
  video.removeEventListener("waiting", handleWaiting);
3428
3788
  video.removeEventListener("playing", handlePlaying);
3429
3789
  video.removeEventListener("pause", handlePause);
3430
3790
  video.removeEventListener("ended", handleEnded);
3431
3791
  };
3432
- }, []);
3792
+ }, [debugAdTiming]);
3433
3793
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
3434
3794
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
3435
3795
  @keyframes spin {
@@ -3536,35 +3896,18 @@ var StormcloudVideoPlayerComponent = import_react.default.memo(
3536
3896
  }
3537
3897
  ),
3538
3898
  (isLoading || isBuffering) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3539
- "div",
3899
+ import_fa.FaSpinner,
3540
3900
  {
3901
+ size: 42,
3902
+ color: "white",
3541
3903
  style: {
3542
3904
  position: "absolute",
3543
- top: "50%",
3544
- left: "50%",
3545
- transform: "translate(-50%, -50%)",
3905
+ top: "calc(50% - 21px)",
3906
+ left: "calc(50% - 21px)",
3546
3907
  zIndex: 20,
3547
- display: "flex",
3548
- alignItems: "center",
3549
- justifyContent: "center",
3550
- background: "linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(20, 20, 20, 0.6) 100%)",
3551
- width: "80px",
3552
- height: "80px",
3553
- borderRadius: "50%",
3554
- backdropFilter: "blur(20px)",
3555
- boxShadow: "0 12px 40px rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.1)"
3556
- },
3557
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3558
- import_fa.FaSpinner,
3559
- {
3560
- size: 28,
3561
- color: "white",
3562
- style: {
3563
- animation: "spin 1s linear infinite",
3564
- filter: "drop-shadow(0 3px 6px rgba(0, 0, 0, 0.8))"
3565
- }
3566
- }
3567
- )
3908
+ animation: "spin 1s linear infinite",
3909
+ filter: "drop-shadow(0 3px 6px rgba(0, 0, 0, 0.8))"
3910
+ }
3568
3911
  }
3569
3912
  ),
3570
3913
  showLicenseWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -4677,6 +5020,7 @@ var defaultProps = {
4677
5020
  showCustomControls: false,
4678
5021
  licenseKey: "",
4679
5022
  adFailsafeTimeoutMs: 1e4,
5023
+ minSegmentsBeforePlay: 2,
4680
5024
  onStart: noop,
4681
5025
  onPlay: noop,
4682
5026
  onPause: noop,
@@ -4850,6 +5194,8 @@ var HlsPlayer = class extends import_react3.Component {
4850
5194
  config.licenseKey = this.props.licenseKey;
4851
5195
  if (this.props.adFailsafeTimeoutMs !== void 0)
4852
5196
  config.adFailsafeTimeoutMs = this.props.adFailsafeTimeoutMs;
5197
+ if (this.props.minSegmentsBeforePlay !== void 0)
5198
+ config.minSegmentsBeforePlay = this.props.minSegmentsBeforePlay;
4853
5199
  this.player = new StormcloudVideoPlayer(config);
4854
5200
  (_b = (_a = this.props).onMount) == null ? void 0 : _b.call(_a, this);
4855
5201
  await this.player.load();
@@ -5456,6 +5802,7 @@ var SUPPORTED_PROPS = [
5456
5802
  "showCustomControls",
5457
5803
  "licenseKey",
5458
5804
  "adFailsafeTimeoutMs",
5805
+ "minSegmentsBeforePlay",
5459
5806
  "onReady",
5460
5807
  "onStart",
5461
5808
  "onPlay",