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.js CHANGED
@@ -198,6 +198,13 @@ function createImaController(video, options) {
198
198
  let adPlaying = false;
199
199
  let originalMutedState = false;
200
200
  const listeners = /* @__PURE__ */ new Map();
201
+ function setAdPlayingFlag(isPlaying) {
202
+ if (isPlaying) {
203
+ video.dataset.stormcloudAdPlaying = "true";
204
+ } else {
205
+ delete video.dataset.stormcloudAdPlaying;
206
+ }
207
+ }
201
208
  function emit(event, payload) {
202
209
  const set = listeners.get(event);
203
210
  if (!set) return;
@@ -440,7 +447,12 @@ function createImaController(video, options) {
440
447
  console.error("[IMA] Ad error:", errorEvent.getError());
441
448
  destroyAdsManager();
442
449
  adPlaying = false;
450
+ const previousMutedState = video.muted;
443
451
  video.muted = originalMutedState;
452
+ setAdPlayingFlag(false);
453
+ console.log(
454
+ `[IMA] Restored mute state after ad error: ${previousMutedState} -> ${originalMutedState}`
455
+ );
444
456
  if (adContainerEl) {
445
457
  adContainerEl.style.pointerEvents = "none";
446
458
  adContainerEl.style.display = "none";
@@ -493,11 +505,13 @@ function createImaController(video, options) {
493
505
  }
494
506
  video.muted = true;
495
507
  adPlaying = true;
508
+ setAdPlayingFlag(true);
496
509
  emit("content_pause");
497
510
  }
498
511
  );
499
512
  adsManager.addEventListener(AdEvent.STARTED, () => {
500
513
  console.log("[IMA] Ad started playing");
514
+ setAdPlayingFlag(true);
501
515
  if (adContainerEl) {
502
516
  adContainerEl.style.pointerEvents = "auto";
503
517
  adContainerEl.style.display = "flex";
@@ -513,6 +527,7 @@ function createImaController(video, options) {
513
527
  console.log("[IMA] Content resume requested");
514
528
  adPlaying = false;
515
529
  video.muted = originalMutedState;
530
+ setAdPlayingFlag(false);
516
531
  if (adContainerEl) {
517
532
  adContainerEl.style.pointerEvents = "none";
518
533
  adContainerEl.style.display = "none";
@@ -536,6 +551,7 @@ function createImaController(video, options) {
536
551
  console.log("[IMA] All ads completed");
537
552
  adPlaying = false;
538
553
  video.muted = originalMutedState;
554
+ setAdPlayingFlag(false);
539
555
  if (adContainerEl) {
540
556
  adContainerEl.style.pointerEvents = "none";
541
557
  adContainerEl.style.display = "none";
@@ -566,6 +582,7 @@ function createImaController(video, options) {
566
582
  console.error("[IMA] Error setting up ads manager:", e);
567
583
  adPlaying = false;
568
584
  video.muted = originalMutedState;
585
+ setAdPlayingFlag(false);
569
586
  if (adContainerEl) {
570
587
  adContainerEl.style.pointerEvents = "none";
571
588
  adContainerEl.style.display = "none";
@@ -595,7 +612,12 @@ function createImaController(video, options) {
595
612
  (adErrorEvent) => {
596
613
  console.error("[IMA] Ads loader error:", adErrorEvent.getError());
597
614
  adPlaying = false;
615
+ const previousMutedState = video.muted;
598
616
  video.muted = originalMutedState;
617
+ setAdPlayingFlag(false);
618
+ console.log(
619
+ `[IMA] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
620
+ );
599
621
  if (adContainerEl) {
600
622
  adContainerEl.style.pointerEvents = "none";
601
623
  adContainerEl.style.display = "none";
@@ -647,12 +669,20 @@ function createImaController(video, options) {
647
669
  console.log(`[IMA] Initializing ads manager (${width}x${height})`);
648
670
  adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
649
671
  adPlaying = true;
672
+ const adVolume = originalMutedState ? 0 : video.volume;
673
+ try {
674
+ adsManager.setVolume(adVolume);
675
+ console.log(`[IMA] Set ad volume to ${adVolume}`);
676
+ } catch (error) {
677
+ console.warn("[IMA] Failed to set ad volume:", error);
678
+ }
650
679
  console.log("[IMA] Starting ad playback");
651
680
  adsManager.start();
652
681
  return Promise.resolve();
653
682
  } catch (error) {
654
683
  console.error("[IMA] Error starting ad playback:", error);
655
684
  adPlaying = false;
685
+ setAdPlayingFlag(false);
656
686
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
657
687
  (_b = video.play()) == null ? void 0 : _b.catch(() => {
658
688
  });
@@ -664,6 +694,7 @@ function createImaController(video, options) {
664
694
  var _a;
665
695
  adPlaying = false;
666
696
  video.muted = originalMutedState;
697
+ setAdPlayingFlag(false);
667
698
  if (adContainerEl) {
668
699
  adContainerEl.style.pointerEvents = "none";
669
700
  adContainerEl.style.display = "none";
@@ -687,6 +718,7 @@ function createImaController(video, options) {
687
718
  destroyAdsManager();
688
719
  adPlaying = false;
689
720
  video.muted = originalMutedState;
721
+ setAdPlayingFlag(false);
690
722
  if (adContainerEl) {
691
723
  adContainerEl.style.pointerEvents = "none";
692
724
  adContainerEl.style.display = "none";
@@ -729,6 +761,9 @@ function createImaController(video, options) {
729
761
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
730
762
  },
731
763
  updateOriginalMutedState(muted) {
764
+ console.log(
765
+ `[IMA] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
766
+ );
732
767
  originalMutedState = muted;
733
768
  },
734
769
  getOriginalMutedState() {
@@ -785,7 +820,10 @@ function createHlsAdPlayer(contentVideo, options) {
785
820
  try {
786
821
  fn(payload);
787
822
  } catch (error) {
788
- console.warn(`[HlsAdPlayer] Error in event listener for ${event}:`, error);
823
+ console.warn(
824
+ `[HlsAdPlayer] Error in event listener for ${event}:`,
825
+ error
826
+ );
789
827
  }
790
828
  }
791
829
  }
@@ -848,7 +886,9 @@ function createHlsAdPlayer(contentVideo, options) {
848
886
  }
849
887
  const mainQuality = getMainStreamQuality();
850
888
  if (!mainQuality) {
851
- console.log("[HlsAdPlayer] No main stream quality info, using first media file");
889
+ console.log(
890
+ "[HlsAdPlayer] No main stream quality info, using first media file"
891
+ );
852
892
  return firstFile;
853
893
  }
854
894
  console.log("[HlsAdPlayer] Main stream quality:", mainQuality);
@@ -884,7 +924,10 @@ function createHlsAdPlayer(contentVideo, options) {
884
924
  const xmlDoc = parser.parseFromString(xmlString, "text/xml");
885
925
  const parserError = xmlDoc.querySelector("parsererror");
886
926
  if (parserError) {
887
- console.error("[HlsAdPlayer] XML parsing error (malformed VAST XML):", parserError.textContent);
927
+ console.error(
928
+ "[HlsAdPlayer] XML parsing error (malformed VAST XML):",
929
+ parserError.textContent
930
+ );
888
931
  return null;
889
932
  }
890
933
  const adElement = xmlDoc.querySelector("Ad");
@@ -900,17 +943,23 @@ function createHlsAdPlayer(contentVideo, options) {
900
943
  const duration = parseInt(durationParts[0] || "0", 10) * 3600 + parseInt(durationParts[1] || "0", 10) * 60 + parseInt(durationParts[2] || "0", 10);
901
944
  const mediaFileElements = xmlDoc.querySelectorAll("MediaFile");
902
945
  const mediaFiles = [];
903
- console.log(`[HlsAdPlayer] Found ${mediaFileElements.length} MediaFile element(s) in VAST XML`);
946
+ console.log(
947
+ `[HlsAdPlayer] Found ${mediaFileElements.length} MediaFile element(s) in VAST XML`
948
+ );
904
949
  mediaFileElements.forEach((mf, index) => {
905
950
  var _a2;
906
951
  const type = mf.getAttribute("type") || "";
907
952
  const url = ((_a2 = mf.textContent) == null ? void 0 : _a2.trim()) || "";
908
953
  const width = mf.getAttribute("width") || "";
909
954
  const height = mf.getAttribute("height") || "";
910
- console.log(`[HlsAdPlayer] MediaFile ${index}: type="${type}", url="${url}", width="${width}", height="${height}"`);
955
+ console.log(
956
+ `[HlsAdPlayer] MediaFile ${index}: type="${type}", url="${url}", width="${width}", height="${height}"`
957
+ );
911
958
  if (type === "application/x-mpegURL" || type.includes("m3u8")) {
912
959
  if (!url) {
913
- console.warn(`[HlsAdPlayer] MediaFile ${index} has HLS type but empty URL`);
960
+ console.warn(
961
+ `[HlsAdPlayer] MediaFile ${index} has HLS type but empty URL`
962
+ );
914
963
  return;
915
964
  }
916
965
  const bitrateAttr = mf.getAttribute("bitrate");
@@ -924,12 +973,16 @@ function createHlsAdPlayer(contentVideo, options) {
924
973
  });
925
974
  console.log(`[HlsAdPlayer] Added HLS MediaFile: ${url}`);
926
975
  } else {
927
- console.log(`[HlsAdPlayer] MediaFile ${index} ignored (type="${type}" is not HLS)`);
976
+ console.log(
977
+ `[HlsAdPlayer] MediaFile ${index} ignored (type="${type}" is not HLS)`
978
+ );
928
979
  }
929
980
  });
930
981
  if (mediaFiles.length === 0) {
931
982
  if (isNoAdAvailable) {
932
- console.warn("[HlsAdPlayer] No ads available (VAST response indicates no ads)");
983
+ console.warn(
984
+ "[HlsAdPlayer] No ads available (VAST response indicates no ads)"
985
+ );
933
986
  } else {
934
987
  console.warn("[HlsAdPlayer] No HLS media files found in VAST XML");
935
988
  }
@@ -992,6 +1045,10 @@ function createHlsAdPlayer(contentVideo, options) {
992
1045
  video.style.backgroundColor = "#000";
993
1046
  video.playsInline = true;
994
1047
  video.muted = false;
1048
+ video.volume = 1;
1049
+ console.log(
1050
+ `[HlsAdPlayer] Created ad video element with volume ${video.volume}`
1051
+ );
995
1052
  return video;
996
1053
  }
997
1054
  function setupAdEventListeners() {
@@ -1051,10 +1108,22 @@ function createHlsAdPlayer(contentVideo, options) {
1051
1108
  }
1052
1109
  });
1053
1110
  }
1111
+ function setAdPlayingFlag(isPlaying) {
1112
+ if (isPlaying) {
1113
+ contentVideo.dataset.stormcloudAdPlaying = "true";
1114
+ } else {
1115
+ delete contentVideo.dataset.stormcloudAdPlaying;
1116
+ }
1117
+ }
1054
1118
  function handleAdComplete() {
1055
1119
  console.log("[HlsAdPlayer] Handling ad completion");
1056
1120
  adPlaying = false;
1121
+ setAdPlayingFlag(false);
1122
+ const previousMutedState = contentVideo.muted;
1057
1123
  contentVideo.muted = originalMutedState;
1124
+ console.log(
1125
+ `[HlsAdPlayer] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
1126
+ );
1058
1127
  if (adContainerEl) {
1059
1128
  adContainerEl.style.display = "none";
1060
1129
  adContainerEl.style.pointerEvents = "none";
@@ -1072,7 +1141,12 @@ function createHlsAdPlayer(contentVideo, options) {
1072
1141
  function handleAdError() {
1073
1142
  console.log("[HlsAdPlayer] Handling ad error");
1074
1143
  adPlaying = false;
1144
+ setAdPlayingFlag(false);
1145
+ const previousMutedState = contentVideo.muted;
1075
1146
  contentVideo.muted = originalMutedState;
1147
+ console.log(
1148
+ `[HlsAdPlayer] Restored mute state: ${previousMutedState} -> ${originalMutedState}`
1149
+ );
1076
1150
  if (adContainerEl) {
1077
1151
  adContainerEl.style.display = "none";
1078
1152
  adContainerEl.style.pointerEvents = "none";
@@ -1109,7 +1183,9 @@ function createHlsAdPlayer(contentVideo, options) {
1109
1183
  async requestAds(vastTagUrl) {
1110
1184
  console.log("[HlsAdPlayer] Requesting ads:", vastTagUrl);
1111
1185
  if (adPlaying) {
1112
- console.warn("[HlsAdPlayer] Cannot request new ads while an ad is playing");
1186
+ console.warn(
1187
+ "[HlsAdPlayer] Cannot request new ads while an ad is playing"
1188
+ );
1113
1189
  return Promise.reject(new Error("Ad already playing"));
1114
1190
  }
1115
1191
  try {
@@ -1120,14 +1196,20 @@ function createHlsAdPlayer(contentVideo, options) {
1120
1196
  }
1121
1197
  const vastXml = await response.text();
1122
1198
  console.log("[HlsAdPlayer] VAST XML received");
1123
- console.log("[HlsAdPlayer] VAST XML content (first 2000 chars):", vastXml.substring(0, 2e3));
1199
+ console.log(
1200
+ "[HlsAdPlayer] VAST XML content (first 2000 chars):",
1201
+ vastXml.substring(0, 2e3)
1202
+ );
1124
1203
  const ad = parseVastXml(vastXml);
1125
1204
  if (!ad) {
1126
1205
  console.warn("[HlsAdPlayer] No ads available from VAST response");
1206
+ emit("ad_error");
1127
1207
  return Promise.resolve();
1128
1208
  }
1129
1209
  currentAd = ad;
1130
- console.log(`[HlsAdPlayer] Ad parsed: ${ad.title}, duration: ${ad.duration}s`);
1210
+ console.log(
1211
+ `[HlsAdPlayer] Ad parsed: ${ad.title}, duration: ${ad.duration}s`
1212
+ );
1131
1213
  fireTrackingPixels(ad.trackingUrls.impression);
1132
1214
  trackingFired.impression = true;
1133
1215
  return Promise.resolve();
@@ -1139,7 +1221,9 @@ function createHlsAdPlayer(contentVideo, options) {
1139
1221
  },
1140
1222
  async play() {
1141
1223
  if (!currentAd) {
1142
- console.warn("[HlsAdPlayer] Cannot play: No ad loaded (no ads available)");
1224
+ console.warn(
1225
+ "[HlsAdPlayer] Cannot play: No ad loaded (no ads available)"
1226
+ );
1143
1227
  return Promise.reject(new Error("No ad loaded"));
1144
1228
  }
1145
1229
  console.log("[HlsAdPlayer] Starting ad playback");
@@ -1157,6 +1241,7 @@ function createHlsAdPlayer(contentVideo, options) {
1157
1241
  thirdQuartile: false,
1158
1242
  complete: false
1159
1243
  };
1244
+ const contentVolume = contentVideo.volume;
1160
1245
  if (!(options == null ? void 0 : options.continueLiveStreamDuringAds)) {
1161
1246
  contentVideo.pause();
1162
1247
  console.log("[HlsAdPlayer] Content paused (VOD mode)");
@@ -1165,6 +1250,15 @@ function createHlsAdPlayer(contentVideo, options) {
1165
1250
  }
1166
1251
  contentVideo.muted = true;
1167
1252
  adPlaying = true;
1253
+ setAdPlayingFlag(true);
1254
+ if (adVideoElement) {
1255
+ const adVolume = originalMutedState ? 0 : contentVolume;
1256
+ adVideoElement.volume = Math.max(0, Math.min(1, adVolume));
1257
+ adVideoElement.muted = false;
1258
+ console.log(
1259
+ `[HlsAdPlayer] Set ad video volume to ${adVideoElement.volume}, muted: ${adVideoElement.muted}, originalMutedState: ${originalMutedState}, contentVolume: ${contentVolume}`
1260
+ );
1261
+ }
1168
1262
  if (adContainerEl) {
1169
1263
  adContainerEl.style.display = "flex";
1170
1264
  adContainerEl.style.pointerEvents = "auto";
@@ -1217,6 +1311,7 @@ function createHlsAdPlayer(contentVideo, options) {
1217
1311
  async stop() {
1218
1312
  console.log("[HlsAdPlayer] Stopping ad");
1219
1313
  adPlaying = false;
1314
+ setAdPlayingFlag(false);
1220
1315
  contentVideo.muted = originalMutedState;
1221
1316
  if (adContainerEl) {
1222
1317
  adContainerEl.style.display = "none";
@@ -1239,6 +1334,7 @@ function createHlsAdPlayer(contentVideo, options) {
1239
1334
  destroy() {
1240
1335
  console.log("[HlsAdPlayer] Destroying");
1241
1336
  adPlaying = false;
1337
+ setAdPlayingFlag(false);
1242
1338
  contentVideo.muted = originalMutedState;
1243
1339
  if (adHls) {
1244
1340
  adHls.destroy();
@@ -1280,6 +1376,9 @@ function createHlsAdPlayer(contentVideo, options) {
1280
1376
  (_a = listeners.get(event)) == null ? void 0 : _a.delete(listener);
1281
1377
  },
1282
1378
  updateOriginalMutedState(muted) {
1379
+ console.log(
1380
+ `[HlsAdPlayer] updateOriginalMutedState called: ${originalMutedState} -> ${muted}`
1381
+ );
1283
1382
  originalMutedState = muted;
1284
1383
  },
1285
1384
  getOriginalMutedState() {
@@ -1772,6 +1871,9 @@ var StormcloudVideoPlayer = class {
1772
1871
  this.isLiveStream = false;
1773
1872
  this.nativeHlsMode = false;
1774
1873
  this.videoSrcProtection = null;
1874
+ this.bufferedSegmentsCount = 0;
1875
+ this.shouldAutoplayAfterBuffering = false;
1876
+ this.hasInitialBufferCompleted = false;
1775
1877
  initializePolyfills();
1776
1878
  const browserOverrides = getBrowserConfigOverrides();
1777
1879
  this.config = { ...config, ...browserOverrides };
@@ -1858,14 +1960,22 @@ var StormcloudVideoPlayer = class {
1858
1960
  liveDurationInfinity: true,
1859
1961
  lowLatencyMode: !!this.config.lowLatencyMode,
1860
1962
  maxLiveSyncPlaybackRate: this.config.lowLatencyMode ? 1.5 : 1,
1861
- ...this.config.lowLatencyMode ? { liveSyncDuration: 2 } : {}
1963
+ ...this.config.lowLatencyMode ? { liveSyncDuration: 2 } : {},
1964
+ maxBufferLength: 30,
1965
+ maxMaxBufferLength: 600,
1966
+ maxBufferSize: 60 * 1e3 * 1e3,
1967
+ maxBufferHole: 0.5,
1968
+ highBufferWatchdogPeriod: 2,
1969
+ nudgeOffset: 0.1,
1970
+ nudgeMaxRetry: 3,
1971
+ startPosition: -1
1862
1972
  });
1863
1973
  this.hls.on(Hls2.Events.MEDIA_ATTACHED, () => {
1864
1974
  var _a2;
1865
1975
  (_a2 = this.hls) == null ? void 0 : _a2.loadSource(this.config.src);
1866
1976
  });
1867
1977
  this.hls.on(Hls2.Events.MANIFEST_PARSED, async (_, data) => {
1868
- var _a2, _b2, _c, _d;
1978
+ var _a2, _b2, _c, _d, _e;
1869
1979
  this.isLiveStream = (_c = (_b2 = (_a2 = this.hls) == null ? void 0 : _a2.levels) == null ? void 0 : _b2.some(
1870
1980
  (level) => {
1871
1981
  var _a3, _b3;
@@ -1883,9 +1993,51 @@ var StormcloudVideoPlayer = class {
1883
1993
  this.ima.destroy();
1884
1994
  this.ima = this.createAdPlayer(this.shouldContinueLiveStreamDuringAds());
1885
1995
  this.ima.initialize();
1886
- if (this.config.autoplay) {
1887
- await ((_d = this.video.play()) == null ? void 0 : _d.catch(() => {
1888
- }));
1996
+ this.bufferedSegmentsCount = 0;
1997
+ this.hasInitialBufferCompleted = false;
1998
+ this.shouldAutoplayAfterBuffering = !!this.config.autoplay;
1999
+ const minSegments = (_d = this.config.minSegmentsBeforePlay) != null ? _d : 2;
2000
+ if (this.config.debugAdTiming) {
2001
+ console.log(
2002
+ "[StormcloudVideoPlayer] Waiting for",
2003
+ minSegments,
2004
+ "segments to buffer before playback"
2005
+ );
2006
+ }
2007
+ if (minSegments === 0 || !this.config.autoplay) {
2008
+ this.hasInitialBufferCompleted = true;
2009
+ if (this.config.autoplay) {
2010
+ await ((_e = this.video.play()) == null ? void 0 : _e.catch(() => {
2011
+ }));
2012
+ }
2013
+ }
2014
+ });
2015
+ this.hls.on(Hls2.Events.FRAG_BUFFERED, async (_evt, data) => {
2016
+ var _a2, _b2;
2017
+ if (this.hasInitialBufferCompleted) {
2018
+ return;
2019
+ }
2020
+ this.bufferedSegmentsCount++;
2021
+ const minSegments = (_a2 = this.config.minSegmentsBeforePlay) != null ? _a2 : 2;
2022
+ if (this.config.debugAdTiming) {
2023
+ console.log(
2024
+ `[StormcloudVideoPlayer] Buffered segment ${this.bufferedSegmentsCount}/${minSegments}`
2025
+ );
2026
+ }
2027
+ if (this.bufferedSegmentsCount >= minSegments) {
2028
+ this.hasInitialBufferCompleted = true;
2029
+ if (this.shouldAutoplayAfterBuffering) {
2030
+ if (this.config.debugAdTiming) {
2031
+ console.log(
2032
+ `[StormcloudVideoPlayer] Initial buffer complete (${this.bufferedSegmentsCount} segments). Starting playback.`
2033
+ );
2034
+ }
2035
+ await ((_b2 = this.video.play()) == null ? void 0 : _b2.catch((err) => {
2036
+ if (this.config.debugAdTiming) {
2037
+ console.warn("[StormcloudVideoPlayer] Autoplay failed:", err);
2038
+ }
2039
+ }));
2040
+ }
1889
2041
  }
1890
2042
  });
1891
2043
  this.hls.on(Hls2.Events.FRAG_PARSING_METADATA, (_evt, data) => {
@@ -1981,6 +2133,7 @@ var StormcloudVideoPlayer = class {
1981
2133
  this.video.autoplay = !!this.config.autoplay;
1982
2134
  this.video.muted = !!this.config.muted;
1983
2135
  this.ima.initialize();
2136
+ this.ima.updateOriginalMutedState(this.video.muted);
1984
2137
  this.ima.on("all_ads_completed", () => {
1985
2138
  if (this.config.debugAdTiming) {
1986
2139
  console.log(
@@ -2718,14 +2871,66 @@ var StormcloudVideoPlayer = class {
2718
2871
  }
2719
2872
  }
2720
2873
  ensureAdStoppedByTimer() {
2874
+ var _a, _b;
2721
2875
  if (!this.inAdBreak) return;
2876
+ this.adStopTimerId = void 0;
2877
+ const adPlaying = this.ima.isAdPlaying();
2878
+ const pendingAds = this.adPodQueue.length > 0;
2879
+ const checkIntervalMs = Math.max(
2880
+ 250,
2881
+ Math.floor((_a = this.config.adBreakCheckIntervalMs) != null ? _a : 1e3)
2882
+ );
2883
+ const maxExtensionMsConfig = this.config.maxAdBreakExtensionMs;
2884
+ const maxExtensionMs = typeof maxExtensionMsConfig === "number" && maxExtensionMsConfig > 0 ? maxExtensionMsConfig : 6e4;
2885
+ let elapsedSinceStartMs = 0;
2886
+ if (this.currentAdBreakStartWallClockMs != null) {
2887
+ elapsedSinceStartMs = Date.now() - this.currentAdBreakStartWallClockMs;
2888
+ }
2889
+ const expectedDurationMs = (_b = this.expectedAdBreakDurationMs) != null ? _b : 0;
2890
+ const overrunMs = Math.max(0, elapsedSinceStartMs - expectedDurationMs);
2891
+ const shouldExtendAdBreak = (adPlaying || pendingAds || this.showAds) && overrunMs < maxExtensionMs;
2892
+ if (shouldExtendAdBreak) {
2893
+ if (this.config.debugAdTiming) {
2894
+ console.log(
2895
+ "[StormcloudVideoPlayer] Extending ad break beyond scheduled duration",
2896
+ {
2897
+ adPlaying,
2898
+ pendingAds,
2899
+ showAds: this.showAds,
2900
+ overrunMs,
2901
+ checkIntervalMs,
2902
+ maxExtensionMs
2903
+ }
2904
+ );
2905
+ }
2906
+ this.scheduleAdStopCountdown(checkIntervalMs);
2907
+ return;
2908
+ }
2909
+ if (this.config.debugAdTiming) {
2910
+ console.log("[StormcloudVideoPlayer] Ending ad break via timer", {
2911
+ adPlaying,
2912
+ pendingAds,
2913
+ showAds: this.showAds,
2914
+ overrunMs,
2915
+ maxExtensionMs
2916
+ });
2917
+ }
2722
2918
  this.inAdBreak = false;
2723
2919
  this.expectedAdBreakDurationMs = void 0;
2724
2920
  this.currentAdBreakStartWallClockMs = void 0;
2725
- this.adStopTimerId = void 0;
2726
- if (this.ima.isAdPlaying()) {
2921
+ this.showAds = false;
2922
+ this.adPodQueue = [];
2923
+ this.currentAdIndex = 0;
2924
+ this.totalAdsInBreak = 0;
2925
+ this.clearAdFailsafeTimer();
2926
+ if (adPlaying) {
2727
2927
  this.ima.stop().catch(() => {
2728
2928
  });
2929
+ return;
2930
+ }
2931
+ const originalMutedState = this.ima.getOriginalMutedState();
2932
+ if (this.video.muted !== originalMutedState) {
2933
+ this.video.muted = originalMutedState;
2729
2934
  }
2730
2935
  }
2731
2936
  scheduleAdStartIn(delayMs) {
@@ -2765,7 +2970,23 @@ var StormcloudVideoPlayer = class {
2765
2970
  }
2766
2971
  return;
2767
2972
  }
2768
- this.ima.updateOriginalMutedState(this.video.muted);
2973
+ if (!this.showAds) {
2974
+ if (this.config.debugAdTiming) {
2975
+ console.log(
2976
+ `[StormcloudVideoPlayer] Capturing original state before ad request:`,
2977
+ {
2978
+ videoMuted: this.video.muted,
2979
+ videoVolume: this.video.volume,
2980
+ showAds: this.showAds
2981
+ }
2982
+ );
2983
+ }
2984
+ this.ima.updateOriginalMutedState(this.video.muted);
2985
+ } else if (this.config.debugAdTiming) {
2986
+ console.log(
2987
+ `[StormcloudVideoPlayer] Keeping existing original mute state (currently showing ads)`
2988
+ );
2989
+ }
2769
2990
  this.startAdFailsafeTimer();
2770
2991
  try {
2771
2992
  await this.ima.requestAds(vastTagUrl);
@@ -2820,11 +3041,12 @@ var StormcloudVideoPlayer = class {
2820
3041
  this.showAds = false;
2821
3042
  this.currentAdIndex = 0;
2822
3043
  this.totalAdsInBreak = 0;
3044
+ const currentMutedState = this.video.muted;
2823
3045
  const originalMutedState = this.ima.getOriginalMutedState();
2824
3046
  this.video.muted = originalMutedState;
2825
3047
  if (this.config.debugAdTiming) {
2826
3048
  console.log(
2827
- `[StormcloudVideoPlayer] Restored mute state to: ${originalMutedState}`
3049
+ `[StormcloudVideoPlayer] Restored mute state: ${currentMutedState} -> ${originalMutedState}`
2828
3050
  );
2829
3051
  }
2830
3052
  if (this.video.paused) {
@@ -2916,6 +3138,7 @@ var StormcloudVideoPlayer = class {
2916
3138
  }
2917
3139
  } else {
2918
3140
  this.video.muted = !this.video.muted;
3141
+ this.ima.updateOriginalMutedState(this.video.muted);
2919
3142
  if (this.config.debugAdTiming) {
2920
3143
  console.log("[StormcloudVideoPlayer] Muted:", this.video.muted);
2921
3144
  }
@@ -2960,11 +3183,47 @@ var StormcloudVideoPlayer = class {
2960
3183
  }
2961
3184
  isMuted() {
2962
3185
  if (this.ima.isAdPlaying()) {
2963
- const adVolume = this.ima.getAdVolume();
2964
- return adVolume === 0;
3186
+ if (this.config.debugAdTiming) {
3187
+ console.log(
3188
+ "[StormcloudVideoPlayer] isMuted() override during ad playback -> false"
3189
+ );
3190
+ }
3191
+ return false;
3192
+ }
3193
+ if (this.config.debugAdTiming) {
3194
+ console.log(
3195
+ `[StormcloudVideoPlayer] isMuted() no ad playing: video.muted=${this.video.muted}`
3196
+ );
2965
3197
  }
2966
3198
  return this.video.muted;
2967
3199
  }
3200
+ setMuted(muted) {
3201
+ const adPlaying = this.ima.isAdPlaying();
3202
+ if (adPlaying && muted === this.video.muted) {
3203
+ if (this.config.debugAdTiming) {
3204
+ console.log(
3205
+ "[StormcloudVideoPlayer] setMuted reflective update during ad ignored",
3206
+ { muted }
3207
+ );
3208
+ }
3209
+ return;
3210
+ }
3211
+ this.video.muted = muted;
3212
+ if (adPlaying) {
3213
+ this.ima.updateOriginalMutedState(muted);
3214
+ this.ima.setAdVolume(muted ? 0 : 1);
3215
+ if (this.config.debugAdTiming) {
3216
+ console.log("[StormcloudVideoPlayer] setMuted applied during ad", {
3217
+ muted
3218
+ });
3219
+ }
3220
+ return;
3221
+ }
3222
+ this.ima.updateOriginalMutedState(muted);
3223
+ if (this.config.debugAdTiming) {
3224
+ console.log("[StormcloudVideoPlayer] setMuted called:", muted);
3225
+ }
3226
+ }
2968
3227
  isFullscreen() {
2969
3228
  return !!document.fullscreenElement;
2970
3229
  }
@@ -3052,10 +3311,12 @@ var StormcloudVideoPlayerComponent = React.memo(
3052
3311
  vastMode,
3053
3312
  vastTagUrl,
3054
3313
  adPlayerType,
3314
+ minSegmentsBeforePlay,
3055
3315
  ...restVideoAttrs
3056
3316
  } = props;
3057
3317
  const videoRef = useRef(null);
3058
3318
  const playerRef = useRef(null);
3319
+ const bufferingTimeoutRef = useRef(null);
3059
3320
  const [adStatus, setAdStatus] = React.useState({ showAds: false, currentIndex: 0, totalAds: 0 });
3060
3321
  const [shouldShowNativeControls, setShouldShowNativeControls] = React.useState(true);
3061
3322
  const [isMuted, setIsMuted] = React.useState(false);
@@ -3177,6 +3438,9 @@ var StormcloudVideoPlayerComponent = React.memo(
3177
3438
  return;
3178
3439
  }
3179
3440
  setShowLicenseWarning(false);
3441
+ if (debugAdTiming) {
3442
+ console.log("[StormcloudUI] Initializing player, isLoading=true");
3443
+ }
3180
3444
  if (playerRef.current) {
3181
3445
  try {
3182
3446
  playerRef.current.destroy();
@@ -3207,17 +3471,25 @@ var StormcloudVideoPlayerComponent = React.memo(
3207
3471
  if (vastMode !== void 0) cfg.vastMode = vastMode;
3208
3472
  if (vastTagUrl !== void 0) cfg.vastTagUrl = vastTagUrl;
3209
3473
  if (adPlayerType !== void 0) cfg.adPlayerType = adPlayerType;
3474
+ if (minSegmentsBeforePlay !== void 0)
3475
+ cfg.minSegmentsBeforePlay = minSegmentsBeforePlay;
3210
3476
  const player = new StormcloudVideoPlayer(cfg);
3211
3477
  playerRef.current = player;
3212
3478
  player.load().then(() => {
3213
3479
  const showNative = player.shouldShowNativeControls();
3214
3480
  setShouldShowNativeControls(showNative);
3481
+ if (debugAdTiming) {
3482
+ console.log(
3483
+ "[StormcloudUI] Player loaded successfully, waiting for video ready"
3484
+ );
3485
+ }
3215
3486
  onReady == null ? void 0 : onReady(player);
3216
3487
  }).catch((error) => {
3217
3488
  console.error(
3218
3489
  "StormcloudVideoPlayer: Failed to load player:",
3219
3490
  error
3220
3491
  );
3492
+ setIsLoading(false);
3221
3493
  onReady == null ? void 0 : onReady(player);
3222
3494
  });
3223
3495
  return () => {
@@ -3234,8 +3506,8 @@ var StormcloudVideoPlayerComponent = React.memo(
3234
3506
  if (autoplay !== void 0 && playerRef.current.videoElement) {
3235
3507
  playerRef.current.videoElement.autoplay = autoplay;
3236
3508
  }
3237
- if (muted !== void 0 && playerRef.current.videoElement) {
3238
- playerRef.current.videoElement.muted = muted;
3509
+ if (muted !== void 0) {
3510
+ playerRef.current.setMuted(muted);
3239
3511
  }
3240
3512
  } catch (error) {
3241
3513
  console.warn("Failed to update player properties:", error);
@@ -3316,26 +3588,108 @@ var StormcloudVideoPlayerComponent = React.memo(
3316
3588
  useEffect(() => {
3317
3589
  if (!videoRef.current) return;
3318
3590
  const handleLoadedMetadata = () => {
3591
+ var _a;
3319
3592
  if (videoRef.current) {
3320
3593
  const video2 = videoRef.current;
3321
3594
  void video2.offsetHeight;
3322
3595
  }
3596
+ if (debugAdTiming) {
3597
+ console.log(
3598
+ "[StormcloudUI] Video event: loadedmetadata, readyState:",
3599
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3600
+ );
3601
+ }
3602
+ };
3603
+ const handleLoadedData = () => {
3604
+ var _a;
3605
+ if (debugAdTiming) {
3606
+ console.log(
3607
+ "[StormcloudUI] Video event: loadeddata, readyState:",
3608
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3609
+ );
3610
+ }
3323
3611
  };
3324
3612
  const handleLoadStart = () => {
3325
- setIsLoading(true);
3326
- setIsBuffering(false);
3613
+ var _a;
3614
+ if (debugAdTiming) {
3615
+ console.log(
3616
+ "[StormcloudUI] Video event: loadstart, readyState:",
3617
+ (_a = videoRef.current) == null ? void 0 : _a.readyState
3618
+ );
3619
+ }
3327
3620
  };
3328
3621
  const handleCanPlay = () => {
3622
+ var _a;
3623
+ setIsLoading(false);
3624
+ if (bufferingTimeoutRef.current) {
3625
+ clearTimeout(bufferingTimeoutRef.current);
3626
+ bufferingTimeoutRef.current = null;
3627
+ }
3628
+ setIsBuffering(false);
3629
+ if (debugAdTiming) {
3630
+ console.log(
3631
+ "[StormcloudUI] Video event: canplay, readyState:",
3632
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3633
+ "- clearing loading state, isLoading=false"
3634
+ );
3635
+ }
3636
+ };
3637
+ const handleCanPlayThrough = () => {
3638
+ var _a;
3329
3639
  setIsLoading(false);
3640
+ if (bufferingTimeoutRef.current) {
3641
+ clearTimeout(bufferingTimeoutRef.current);
3642
+ bufferingTimeoutRef.current = null;
3643
+ }
3330
3644
  setIsBuffering(false);
3645
+ if (debugAdTiming) {
3646
+ console.log(
3647
+ "[StormcloudUI] Video event: canplaythrough, readyState:",
3648
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3649
+ "- clearing loading state, isLoading=false"
3650
+ );
3651
+ }
3331
3652
  };
3332
3653
  const handleWaiting = () => {
3333
- setIsBuffering(true);
3654
+ var _a;
3655
+ if (bufferingTimeoutRef.current) {
3656
+ clearTimeout(bufferingTimeoutRef.current);
3657
+ }
3658
+ bufferingTimeoutRef.current = window.setTimeout(() => {
3659
+ var _a2;
3660
+ setIsBuffering(true);
3661
+ if (debugAdTiming) {
3662
+ console.log(
3663
+ "[StormcloudUI] Video buffering detected (after 300ms delay), readyState:",
3664
+ (_a2 = videoRef.current) == null ? void 0 : _a2.readyState,
3665
+ "- showing spinner, isBuffering=true"
3666
+ );
3667
+ }
3668
+ }, 300);
3669
+ if (debugAdTiming) {
3670
+ console.log(
3671
+ "[StormcloudUI] Video event: waiting, readyState:",
3672
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3673
+ "- buffering delay started (300ms)"
3674
+ );
3675
+ }
3334
3676
  };
3335
3677
  const handlePlaying = () => {
3678
+ var _a;
3336
3679
  setIsLoading(false);
3680
+ if (bufferingTimeoutRef.current) {
3681
+ clearTimeout(bufferingTimeoutRef.current);
3682
+ bufferingTimeoutRef.current = null;
3683
+ }
3337
3684
  setIsBuffering(false);
3338
3685
  setShowCenterPlay(false);
3686
+ if (debugAdTiming) {
3687
+ console.log(
3688
+ "[StormcloudUI] Video event: playing, readyState:",
3689
+ (_a = videoRef.current) == null ? void 0 : _a.readyState,
3690
+ "- playback started, isLoading=false, isBuffering=false"
3691
+ );
3692
+ }
3339
3693
  };
3340
3694
  const handlePause = () => {
3341
3695
  if (playerRef.current && !playerRef.current.isShowingAds()) {
@@ -3350,8 +3704,9 @@ var StormcloudVideoPlayerComponent = React.memo(
3350
3704
  const video = videoRef.current;
3351
3705
  video.addEventListener("loadstart", handleLoadStart);
3352
3706
  video.addEventListener("loadedmetadata", handleLoadedMetadata);
3353
- video.addEventListener("loadeddata", handleLoadedMetadata);
3707
+ video.addEventListener("loadeddata", handleLoadedData);
3354
3708
  video.addEventListener("canplay", handleCanPlay);
3709
+ video.addEventListener("canplaythrough", handleCanPlayThrough);
3355
3710
  video.addEventListener("waiting", handleWaiting);
3356
3711
  video.addEventListener("playing", handlePlaying);
3357
3712
  video.addEventListener("pause", handlePause);
@@ -3360,16 +3715,21 @@ var StormcloudVideoPlayerComponent = React.memo(
3360
3715
  setShowCenterPlay(true);
3361
3716
  }
3362
3717
  return () => {
3718
+ if (bufferingTimeoutRef.current) {
3719
+ clearTimeout(bufferingTimeoutRef.current);
3720
+ bufferingTimeoutRef.current = null;
3721
+ }
3363
3722
  video.removeEventListener("loadstart", handleLoadStart);
3364
3723
  video.removeEventListener("loadedmetadata", handleLoadedMetadata);
3365
- video.removeEventListener("loadeddata", handleLoadedMetadata);
3724
+ video.removeEventListener("loadeddata", handleLoadedData);
3366
3725
  video.removeEventListener("canplay", handleCanPlay);
3726
+ video.removeEventListener("canplaythrough", handleCanPlayThrough);
3367
3727
  video.removeEventListener("waiting", handleWaiting);
3368
3728
  video.removeEventListener("playing", handlePlaying);
3369
3729
  video.removeEventListener("pause", handlePause);
3370
3730
  video.removeEventListener("ended", handleEnded);
3371
3731
  };
3372
- }, []);
3732
+ }, [debugAdTiming]);
3373
3733
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3374
3734
  /* @__PURE__ */ jsx("style", { children: `
3375
3735
  @keyframes spin {
@@ -3476,35 +3836,18 @@ var StormcloudVideoPlayerComponent = React.memo(
3476
3836
  }
3477
3837
  ),
3478
3838
  (isLoading || isBuffering) && /* @__PURE__ */ jsx(
3479
- "div",
3839
+ FaSpinner,
3480
3840
  {
3841
+ size: 42,
3842
+ color: "white",
3481
3843
  style: {
3482
3844
  position: "absolute",
3483
- top: "50%",
3484
- left: "50%",
3485
- transform: "translate(-50%, -50%)",
3845
+ top: "calc(50% - 21px)",
3846
+ left: "calc(50% - 21px)",
3486
3847
  zIndex: 20,
3487
- display: "flex",
3488
- alignItems: "center",
3489
- justifyContent: "center",
3490
- background: "linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(20, 20, 20, 0.6) 100%)",
3491
- width: "80px",
3492
- height: "80px",
3493
- borderRadius: "50%",
3494
- backdropFilter: "blur(20px)",
3495
- boxShadow: "0 12px 40px rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.1)"
3496
- },
3497
- children: /* @__PURE__ */ jsx(
3498
- FaSpinner,
3499
- {
3500
- size: 28,
3501
- color: "white",
3502
- style: {
3503
- animation: "spin 1s linear infinite",
3504
- filter: "drop-shadow(0 3px 6px rgba(0, 0, 0, 0.8))"
3505
- }
3506
- }
3507
- )
3848
+ animation: "spin 1s linear infinite",
3849
+ filter: "drop-shadow(0 3px 6px rgba(0, 0, 0, 0.8))"
3850
+ }
3508
3851
  }
3509
3852
  ),
3510
3853
  showLicenseWarning && /* @__PURE__ */ jsxs(
@@ -4617,6 +4960,7 @@ var defaultProps = {
4617
4960
  showCustomControls: false,
4618
4961
  licenseKey: "",
4619
4962
  adFailsafeTimeoutMs: 1e4,
4963
+ minSegmentsBeforePlay: 2,
4620
4964
  onStart: noop,
4621
4965
  onPlay: noop,
4622
4966
  onPause: noop,
@@ -4790,6 +5134,8 @@ var HlsPlayer = class extends Component {
4790
5134
  config.licenseKey = this.props.licenseKey;
4791
5135
  if (this.props.adFailsafeTimeoutMs !== void 0)
4792
5136
  config.adFailsafeTimeoutMs = this.props.adFailsafeTimeoutMs;
5137
+ if (this.props.minSegmentsBeforePlay !== void 0)
5138
+ config.minSegmentsBeforePlay = this.props.minSegmentsBeforePlay;
4793
5139
  this.player = new StormcloudVideoPlayer(config);
4794
5140
  (_b = (_a = this.props).onMount) == null ? void 0 : _b.call(_a, this);
4795
5141
  await this.player.load();
@@ -5396,6 +5742,7 @@ var SUPPORTED_PROPS = [
5396
5742
  "showCustomControls",
5397
5743
  "licenseKey",
5398
5744
  "adFailsafeTimeoutMs",
5745
+ "minSegmentsBeforePlay",
5399
5746
  "onReady",
5400
5747
  "onStart",
5401
5748
  "onPlay",