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