stormcloud-video-player 0.8.77 → 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.
@@ -19,6 +19,7 @@ declare class StormcloudVideoPlayer {
19
19
  private readonly hlsEngine;
20
20
  private readonly adBreak;
21
21
  private readonly floorLadder;
22
+ private readonly contentWatchdog;
22
23
  private edgePodBaseUrl;
23
24
  constructor(config: StormcloudVideoPlayerConfig);
24
25
  private get debug();
@@ -2299,6 +2299,9 @@ function requiresMediaPipelineResetAfterAds() {
2299
2299
  var browser = detectBrowser();
2300
2300
  return browser.name === "Samsung Tizen" && ((_browser_tizenVersion = browser.tizenVersion) !== null && _browser_tizenVersion !== void 0 ? _browser_tizenVersion : 0) >= 5;
2301
2301
  }
2302
+ function prefersProgressiveAdMedia() {
2303
+ return detectBrowser().isSmartTV;
2304
+ }
2302
2305
  function getBrowserConfigOverrides() {
2303
2306
  var browser = detectBrowser();
2304
2307
  var overrides = {};
@@ -3142,6 +3145,7 @@ function createHlsAdPlayer(contentVideo, options) {
3142
3145
  var probe = adVideoElement !== null && adVideoElement !== void 0 ? adVideoElement : document.createElement("video");
3143
3146
  var pageIsHttps = typeof location !== "undefined" && location.protocol === "https:";
3144
3147
  var target = getMediaTargetQuality();
3148
+ var deprioritizeHls = prefersProgressiveAdMedia();
3145
3149
  var accepted = [];
3146
3150
  mediaFiles.forEach(function(file, index) {
3147
3151
  var isHls = isHlsMediaFile(file);
@@ -3172,19 +3176,24 @@ function createHlsAdPlayer(contentVideo, options) {
3172
3176
  var bitrateDiff = Math.abs((file.bitrate || 5e3) * 1e3 - target.bitrate);
3173
3177
  score = resolutionDiff * 2 + bitrateDiff / 1e3;
3174
3178
  }
3179
+ var deprioritizedHls = deprioritizeHls && isHls;
3175
3180
  accepted.push({
3176
3181
  file: file,
3177
3182
  score: score,
3178
- insecure: insecure
3183
+ insecure: insecure,
3184
+ deprioritizedHls: deprioritizedHls
3179
3185
  });
3180
3186
  logFunnel("media_candidate", _object_spread_props(_object_spread({}, details), {
3181
3187
  reason: "accepted",
3182
- why: insecure ? "insecure_http_deprioritized" : "playable",
3188
+ why: insecure ? "insecure_http_deprioritized" : deprioritizedHls ? "hls_deprioritized_on_tv" : "playable",
3183
3189
  score: score
3184
3190
  }));
3185
3191
  });
3186
3192
  accepted.sort(function(a, b) {
3187
3193
  if (a.insecure !== b.insecure) return a.insecure ? 1 : -1;
3194
+ if (a.deprioritizedHls !== b.deprioritizedHls) {
3195
+ return a.deprioritizedHls ? 1 : -1;
3196
+ }
3188
3197
  return a.score - b.score;
3189
3198
  });
3190
3199
  return accepted.map(function(entry) {
@@ -4857,8 +4866,8 @@ function createHlsAdPlayer(contentVideo, options) {
4857
4866
  console.warn("[HlsAdPlayer] Error preparing ad video for new source:", error);
4858
4867
  }
4859
4868
  }
4860
- function recreateAdVideoElement() {
4861
- console.log("[HlsAdPlayer] Recreating ad video element (single-decoder platform)");
4869
+ function recreateAdVideoElement(reason) {
4870
+ console.log("[HlsAdPlayer] Recreating ad video element (".concat(reason, ")"));
4862
4871
  teardownAdEventListeners();
4863
4872
  prepareVideoElementForNewSource();
4864
4873
  if (adVideoElement) {
@@ -4902,8 +4911,9 @@ function createHlsAdPlayer(contentVideo, options) {
4902
4911
  }
4903
4912
  function resetVideoElementForNextSource() {
4904
4913
  clearStallWatchdog();
4905
- if (requiresMediaPipelineResetAfterAds()) {
4906
- recreateAdVideoElement();
4914
+ var previousSourceUsedHlsJs = adHls !== void 0;
4915
+ if (requiresMediaPipelineResetAfterAds() || previousSourceUsedHlsJs) {
4916
+ recreateAdVideoElement(previousSourceUsedHlsJs ? "after hls.js source" : "single-decoder platform");
4907
4917
  setupAdEventListeners();
4908
4918
  } else {
4909
4919
  prepareVideoElementForNewSource();
@@ -12385,6 +12395,181 @@ var AdBreakOrchestrator = /*#__PURE__*/ function() {
12385
12395
  ]);
12386
12396
  return AdBreakOrchestrator;
12387
12397
  }();
12398
+ // src/player/ContentHealthWatchdog.ts
12399
+ var POLL_INTERVAL_MS = 2e3;
12400
+ var STALL_THRESHOLD_MS = 12e3;
12401
+ var POST_BREAK_GRACE_MS = 5e4;
12402
+ var MAX_BREAK_SUPPRESSION_MS = 10 * 60 * 1e3;
12403
+ var SELF_INFLICTED_PAUSE_WINDOW_MS = 5e3;
12404
+ var HEALTHY_PLAYBACK_SECONDS = 5;
12405
+ var MAX_NORMAL_ADVANCE_PER_TICK_S = 4;
12406
+ var LIGHT_STEP_COOLDOWN_MS = 1e4;
12407
+ var RELOAD_BASE_COOLDOWN_MS = 15e3;
12408
+ var RELOAD_MAX_COOLDOWN_MS = 12e4;
12409
+ var ContentHealthWatchdog = /*#__PURE__*/ function() {
12410
+ function ContentHealthWatchdog(host) {
12411
+ var _this = this;
12412
+ _class_call_check(this, ContentHealthWatchdog);
12413
+ this.host = host;
12414
+ this.armed = false;
12415
+ this.userPaused = false;
12416
+ this.wasInBreak = false;
12417
+ this.breakStartedAt = 0;
12418
+ this.stuckBreakLogged = false;
12419
+ this.graceUntil = 0;
12420
+ this.lastPosition = 0;
12421
+ this.lastProgressAt = 0;
12422
+ this.lastRealPlaybackAt = 0;
12423
+ this.healthySeconds = 0;
12424
+ this.step = 0;
12425
+ this.nextActionAt = 0;
12426
+ this.lastActionAt = 0;
12427
+ this.onPlaying = function() {
12428
+ if (_this.host.isAdPlaying()) return;
12429
+ _this.armed = true;
12430
+ _this.userPaused = false;
12431
+ };
12432
+ this.onPause = function() {
12433
+ if (_this.host.isBreakActive() || _this.host.isAdPlaying() || Date.now() - _this.lastActionAt < SELF_INFLICTED_PAUSE_WINDOW_MS) {
12434
+ return;
12435
+ }
12436
+ _this.userPaused = true;
12437
+ };
12438
+ this.onPlay = function() {
12439
+ _this.userPaused = false;
12440
+ };
12441
+ }
12442
+ _create_class(ContentHealthWatchdog, [
12443
+ {
12444
+ key: "start",
12445
+ value: function start() {
12446
+ var _this = this;
12447
+ if (this.intervalId !== void 0) return;
12448
+ var video = this.host.video;
12449
+ video.addEventListener("playing", this.onPlaying);
12450
+ video.addEventListener("pause", this.onPause);
12451
+ video.addEventListener("play", this.onPlay);
12452
+ this.resetProgress(Date.now());
12453
+ this.intervalId = window.setInterval(function() {
12454
+ return _this.tick();
12455
+ }, POLL_INTERVAL_MS);
12456
+ }
12457
+ },
12458
+ {
12459
+ key: "stop",
12460
+ value: function stop() {
12461
+ if (this.intervalId !== void 0) {
12462
+ clearInterval(this.intervalId);
12463
+ this.intervalId = void 0;
12464
+ }
12465
+ var video = this.host.video;
12466
+ video.removeEventListener("playing", this.onPlaying);
12467
+ video.removeEventListener("pause", this.onPause);
12468
+ video.removeEventListener("play", this.onPlay);
12469
+ this.armed = false;
12470
+ this.step = 0;
12471
+ }
12472
+ },
12473
+ {
12474
+ key: "resetProgress",
12475
+ value: function resetProgress(now) {
12476
+ this.lastPosition = this.host.video.currentTime;
12477
+ this.lastProgressAt = now;
12478
+ }
12479
+ },
12480
+ {
12481
+ key: "tick",
12482
+ value: function tick() {
12483
+ var now = Date.now();
12484
+ var video = this.host.video;
12485
+ var hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
12486
+ if (!this.armed || this.userPaused || hidden || video.ended || !this.host.isLiveStream() || !this.host.hasHlsInstance()) {
12487
+ this.resetProgress(now);
12488
+ return;
12489
+ }
12490
+ var breakActive = this.host.isBreakActive() || this.host.isAdPlaying();
12491
+ if (breakActive && !this.wasInBreak) {
12492
+ this.wasInBreak = true;
12493
+ this.breakStartedAt = now;
12494
+ this.stuckBreakLogged = false;
12495
+ }
12496
+ if (breakActive && now - this.breakStartedAt < MAX_BREAK_SUPPRESSION_MS) {
12497
+ this.resetProgress(now);
12498
+ return;
12499
+ }
12500
+ if (breakActive && !this.stuckBreakLogged) {
12501
+ this.stuckBreakLogged = true;
12502
+ console.warn("[ContentWatchdog] Ad break state active for over ".concat(MAX_BREAK_SUPPRESSION_MS / 6e4, " min — monitoring content anyway"));
12503
+ }
12504
+ if (!breakActive && this.wasInBreak) {
12505
+ this.wasInBreak = false;
12506
+ this.graceUntil = now + POST_BREAK_GRACE_MS;
12507
+ this.resetProgress(now);
12508
+ return;
12509
+ }
12510
+ var currentTime = video.currentTime;
12511
+ var delta = currentTime - this.lastPosition;
12512
+ this.lastPosition = currentTime;
12513
+ if (delta > 0.25 && delta <= MAX_NORMAL_ADVANCE_PER_TICK_S && !video.paused) {
12514
+ this.lastProgressAt = now;
12515
+ this.lastRealPlaybackAt = now;
12516
+ this.healthySeconds += delta;
12517
+ if (this.step > 0 && this.healthySeconds >= HEALTHY_PLAYBACK_SECONDS) {
12518
+ console.log("[ContentWatchdog] Content playback recovered after ".concat(this.step, " recovery step(s)"));
12519
+ this.step = 0;
12520
+ this.nextActionAt = 0;
12521
+ }
12522
+ return;
12523
+ }
12524
+ if (now < this.graceUntil) {
12525
+ this.lastProgressAt = now;
12526
+ return;
12527
+ }
12528
+ if (now - this.lastProgressAt < STALL_THRESHOLD_MS || now < this.nextActionAt) {
12529
+ return;
12530
+ }
12531
+ this.recover(now);
12532
+ }
12533
+ },
12534
+ {
12535
+ key: "recover",
12536
+ value: function recover(now) {
12537
+ var video = this.host.video;
12538
+ 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");
12539
+ var stalledForS = ((now - (this.lastRealPlaybackAt || this.lastProgressAt)) / 1e3).toFixed(0);
12540
+ var fullReloadOnly = this.host.requiresMediaPipelineResetAfterAds();
12541
+ var action = fullReloadOnly || this.step >= 2 ? "reload" : this.step === 0 ? "recover_media_error" : "restart_load";
12542
+ console.warn("[ContentWatchdog] Content stalled for ".concat(stalledForS, "s — ").concat(action, " (step ").concat(this.step + 1, ") — ").concat(state));
12543
+ this.lastActionAt = now;
12544
+ this.healthySeconds = 0;
12545
+ if (action === "recover_media_error") {
12546
+ this.host.recoverMediaError();
12547
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12548
+ } else if (action === "restart_load") {
12549
+ var liveSyncPos = this.host.getLiveSyncPosition();
12550
+ var position = liveSyncPos != null && liveSyncPos > video.currentTime ? liveSyncPos : video.currentTime;
12551
+ if (position !== video.currentTime) {
12552
+ video.currentTime = position;
12553
+ }
12554
+ this.host.restartHlsLoad(position);
12555
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12556
+ } else {
12557
+ var reloadIndex = fullReloadOnly ? this.step : this.step - 2;
12558
+ var cooldown = Math.min(RELOAD_BASE_COOLDOWN_MS * Math.pow(2, reloadIndex), RELOAD_MAX_COOLDOWN_MS);
12559
+ this.host.reloadLiveStream();
12560
+ this.nextActionAt = now + cooldown;
12561
+ }
12562
+ this.step++;
12563
+ this.resetProgress(now);
12564
+ if (video.paused) {
12565
+ var _video_play;
12566
+ (_video_play = video.play()) === null || _video_play === void 0 ? void 0 : _video_play.catch(function() {});
12567
+ }
12568
+ }
12569
+ }
12570
+ ]);
12571
+ return ContentHealthWatchdog;
12572
+ }();
12388
12573
  // src/player/StormcloudVideoPlayer.ts
12389
12574
  var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12390
12575
  function StormcloudVideoPlayer(config) {
@@ -12515,6 +12700,36 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12515
12700
  return _this.palNonce.sendAdTouch(event);
12516
12701
  }
12517
12702
  }, this.timing, this.adConfig, this.cueManager, this.placeholder);
12703
+ this.contentWatchdog = new ContentHealthWatchdog({
12704
+ video: this.video,
12705
+ isLiveStream: function isLiveStream() {
12706
+ return _this.hlsEngine.isLiveStream;
12707
+ },
12708
+ hasHlsInstance: function hasHlsInstance() {
12709
+ return !!_this.hlsEngine.hls;
12710
+ },
12711
+ isBreakActive: function isBreakActive() {
12712
+ return _this.adBreak.inAdBreak || _this.adBreak.isBreakActive();
12713
+ },
12714
+ isAdPlaying: function isAdPlaying() {
12715
+ return _this.adPlayer.isAdPlaying();
12716
+ },
12717
+ requiresMediaPipelineResetAfterAds: function requiresMediaPipelineResetAfterAds1() {
12718
+ return requiresMediaPipelineResetAfterAds();
12719
+ },
12720
+ getLiveSyncPosition: function getLiveSyncPosition() {
12721
+ return _this.hlsEngine.getLiveSyncPosition();
12722
+ },
12723
+ recoverMediaError: function recoverMediaError() {
12724
+ return _this.hlsEngine.recoverMediaError();
12725
+ },
12726
+ restartHlsLoad: function restartHlsLoad(position) {
12727
+ return _this.hlsEngine.restartLoadAt(position);
12728
+ },
12729
+ reloadLiveStream: function reloadLiveStream() {
12730
+ return _this.hlsEngine.reloadLiveStream();
12731
+ }
12732
+ });
12518
12733
  }
12519
12734
  _create_class(StormcloudVideoPlayer, [
12520
12735
  {
@@ -12835,6 +13050,9 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12835
13050
  }
12836
13051
  };
12837
13052
  this.video.addEventListener("emptied", this.emptiedHandler);
13053
+ if (this.config.contentStallWatchdog !== false) {
13054
+ this.contentWatchdog.start();
13055
+ }
12838
13056
  }
12839
13057
  },
12840
13058
  {
@@ -13319,6 +13537,7 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
13319
13537
  key: "destroy",
13320
13538
  value: function destroy() {
13321
13539
  var _this_adPlayer;
13540
+ this.contentWatchdog.stop();
13322
13541
  this.adBreak.cancelScheduledRequest();
13323
13542
  this.timing.destroy();
13324
13543
  this.adBreak.clearPendingAdBreak();