stormcloud-video-player 0.8.76 → 0.8.78

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/types.d.cts CHANGED
@@ -25,6 +25,7 @@ interface StormcloudVideoPlayerConfig {
25
25
  debugAdTiming?: boolean;
26
26
  adFailsafeTimeoutMs?: number;
27
27
  adMediaStartTimeoutMs?: number;
28
+ contentStallWatchdog?: boolean;
28
29
  adBreakCheckIntervalMs?: number;
29
30
  maxAdBreakExtensionMs?: number;
30
31
  minRemainingMsToStartAd?: number;
@@ -2253,6 +2253,9 @@ function requiresMediaPipelineResetAfterAds() {
2253
2253
  var browser = detectBrowser();
2254
2254
  return browser.name === "Samsung Tizen" && ((_browser_tizenVersion = browser.tizenVersion) !== null && _browser_tizenVersion !== void 0 ? _browser_tizenVersion : 0) >= 5;
2255
2255
  }
2256
+ function prefersProgressiveAdMedia() {
2257
+ return detectBrowser().isSmartTV;
2258
+ }
2256
2259
  function getBrowserConfigOverrides() {
2257
2260
  var browser = detectBrowser();
2258
2261
  var overrides = {};
@@ -3096,6 +3099,7 @@ function createHlsAdPlayer(contentVideo, options) {
3096
3099
  var probe = adVideoElement !== null && adVideoElement !== void 0 ? adVideoElement : document.createElement("video");
3097
3100
  var pageIsHttps = typeof location !== "undefined" && location.protocol === "https:";
3098
3101
  var target = getMediaTargetQuality();
3102
+ var deprioritizeHls = prefersProgressiveAdMedia();
3099
3103
  var accepted = [];
3100
3104
  mediaFiles.forEach(function(file, index) {
3101
3105
  var isHls = isHlsMediaFile(file);
@@ -3126,19 +3130,24 @@ function createHlsAdPlayer(contentVideo, options) {
3126
3130
  var bitrateDiff = Math.abs((file.bitrate || 5e3) * 1e3 - target.bitrate);
3127
3131
  score = resolutionDiff * 2 + bitrateDiff / 1e3;
3128
3132
  }
3133
+ var deprioritizedHls = deprioritizeHls && isHls;
3129
3134
  accepted.push({
3130
3135
  file: file,
3131
3136
  score: score,
3132
- insecure: insecure
3137
+ insecure: insecure,
3138
+ deprioritizedHls: deprioritizedHls
3133
3139
  });
3134
3140
  logFunnel("media_candidate", _object_spread_props(_object_spread({}, details), {
3135
3141
  reason: "accepted",
3136
- why: insecure ? "insecure_http_deprioritized" : "playable",
3142
+ why: insecure ? "insecure_http_deprioritized" : deprioritizedHls ? "hls_deprioritized_on_tv" : "playable",
3137
3143
  score: score
3138
3144
  }));
3139
3145
  });
3140
3146
  accepted.sort(function(a, b) {
3141
3147
  if (a.insecure !== b.insecure) return a.insecure ? 1 : -1;
3148
+ if (a.deprioritizedHls !== b.deprioritizedHls) {
3149
+ return a.deprioritizedHls ? 1 : -1;
3150
+ }
3142
3151
  return a.score - b.score;
3143
3152
  });
3144
3153
  return accepted.map(function(entry) {
@@ -4811,8 +4820,8 @@ function createHlsAdPlayer(contentVideo, options) {
4811
4820
  console.warn("[HlsAdPlayer] Error preparing ad video for new source:", error);
4812
4821
  }
4813
4822
  }
4814
- function recreateAdVideoElement() {
4815
- console.log("[HlsAdPlayer] Recreating ad video element (single-decoder platform)");
4823
+ function recreateAdVideoElement(reason) {
4824
+ console.log("[HlsAdPlayer] Recreating ad video element (".concat(reason, ")"));
4816
4825
  teardownAdEventListeners();
4817
4826
  prepareVideoElementForNewSource();
4818
4827
  if (adVideoElement) {
@@ -4856,8 +4865,9 @@ function createHlsAdPlayer(contentVideo, options) {
4856
4865
  }
4857
4866
  function resetVideoElementForNextSource() {
4858
4867
  clearStallWatchdog();
4859
- if (requiresMediaPipelineResetAfterAds()) {
4860
- recreateAdVideoElement();
4868
+ var previousSourceUsedHlsJs = adHls !== void 0;
4869
+ if (requiresMediaPipelineResetAfterAds() || previousSourceUsedHlsJs) {
4870
+ recreateAdVideoElement(previousSourceUsedHlsJs ? "after hls.js source" : "single-decoder platform");
4861
4871
  setupAdEventListeners();
4862
4872
  } else {
4863
4873
  prepareVideoElementForNewSource();
@@ -12339,6 +12349,181 @@ var AdBreakOrchestrator = /*#__PURE__*/ function() {
12339
12349
  ]);
12340
12350
  return AdBreakOrchestrator;
12341
12351
  }();
12352
+ // src/player/ContentHealthWatchdog.ts
12353
+ var POLL_INTERVAL_MS = 2e3;
12354
+ var STALL_THRESHOLD_MS = 12e3;
12355
+ var POST_BREAK_GRACE_MS = 5e4;
12356
+ var MAX_BREAK_SUPPRESSION_MS = 10 * 60 * 1e3;
12357
+ var SELF_INFLICTED_PAUSE_WINDOW_MS = 5e3;
12358
+ var HEALTHY_PLAYBACK_SECONDS = 5;
12359
+ var MAX_NORMAL_ADVANCE_PER_TICK_S = 4;
12360
+ var LIGHT_STEP_COOLDOWN_MS = 1e4;
12361
+ var RELOAD_BASE_COOLDOWN_MS = 15e3;
12362
+ var RELOAD_MAX_COOLDOWN_MS = 12e4;
12363
+ var ContentHealthWatchdog = /*#__PURE__*/ function() {
12364
+ function ContentHealthWatchdog(host) {
12365
+ var _this = this;
12366
+ _class_call_check(this, ContentHealthWatchdog);
12367
+ this.host = host;
12368
+ this.armed = false;
12369
+ this.userPaused = false;
12370
+ this.wasInBreak = false;
12371
+ this.breakStartedAt = 0;
12372
+ this.stuckBreakLogged = false;
12373
+ this.graceUntil = 0;
12374
+ this.lastPosition = 0;
12375
+ this.lastProgressAt = 0;
12376
+ this.lastRealPlaybackAt = 0;
12377
+ this.healthySeconds = 0;
12378
+ this.step = 0;
12379
+ this.nextActionAt = 0;
12380
+ this.lastActionAt = 0;
12381
+ this.onPlaying = function() {
12382
+ if (_this.host.isAdPlaying()) return;
12383
+ _this.armed = true;
12384
+ _this.userPaused = false;
12385
+ };
12386
+ this.onPause = function() {
12387
+ if (_this.host.isBreakActive() || _this.host.isAdPlaying() || Date.now() - _this.lastActionAt < SELF_INFLICTED_PAUSE_WINDOW_MS) {
12388
+ return;
12389
+ }
12390
+ _this.userPaused = true;
12391
+ };
12392
+ this.onPlay = function() {
12393
+ _this.userPaused = false;
12394
+ };
12395
+ }
12396
+ _create_class(ContentHealthWatchdog, [
12397
+ {
12398
+ key: "start",
12399
+ value: function start() {
12400
+ var _this = this;
12401
+ if (this.intervalId !== void 0) return;
12402
+ var video = this.host.video;
12403
+ video.addEventListener("playing", this.onPlaying);
12404
+ video.addEventListener("pause", this.onPause);
12405
+ video.addEventListener("play", this.onPlay);
12406
+ this.resetProgress(Date.now());
12407
+ this.intervalId = window.setInterval(function() {
12408
+ return _this.tick();
12409
+ }, POLL_INTERVAL_MS);
12410
+ }
12411
+ },
12412
+ {
12413
+ key: "stop",
12414
+ value: function stop() {
12415
+ if (this.intervalId !== void 0) {
12416
+ clearInterval(this.intervalId);
12417
+ this.intervalId = void 0;
12418
+ }
12419
+ var video = this.host.video;
12420
+ video.removeEventListener("playing", this.onPlaying);
12421
+ video.removeEventListener("pause", this.onPause);
12422
+ video.removeEventListener("play", this.onPlay);
12423
+ this.armed = false;
12424
+ this.step = 0;
12425
+ }
12426
+ },
12427
+ {
12428
+ key: "resetProgress",
12429
+ value: function resetProgress(now) {
12430
+ this.lastPosition = this.host.video.currentTime;
12431
+ this.lastProgressAt = now;
12432
+ }
12433
+ },
12434
+ {
12435
+ key: "tick",
12436
+ value: function tick() {
12437
+ var now = Date.now();
12438
+ var video = this.host.video;
12439
+ var hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
12440
+ if (!this.armed || this.userPaused || hidden || video.ended || !this.host.isLiveStream() || !this.host.hasHlsInstance()) {
12441
+ this.resetProgress(now);
12442
+ return;
12443
+ }
12444
+ var breakActive = this.host.isBreakActive() || this.host.isAdPlaying();
12445
+ if (breakActive && !this.wasInBreak) {
12446
+ this.wasInBreak = true;
12447
+ this.breakStartedAt = now;
12448
+ this.stuckBreakLogged = false;
12449
+ }
12450
+ if (breakActive && now - this.breakStartedAt < MAX_BREAK_SUPPRESSION_MS) {
12451
+ this.resetProgress(now);
12452
+ return;
12453
+ }
12454
+ if (breakActive && !this.stuckBreakLogged) {
12455
+ this.stuckBreakLogged = true;
12456
+ console.warn("[ContentWatchdog] Ad break state active for over ".concat(MAX_BREAK_SUPPRESSION_MS / 6e4, " min — monitoring content anyway"));
12457
+ }
12458
+ if (!breakActive && this.wasInBreak) {
12459
+ this.wasInBreak = false;
12460
+ this.graceUntil = now + POST_BREAK_GRACE_MS;
12461
+ this.resetProgress(now);
12462
+ return;
12463
+ }
12464
+ var currentTime = video.currentTime;
12465
+ var delta = currentTime - this.lastPosition;
12466
+ this.lastPosition = currentTime;
12467
+ if (delta > 0.25 && delta <= MAX_NORMAL_ADVANCE_PER_TICK_S && !video.paused) {
12468
+ this.lastProgressAt = now;
12469
+ this.lastRealPlaybackAt = now;
12470
+ this.healthySeconds += delta;
12471
+ if (this.step > 0 && this.healthySeconds >= HEALTHY_PLAYBACK_SECONDS) {
12472
+ console.log("[ContentWatchdog] Content playback recovered after ".concat(this.step, " recovery step(s)"));
12473
+ this.step = 0;
12474
+ this.nextActionAt = 0;
12475
+ }
12476
+ return;
12477
+ }
12478
+ if (now < this.graceUntil) {
12479
+ this.lastProgressAt = now;
12480
+ return;
12481
+ }
12482
+ if (now - this.lastProgressAt < STALL_THRESHOLD_MS || now < this.nextActionAt) {
12483
+ return;
12484
+ }
12485
+ this.recover(now);
12486
+ }
12487
+ },
12488
+ {
12489
+ key: "recover",
12490
+ value: function recover(now) {
12491
+ var video = this.host.video;
12492
+ var state = "t=".concat(video.currentTime.toFixed(2), " paused=").concat(video.paused, " readyState=").concat(video.readyState, " networkState=").concat(video.networkState, " error=").concat(video.error ? video.error.code : "none");
12493
+ var stalledForS = ((now - (this.lastRealPlaybackAt || this.lastProgressAt)) / 1e3).toFixed(0);
12494
+ var fullReloadOnly = this.host.requiresMediaPipelineResetAfterAds();
12495
+ var action = fullReloadOnly || this.step >= 2 ? "reload" : this.step === 0 ? "recover_media_error" : "restart_load";
12496
+ console.warn("[ContentWatchdog] Content stalled for ".concat(stalledForS, "s — ").concat(action, " (step ").concat(this.step + 1, ") — ").concat(state));
12497
+ this.lastActionAt = now;
12498
+ this.healthySeconds = 0;
12499
+ if (action === "recover_media_error") {
12500
+ this.host.recoverMediaError();
12501
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12502
+ } else if (action === "restart_load") {
12503
+ var liveSyncPos = this.host.getLiveSyncPosition();
12504
+ var position = liveSyncPos != null && liveSyncPos > video.currentTime ? liveSyncPos : video.currentTime;
12505
+ if (position !== video.currentTime) {
12506
+ video.currentTime = position;
12507
+ }
12508
+ this.host.restartHlsLoad(position);
12509
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12510
+ } else {
12511
+ var reloadIndex = fullReloadOnly ? this.step : this.step - 2;
12512
+ var cooldown = Math.min(RELOAD_BASE_COOLDOWN_MS * Math.pow(2, reloadIndex), RELOAD_MAX_COOLDOWN_MS);
12513
+ this.host.reloadLiveStream();
12514
+ this.nextActionAt = now + cooldown;
12515
+ }
12516
+ this.step++;
12517
+ this.resetProgress(now);
12518
+ if (video.paused) {
12519
+ var _video_play;
12520
+ (_video_play = video.play()) === null || _video_play === void 0 ? void 0 : _video_play.catch(function() {});
12521
+ }
12522
+ }
12523
+ }
12524
+ ]);
12525
+ return ContentHealthWatchdog;
12526
+ }();
12342
12527
  // src/player/StormcloudVideoPlayer.ts
12343
12528
  var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12344
12529
  function StormcloudVideoPlayer(config) {
@@ -12469,6 +12654,36 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12469
12654
  return _this.palNonce.sendAdTouch(event);
12470
12655
  }
12471
12656
  }, this.timing, this.adConfig, this.cueManager, this.placeholder);
12657
+ this.contentWatchdog = new ContentHealthWatchdog({
12658
+ video: this.video,
12659
+ isLiveStream: function isLiveStream() {
12660
+ return _this.hlsEngine.isLiveStream;
12661
+ },
12662
+ hasHlsInstance: function hasHlsInstance() {
12663
+ return !!_this.hlsEngine.hls;
12664
+ },
12665
+ isBreakActive: function isBreakActive() {
12666
+ return _this.adBreak.inAdBreak || _this.adBreak.isBreakActive();
12667
+ },
12668
+ isAdPlaying: function isAdPlaying() {
12669
+ return _this.adPlayer.isAdPlaying();
12670
+ },
12671
+ requiresMediaPipelineResetAfterAds: function requiresMediaPipelineResetAfterAds1() {
12672
+ return requiresMediaPipelineResetAfterAds();
12673
+ },
12674
+ getLiveSyncPosition: function getLiveSyncPosition() {
12675
+ return _this.hlsEngine.getLiveSyncPosition();
12676
+ },
12677
+ recoverMediaError: function recoverMediaError() {
12678
+ return _this.hlsEngine.recoverMediaError();
12679
+ },
12680
+ restartHlsLoad: function restartHlsLoad(position) {
12681
+ return _this.hlsEngine.restartLoadAt(position);
12682
+ },
12683
+ reloadLiveStream: function reloadLiveStream() {
12684
+ return _this.hlsEngine.reloadLiveStream();
12685
+ }
12686
+ });
12472
12687
  }
12473
12688
  _create_class(StormcloudVideoPlayer, [
12474
12689
  {
@@ -12789,6 +13004,9 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12789
13004
  }
12790
13005
  };
12791
13006
  this.video.addEventListener("emptied", this.emptiedHandler);
13007
+ if (this.config.contentStallWatchdog !== false) {
13008
+ this.contentWatchdog.start();
13009
+ }
12792
13010
  }
12793
13011
  },
12794
13012
  {
@@ -13273,6 +13491,7 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
13273
13491
  key: "destroy",
13274
13492
  value: function destroy() {
13275
13493
  var _this_adPlayer;
13494
+ this.contentWatchdog.stop();
13276
13495
  this.adBreak.cancelScheduledRequest();
13277
13496
  this.timing.destroy();
13278
13497
  this.adBreak.clearPendingAdBreak();