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/index.d.ts CHANGED
@@ -58,6 +58,7 @@ interface StormcloudVideoPlayerConfig {
58
58
  debugAdTiming?: boolean;
59
59
  adFailsafeTimeoutMs?: number;
60
60
  adMediaStartTimeoutMs?: number;
61
+ contentStallWatchdog?: boolean;
61
62
  adBreakCheckIntervalMs?: number;
62
63
  maxAdBreakExtensionMs?: number;
63
64
  minRemainingMsToStartAd?: number;
@@ -285,6 +286,7 @@ declare class StormcloudVideoPlayer {
285
286
  private readonly hlsEngine;
286
287
  private readonly adBreak;
287
288
  private readonly floorLadder;
289
+ private readonly contentWatchdog;
288
290
  private edgePodBaseUrl;
289
291
  constructor(config: StormcloudVideoPlayerConfig);
290
292
  private get debug();
@@ -374,9 +376,13 @@ interface BaseStormcloudPlayerProps {
374
376
  licenseKey?: string;
375
377
  isVmap?: boolean;
376
378
  vmapUrl?: string;
379
+ vastTagUrl?: string;
377
380
  adFailsafeTimeoutMs?: number;
378
381
  minSegmentsBeforePlay?: number;
379
382
  ctvAdRequest?: boolean;
383
+ minRemainingMsToRequestAd?: number;
384
+ maxNoFillsPerBreak?: number;
385
+ maxAdBreakExtensionMs?: number;
380
386
  onReady?: (player: StormcloudVideoPlayer) => void;
381
387
  onStart?: () => void;
382
388
  onPlay?: () => void;
package/lib/index.js CHANGED
@@ -2243,6 +2243,9 @@ function requiresMediaPipelineResetAfterAds() {
2243
2243
  var browser = detectBrowser();
2244
2244
  return browser.name === "Samsung Tizen" && ((_browser_tizenVersion = browser.tizenVersion) !== null && _browser_tizenVersion !== void 0 ? _browser_tizenVersion : 0) >= 5;
2245
2245
  }
2246
+ function prefersProgressiveAdMedia() {
2247
+ return detectBrowser().isSmartTV;
2248
+ }
2246
2249
  function getBrowserConfigOverrides() {
2247
2250
  var browser = detectBrowser();
2248
2251
  var overrides = {};
@@ -3109,6 +3112,7 @@ function createHlsAdPlayer(contentVideo, options) {
3109
3112
  var probe = adVideoElement !== null && adVideoElement !== void 0 ? adVideoElement : document.createElement("video");
3110
3113
  var pageIsHttps = typeof location !== "undefined" && location.protocol === "https:";
3111
3114
  var target = getMediaTargetQuality();
3115
+ var deprioritizeHls = prefersProgressiveAdMedia();
3112
3116
  var accepted = [];
3113
3117
  mediaFiles.forEach(function(file, index) {
3114
3118
  var isHls = isHlsMediaFile(file);
@@ -3139,19 +3143,24 @@ function createHlsAdPlayer(contentVideo, options) {
3139
3143
  var bitrateDiff = Math.abs((file.bitrate || 5e3) * 1e3 - target.bitrate);
3140
3144
  score = resolutionDiff * 2 + bitrateDiff / 1e3;
3141
3145
  }
3146
+ var deprioritizedHls = deprioritizeHls && isHls;
3142
3147
  accepted.push({
3143
3148
  file: file,
3144
3149
  score: score,
3145
- insecure: insecure
3150
+ insecure: insecure,
3151
+ deprioritizedHls: deprioritizedHls
3146
3152
  });
3147
3153
  logFunnel("media_candidate", _object_spread_props(_object_spread({}, details), {
3148
3154
  reason: "accepted",
3149
- why: insecure ? "insecure_http_deprioritized" : "playable",
3155
+ why: insecure ? "insecure_http_deprioritized" : deprioritizedHls ? "hls_deprioritized_on_tv" : "playable",
3150
3156
  score: score
3151
3157
  }));
3152
3158
  });
3153
3159
  accepted.sort(function(a, b) {
3154
3160
  if (a.insecure !== b.insecure) return a.insecure ? 1 : -1;
3161
+ if (a.deprioritizedHls !== b.deprioritizedHls) {
3162
+ return a.deprioritizedHls ? 1 : -1;
3163
+ }
3155
3164
  return a.score - b.score;
3156
3165
  });
3157
3166
  return accepted.map(function(entry) {
@@ -4824,8 +4833,8 @@ function createHlsAdPlayer(contentVideo, options) {
4824
4833
  console.warn("[HlsAdPlayer] Error preparing ad video for new source:", error);
4825
4834
  }
4826
4835
  }
4827
- function recreateAdVideoElement() {
4828
- console.log("[HlsAdPlayer] Recreating ad video element (single-decoder platform)");
4836
+ function recreateAdVideoElement(reason) {
4837
+ console.log("[HlsAdPlayer] Recreating ad video element (".concat(reason, ")"));
4829
4838
  teardownAdEventListeners();
4830
4839
  prepareVideoElementForNewSource();
4831
4840
  if (adVideoElement) {
@@ -4869,8 +4878,9 @@ function createHlsAdPlayer(contentVideo, options) {
4869
4878
  }
4870
4879
  function resetVideoElementForNextSource() {
4871
4880
  clearStallWatchdog();
4872
- if (requiresMediaPipelineResetAfterAds()) {
4873
- recreateAdVideoElement();
4881
+ var previousSourceUsedHlsJs = adHls !== void 0;
4882
+ if (requiresMediaPipelineResetAfterAds() || previousSourceUsedHlsJs) {
4883
+ recreateAdVideoElement(previousSourceUsedHlsJs ? "after hls.js source" : "single-decoder platform");
4874
4884
  setupAdEventListeners();
4875
4885
  } else {
4876
4886
  prepareVideoElementForNewSource();
@@ -12367,6 +12377,182 @@ var AdBreakOrchestrator = /*#__PURE__*/ function() {
12367
12377
  ]);
12368
12378
  return AdBreakOrchestrator;
12369
12379
  }();
12380
+ // src/player/ContentHealthWatchdog.ts
12381
+ var POLL_INTERVAL_MS = 2e3;
12382
+ var STALL_THRESHOLD_MS = 12e3;
12383
+ var POST_BREAK_GRACE_MS = 5e4;
12384
+ var MAX_BREAK_SUPPRESSION_MS = 10 * 60 * 1e3;
12385
+ var SELF_INFLICTED_PAUSE_WINDOW_MS = 5e3;
12386
+ var HEALTHY_PLAYBACK_SECONDS = 5;
12387
+ var MAX_NORMAL_ADVANCE_PER_TICK_S = 4;
12388
+ var LIGHT_STEP_COOLDOWN_MS = 1e4;
12389
+ var RELOAD_BASE_COOLDOWN_MS = 15e3;
12390
+ var RELOAD_MAX_COOLDOWN_MS = 12e4;
12391
+ var ContentHealthWatchdog = /*#__PURE__*/ function() {
12392
+ "use strict";
12393
+ function ContentHealthWatchdog(host) {
12394
+ var _this = this;
12395
+ _class_call_check(this, ContentHealthWatchdog);
12396
+ this.host = host;
12397
+ this.armed = false;
12398
+ this.userPaused = false;
12399
+ this.wasInBreak = false;
12400
+ this.breakStartedAt = 0;
12401
+ this.stuckBreakLogged = false;
12402
+ this.graceUntil = 0;
12403
+ this.lastPosition = 0;
12404
+ this.lastProgressAt = 0;
12405
+ this.lastRealPlaybackAt = 0;
12406
+ this.healthySeconds = 0;
12407
+ this.step = 0;
12408
+ this.nextActionAt = 0;
12409
+ this.lastActionAt = 0;
12410
+ this.onPlaying = function() {
12411
+ if (_this.host.isAdPlaying()) return;
12412
+ _this.armed = true;
12413
+ _this.userPaused = false;
12414
+ };
12415
+ this.onPause = function() {
12416
+ if (_this.host.isBreakActive() || _this.host.isAdPlaying() || Date.now() - _this.lastActionAt < SELF_INFLICTED_PAUSE_WINDOW_MS) {
12417
+ return;
12418
+ }
12419
+ _this.userPaused = true;
12420
+ };
12421
+ this.onPlay = function() {
12422
+ _this.userPaused = false;
12423
+ };
12424
+ }
12425
+ _create_class(ContentHealthWatchdog, [
12426
+ {
12427
+ key: "start",
12428
+ value: function start() {
12429
+ var _this = this;
12430
+ if (this.intervalId !== void 0) return;
12431
+ var video = this.host.video;
12432
+ video.addEventListener("playing", this.onPlaying);
12433
+ video.addEventListener("pause", this.onPause);
12434
+ video.addEventListener("play", this.onPlay);
12435
+ this.resetProgress(Date.now());
12436
+ this.intervalId = window.setInterval(function() {
12437
+ return _this.tick();
12438
+ }, POLL_INTERVAL_MS);
12439
+ }
12440
+ },
12441
+ {
12442
+ key: "stop",
12443
+ value: function stop() {
12444
+ if (this.intervalId !== void 0) {
12445
+ clearInterval(this.intervalId);
12446
+ this.intervalId = void 0;
12447
+ }
12448
+ var video = this.host.video;
12449
+ video.removeEventListener("playing", this.onPlaying);
12450
+ video.removeEventListener("pause", this.onPause);
12451
+ video.removeEventListener("play", this.onPlay);
12452
+ this.armed = false;
12453
+ this.step = 0;
12454
+ }
12455
+ },
12456
+ {
12457
+ key: "resetProgress",
12458
+ value: function resetProgress(now) {
12459
+ this.lastPosition = this.host.video.currentTime;
12460
+ this.lastProgressAt = now;
12461
+ }
12462
+ },
12463
+ {
12464
+ key: "tick",
12465
+ value: function tick() {
12466
+ var now = Date.now();
12467
+ var video = this.host.video;
12468
+ var hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
12469
+ if (!this.armed || this.userPaused || hidden || video.ended || !this.host.isLiveStream() || !this.host.hasHlsInstance()) {
12470
+ this.resetProgress(now);
12471
+ return;
12472
+ }
12473
+ var breakActive = this.host.isBreakActive() || this.host.isAdPlaying();
12474
+ if (breakActive && !this.wasInBreak) {
12475
+ this.wasInBreak = true;
12476
+ this.breakStartedAt = now;
12477
+ this.stuckBreakLogged = false;
12478
+ }
12479
+ if (breakActive && now - this.breakStartedAt < MAX_BREAK_SUPPRESSION_MS) {
12480
+ this.resetProgress(now);
12481
+ return;
12482
+ }
12483
+ if (breakActive && !this.stuckBreakLogged) {
12484
+ this.stuckBreakLogged = true;
12485
+ console.warn("[ContentWatchdog] Ad break state active for over ".concat(MAX_BREAK_SUPPRESSION_MS / 6e4, " min — monitoring content anyway"));
12486
+ }
12487
+ if (!breakActive && this.wasInBreak) {
12488
+ this.wasInBreak = false;
12489
+ this.graceUntil = now + POST_BREAK_GRACE_MS;
12490
+ this.resetProgress(now);
12491
+ return;
12492
+ }
12493
+ var currentTime = video.currentTime;
12494
+ var delta = currentTime - this.lastPosition;
12495
+ this.lastPosition = currentTime;
12496
+ if (delta > 0.25 && delta <= MAX_NORMAL_ADVANCE_PER_TICK_S && !video.paused) {
12497
+ this.lastProgressAt = now;
12498
+ this.lastRealPlaybackAt = now;
12499
+ this.healthySeconds += delta;
12500
+ if (this.step > 0 && this.healthySeconds >= HEALTHY_PLAYBACK_SECONDS) {
12501
+ console.log("[ContentWatchdog] Content playback recovered after ".concat(this.step, " recovery step(s)"));
12502
+ this.step = 0;
12503
+ this.nextActionAt = 0;
12504
+ }
12505
+ return;
12506
+ }
12507
+ if (now < this.graceUntil) {
12508
+ this.lastProgressAt = now;
12509
+ return;
12510
+ }
12511
+ if (now - this.lastProgressAt < STALL_THRESHOLD_MS || now < this.nextActionAt) {
12512
+ return;
12513
+ }
12514
+ this.recover(now);
12515
+ }
12516
+ },
12517
+ {
12518
+ key: "recover",
12519
+ value: function recover(now) {
12520
+ var video = this.host.video;
12521
+ 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");
12522
+ var stalledForS = ((now - (this.lastRealPlaybackAt || this.lastProgressAt)) / 1e3).toFixed(0);
12523
+ var fullReloadOnly = this.host.requiresMediaPipelineResetAfterAds();
12524
+ var action = fullReloadOnly || this.step >= 2 ? "reload" : this.step === 0 ? "recover_media_error" : "restart_load";
12525
+ console.warn("[ContentWatchdog] Content stalled for ".concat(stalledForS, "s — ").concat(action, " (step ").concat(this.step + 1, ") — ").concat(state));
12526
+ this.lastActionAt = now;
12527
+ this.healthySeconds = 0;
12528
+ if (action === "recover_media_error") {
12529
+ this.host.recoverMediaError();
12530
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12531
+ } else if (action === "restart_load") {
12532
+ var liveSyncPos = this.host.getLiveSyncPosition();
12533
+ var position = liveSyncPos != null && liveSyncPos > video.currentTime ? liveSyncPos : video.currentTime;
12534
+ if (position !== video.currentTime) {
12535
+ video.currentTime = position;
12536
+ }
12537
+ this.host.restartHlsLoad(position);
12538
+ this.nextActionAt = now + LIGHT_STEP_COOLDOWN_MS;
12539
+ } else {
12540
+ var reloadIndex = fullReloadOnly ? this.step : this.step - 2;
12541
+ var cooldown = Math.min(RELOAD_BASE_COOLDOWN_MS * Math.pow(2, reloadIndex), RELOAD_MAX_COOLDOWN_MS);
12542
+ this.host.reloadLiveStream();
12543
+ this.nextActionAt = now + cooldown;
12544
+ }
12545
+ this.step++;
12546
+ this.resetProgress(now);
12547
+ if (video.paused) {
12548
+ var _video_play;
12549
+ (_video_play = video.play()) === null || _video_play === void 0 ? void 0 : _video_play.catch(function() {});
12550
+ }
12551
+ }
12552
+ }
12553
+ ]);
12554
+ return ContentHealthWatchdog;
12555
+ }();
12370
12556
  // src/player/StormcloudVideoPlayer.ts
12371
12557
  var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12372
12558
  "use strict";
@@ -12498,6 +12684,36 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12498
12684
  return _this.palNonce.sendAdTouch(event);
12499
12685
  }
12500
12686
  }, this.timing, this.adConfig, this.cueManager, this.placeholder);
12687
+ this.contentWatchdog = new ContentHealthWatchdog({
12688
+ video: this.video,
12689
+ isLiveStream: function isLiveStream() {
12690
+ return _this.hlsEngine.isLiveStream;
12691
+ },
12692
+ hasHlsInstance: function hasHlsInstance() {
12693
+ return !!_this.hlsEngine.hls;
12694
+ },
12695
+ isBreakActive: function isBreakActive() {
12696
+ return _this.adBreak.inAdBreak || _this.adBreak.isBreakActive();
12697
+ },
12698
+ isAdPlaying: function isAdPlaying() {
12699
+ return _this.adPlayer.isAdPlaying();
12700
+ },
12701
+ requiresMediaPipelineResetAfterAds: function requiresMediaPipelineResetAfterAds1() {
12702
+ return requiresMediaPipelineResetAfterAds();
12703
+ },
12704
+ getLiveSyncPosition: function getLiveSyncPosition() {
12705
+ return _this.hlsEngine.getLiveSyncPosition();
12706
+ },
12707
+ recoverMediaError: function recoverMediaError() {
12708
+ return _this.hlsEngine.recoverMediaError();
12709
+ },
12710
+ restartHlsLoad: function restartHlsLoad(position) {
12711
+ return _this.hlsEngine.restartLoadAt(position);
12712
+ },
12713
+ reloadLiveStream: function reloadLiveStream() {
12714
+ return _this.hlsEngine.reloadLiveStream();
12715
+ }
12716
+ });
12501
12717
  }
12502
12718
  _create_class(StormcloudVideoPlayer, [
12503
12719
  {
@@ -12818,6 +13034,9 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
12818
13034
  }
12819
13035
  };
12820
13036
  this.video.addEventListener("emptied", this.emptiedHandler);
13037
+ if (this.config.contentStallWatchdog !== false) {
13038
+ this.contentWatchdog.start();
13039
+ }
12821
13040
  }
12822
13041
  },
12823
13042
  {
@@ -13302,6 +13521,7 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
13302
13521
  key: "destroy",
13303
13522
  value: function destroy() {
13304
13523
  var _this_adPlayer;
13524
+ this.contentWatchdog.stop();
13305
13525
  this.adBreak.cancelScheduledRequest();
13306
13526
  this.timing.destroy();
13307
13527
  this.adBreak.clearPendingAdBreak();
@@ -14975,6 +15195,12 @@ var HlsPlayer = /*#__PURE__*/ function(Component) {
14975
15195
  if (_this1.props.adFailsafeTimeoutMs !== void 0) config.adFailsafeTimeoutMs = _this1.props.adFailsafeTimeoutMs;
14976
15196
  if (_this1.props.minSegmentsBeforePlay !== void 0) config.minSegmentsBeforePlay = _this1.props.minSegmentsBeforePlay;
14977
15197
  if (_this1.props.ctvAdRequest !== void 0) config.ctvAdRequest = _this1.props.ctvAdRequest;
15198
+ if (_this1.props.vastTagUrl !== void 0) config.vastTagUrl = _this1.props.vastTagUrl;
15199
+ if (_this1.props.isVmap !== void 0) config.isVmap = _this1.props.isVmap;
15200
+ if (_this1.props.vmapUrl !== void 0) config.vmapUrl = _this1.props.vmapUrl;
15201
+ if (_this1.props.minRemainingMsToRequestAd !== void 0) config.minRemainingMsToRequestAd = _this1.props.minRemainingMsToRequestAd;
15202
+ if (_this1.props.maxNoFillsPerBreak !== void 0) config.maxNoFillsPerBreak = _this1.props.maxNoFillsPerBreak;
15203
+ if (_this1.props.maxAdBreakExtensionMs !== void 0) config.maxAdBreakExtensionMs = _this1.props.maxAdBreakExtensionMs;
14978
15204
  _this1.player = new StormcloudVideoPlayer(config);
14979
15205
  (_this_props_onMount = (_this_props = _this1.props).onMount) === null || _this_props_onMount === void 0 ? void 0 : _this_props_onMount.call(_this_props, _this1);
14980
15206
  return [
@@ -15740,9 +15966,13 @@ var SUPPORTED_PROPS = [
15740
15966
  "licenseKey",
15741
15967
  "isVmap",
15742
15968
  "vmapUrl",
15969
+ "vastTagUrl",
15743
15970
  "adFailsafeTimeoutMs",
15744
15971
  "minSegmentsBeforePlay",
15745
15972
  "ctvAdRequest",
15973
+ "minRemainingMsToRequestAd",
15974
+ "maxNoFillsPerBreak",
15975
+ "maxAdBreakExtensionMs",
15746
15976
  "onReady",
15747
15977
  "onStart",
15748
15978
  "onPlay",