UNPKG

shaka-player

Version:
230 lines (200 loc) 6.34 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.media.StallDetector'); goog.provide('shaka.media.StallDetector.Implementation'); goog.provide('shaka.media.StallDetector.MediaElementImplementation'); goog.require('shaka.media.TimeRangesUtils'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IReleasable'); /** * Some platforms/browsers can get stuck in the middle of a buffered range (e.g. * when seeking in a background tab). Detect when we get stuck so that the * player can respond. * * @implements {shaka.util.IReleasable} * @final */ shaka.media.StallDetector = class { /** * @param {shaka.media.StallDetector.Implementation} implementation * @param {number} stallThresholdSeconds * @param {function(!Event)} onEvent * Called when an event is raised to be sent to the application. */ constructor(implementation, stallThresholdSeconds, onEvent) { /** @private {?function(!Event)} */ this.onEvent_ = onEvent; /** @private {shaka.media.StallDetector.Implementation} */ this.implementation_ = implementation; /** @private {boolean} */ this.wasMakingProgress_ = implementation.shouldBeMakingProgress(); /** @private {number} */ this.value_ = implementation.getPresentationSeconds(); /** @private {number} */ this.lastUpdateSeconds_ = implementation.getWallSeconds(); /** @private {boolean} */ this.didJump_ = false; /** @private {number} */ this.stallsDetected_ = 0; /** * The amount of time in seconds that we must have the same value of * |value_| before we declare it as a stall. * * @private {number} */ this.stallThresholdSeconds_ = stallThresholdSeconds; /** @private {function(number, number)} */ this.onStall_ = () => {}; } /** @override */ release() { // Drop external references to make things easier on the GC. this.implementation_ = null; this.onEvent_ = null; this.onStall_ = () => {}; } /** * Set the callback that should be called when a stall is detected. Calling * this will override any previous calls to |onStall|. * * @param {function(number, number)} doThis */ onStall(doThis) { this.onStall_ = doThis; } /** * Returns the number of playback stalls detected. */ getStallsDetected() { return this.stallsDetected_; } /** * Have the detector update itself and fire the "on stall" callback if a stall * was detected. * * @return {boolean} True if action was taken. */ poll() { const impl = this.implementation_; const shouldBeMakingProgress = impl.shouldBeMakingProgress(); const value = impl.getPresentationSeconds(); const wallTimeSeconds = impl.getWallSeconds(); const acceptUpdate = this.value_ != value || this.wasMakingProgress_ != shouldBeMakingProgress; if (acceptUpdate) { this.lastUpdateSeconds_ = wallTimeSeconds; this.value_ = value; this.wasMakingProgress_ = shouldBeMakingProgress; this.didJump_ = false; } const stallSeconds = wallTimeSeconds - this.lastUpdateSeconds_; const triggerCallback = stallSeconds >= this.stallThresholdSeconds_ && shouldBeMakingProgress && !this.didJump_; if (triggerCallback) { this.onStall_(this.value_, stallSeconds); this.didJump_ = true; // If the onStall_ method updated the current time, update our stored // value so we don't think that was an update. this.value_ = impl.getPresentationSeconds(); this.stallsDetected_++; this.onEvent_(new shaka.util.FakeEvent( shaka.util.FakeEvent.EventName.StallDetected)); } return triggerCallback; } }; /** * @interface */ shaka.media.StallDetector.Implementation = class { /** * Check if the presentation time should be changing. This will return |true| * when we expect the presentation time to change. * * @return {boolean} */ shouldBeMakingProgress() {} /** * Get the presentation time in seconds. * * @return {number} */ getPresentationSeconds() {} /** * Get the time wall time in seconds. * * @return {number} */ getWallSeconds() {} }; /** * Some platforms/browsers can get stuck in the middle of a buffered range (e.g. * when seeking in a background tab). Force a seek to help get it going again. * * @implements {shaka.media.StallDetector.Implementation} * @final */ shaka.media.StallDetector.MediaElementImplementation = class { /** * @param {!HTMLMediaElement} mediaElement */ constructor(mediaElement) { /** @private {!HTMLMediaElement} */ this.mediaElement_ = mediaElement; } /** @override */ shouldBeMakingProgress() { // If we are not trying to play, the lack of change could be misidentified // as a stall. if (this.mediaElement_.paused) { return false; } if (this.mediaElement_.playbackRate == 0) { return false; } // If we have don't have enough content, we are not stalled, we are // buffering. if (this.mediaElement_.buffered.length == 0) { return false; } return shaka.media.StallDetector.MediaElementImplementation.hasContentFor_( this.mediaElement_.buffered, /* timeInSeconds= */ this.mediaElement_.currentTime); } /** @override */ getPresentationSeconds() { return this.mediaElement_.currentTime; } /** @override */ getWallSeconds() { return Date.now() / 1000; } /** * Check if we have buffered enough content to play at |timeInSeconds|. Ignore * the end of the buffered range since it may not play any more on all * platforms. * * @param {!TimeRanges} buffered * @param {number} timeInSeconds * @return {boolean} * @private */ static hasContentFor_(buffered, timeInSeconds) { const TimeRangesUtils = shaka.media.TimeRangesUtils; for (const {start, end} of TimeRangesUtils.getBufferedInfo(buffered)) { // Can be as much as 100ms before the range if (timeInSeconds < start - 0.1) { continue; } // Must be at least 500ms inside the range if (timeInSeconds > end - 0.5) { continue; } return true; } return false; } };