Source: lib/media/stall_detector.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.StallDetector');
  7. goog.provide('shaka.media.StallDetector.Implementation');
  8. goog.provide('shaka.media.StallDetector.MediaElementImplementation');
  9. goog.require('shaka.media.TimeRangesUtils');
  10. goog.require('shaka.util.IReleasable');
  11. /**
  12. * Some platforms/browsers can get stuck in the middle of a buffered range (e.g.
  13. * when seeking in a background tab). Detect when we get stuck so that the
  14. * player can respond.
  15. *
  16. * @implements {shaka.util.IReleasable}
  17. * @final
  18. */
  19. shaka.media.StallDetector = class {
  20. /**
  21. * @param {shaka.media.StallDetector.Implementation} implementation
  22. * @param {number} stallThresholdSeconds
  23. */
  24. constructor(implementation, stallThresholdSeconds) {
  25. /** @private {shaka.media.StallDetector.Implementation} */
  26. this.implementation_ = implementation;
  27. /** @private {boolean} */
  28. this.wasMakingProgress_ = implementation.shouldBeMakingProgress();
  29. /** @private {number} */
  30. this.value_ = implementation.getPresentationSeconds();
  31. /** @private {number} */
  32. this.lastUpdateSeconds_ = implementation.getWallSeconds();
  33. /** @private {boolean} */
  34. this.didJump_ = false;
  35. /**
  36. * The amount of time in seconds that we must have the same value of
  37. * |value_| before we declare it as a stall.
  38. *
  39. * @private {number}
  40. */
  41. this.stallThresholdSeconds_ = stallThresholdSeconds;
  42. /** @private {function(number, number)} */
  43. this.onStall_ = () => {};
  44. }
  45. /** @override */
  46. release() {
  47. // Drop external references to make things easier on the GC.
  48. this.implementation_ = null;
  49. this.onStall_ = () => {};
  50. }
  51. /**
  52. * Set the callback that should be called when a stall is detected. Calling
  53. * this will override any previous calls to |onStall|.
  54. *
  55. * @param {function(number, number)} doThis
  56. */
  57. onStall(doThis) {
  58. this.onStall_ = doThis;
  59. }
  60. /**
  61. * Have the detector update itself and fire the "on stall" callback if a stall
  62. * was detected.
  63. *
  64. * @return {boolean} True if action was taken.
  65. */
  66. poll() {
  67. const impl = this.implementation_;
  68. const shouldBeMakingProgress = impl.shouldBeMakingProgress();
  69. const value = impl.getPresentationSeconds();
  70. const wallTimeSeconds = impl.getWallSeconds();
  71. const acceptUpdate = this.value_ != value ||
  72. this.wasMakingProgress_ != shouldBeMakingProgress;
  73. if (acceptUpdate) {
  74. this.lastUpdateSeconds_ = wallTimeSeconds;
  75. this.value_ = value;
  76. this.wasMakingProgress_ = shouldBeMakingProgress;
  77. this.didJump_ = false;
  78. }
  79. const stallSeconds = wallTimeSeconds - this.lastUpdateSeconds_;
  80. const triggerCallback = stallSeconds >= this.stallThresholdSeconds_ &&
  81. shouldBeMakingProgress && !this.didJump_;
  82. if (triggerCallback) {
  83. this.onStall_(this.value_, stallSeconds);
  84. this.didJump_ = true;
  85. // If the onStall_ method updated the current time, update our stored
  86. // value so we don't think that was an update.
  87. this.value_ = impl.getPresentationSeconds();
  88. }
  89. return triggerCallback;
  90. }
  91. };
  92. /**
  93. * @interface
  94. */
  95. shaka.media.StallDetector.Implementation = class {
  96. /**
  97. * Check if the presentation time should be changing. This will return |true|
  98. * when we expect the presentation time to change.
  99. *
  100. * @return {boolean}
  101. */
  102. shouldBeMakingProgress() {}
  103. /**
  104. * Get the presentation time in seconds.
  105. *
  106. * @return {number}
  107. */
  108. getPresentationSeconds() {}
  109. /**
  110. * Get the time wall time in seconds.
  111. *
  112. * @return {number}
  113. */
  114. getWallSeconds() {}
  115. };
  116. /**
  117. * Some platforms/browsers can get stuck in the middle of a buffered range (e.g.
  118. * when seeking in a background tab). Force a seek to help get it going again.
  119. *
  120. * @implements {shaka.media.StallDetector.Implementation}
  121. * @final
  122. */
  123. shaka.media.StallDetector.MediaElementImplementation = class {
  124. /**
  125. * @param {!HTMLMediaElement} mediaElement
  126. */
  127. constructor(mediaElement) {
  128. /** @private {!HTMLMediaElement} */
  129. this.mediaElement_ = mediaElement;
  130. }
  131. /** @override */
  132. shouldBeMakingProgress() {
  133. // If we are not trying to play, the lack of change could be misidentified
  134. // as a stall.
  135. if (this.mediaElement_.paused) {
  136. return false;
  137. }
  138. if (this.mediaElement_.playbackRate == 0) {
  139. return false;
  140. }
  141. // If we have don't have enough content, we are not stalled, we are
  142. // buffering.
  143. if (this.mediaElement_.buffered.length == 0) {
  144. return false;
  145. }
  146. return shaka.media.StallDetector.MediaElementImplementation.hasContentFor_(
  147. this.mediaElement_.buffered,
  148. /* timeInSeconds= */ this.mediaElement_.currentTime);
  149. }
  150. /** @override */
  151. getPresentationSeconds() {
  152. return this.mediaElement_.currentTime;
  153. }
  154. /** @override */
  155. getWallSeconds() {
  156. return Date.now() / 1000;
  157. }
  158. /**
  159. * Check if we have buffered enough content to play at |timeInSeconds|. Ignore
  160. * the end of the buffered range since it may not play any more on all
  161. * platforms.
  162. *
  163. * @param {!TimeRanges} buffered
  164. * @param {number} timeInSeconds
  165. * @return {boolean}
  166. * @private
  167. */
  168. static hasContentFor_(buffered, timeInSeconds) {
  169. const TimeRangesUtils = shaka.media.TimeRangesUtils;
  170. for (const {start, end} of TimeRangesUtils.getBufferedInfo(buffered)) {
  171. // Can be as much as 100ms before the range
  172. if (timeInSeconds < start - 0.1) {
  173. continue;
  174. }
  175. // Must be at least 500ms inside the range
  176. if (timeInSeconds > end - 0.5) {
  177. continue;
  178. }
  179. return true;
  180. }
  181. return false;
  182. }
  183. };