Skip to content

Commit

Permalink
Make Stall Detector Configurable
Browse files Browse the repository at this point in the history
Make the stall detector configurable via Player.configure. This
should make it easier for developers to work around stall issues
and make it easier for us to find ideal configuration for
different platforms.

This exposes:

- Enabled   : A flag for whether or not the player should
               initialize and use a stall detector.

- Threshold : The number of seconds that must pass without
               progress before firing the stall-event.

 - Skip     : The number of seconds to skip forward after a
              stall-event.

To make enable possible, the stall detector was made optional and
is now passed into the gap jumper. This allows Playhead to evaluate
the config and create the detector as desired.

Issue #1839

Change-Id: Ife1bf34b4cfc7b469f4b0beb312a06d5b5cd81a9
  • Loading branch information
vaage committed Mar 20, 2019
1 parent 9c224c1 commit fd2d61d
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 21 deletions.
15 changes: 14 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,10 @@ shaka.extern.ManifestConfiguration;
* jumpLargeGaps: boolean,
* durationBackoff: number,
* forceTransmuxTS: boolean,
* safeSeekOffset: number
* safeSeekOffset: number,
* stallEnabled: boolean,
* stallThreshold: number,
* stallSkip: number
* }}
*
* @description
Expand Down Expand Up @@ -653,6 +656,16 @@ shaka.extern.ManifestConfiguration;
* more time to buffer before falling outside again, but increases the forward
* jump in the stream skipping more content. This is helpful for lower
* bandwidth scenarios. Defaults to 5 if not provided.
* @property {boolean} stallEnabled
* When set to |true|, the stall detector logic will run, skipping forward
* |stallSkip| seconds whenever the playhead stops moving for |stallThreshold|
* seconds.
* @property {number} stallThreshold
* The maximum number of seconds that may elapse without the playhead moving
* (when playback is expected) before it will be labeled as a stall.
* @property {number} stallSkip
* The number of seconds that the player will skip forward when a stall has
* been detected.
* @exportDoc
*/
shaka.extern.StreamingConfiguration;
Expand Down
33 changes: 20 additions & 13 deletions lib/media/gap_jumping_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ goog.require('shaka.util.Timer');
* @param {!HTMLMediaElement} video
* @param {!shaka.media.PresentationTimeline} timeline
* @param {shaka.extern.StreamingConfiguration} config
* @param {shaka.media.StallDetector} stallDetector
* The stall detector is used to keep the playhead moving while in a playable
* region. The gap jumping controller takes ownership over the stall detector.
* If no stall detection logic is desired, |null| may be provided.
* @param {function(!Event)} onEvent Called when an event is raised to be sent
* to the application.
*
* @constructor
* @struct
* @implements {shaka.util.IReleasable}
*/
shaka.media.GapJumpingController = function(video, timeline, config, onEvent) {
shaka.media.GapJumpingController = function(
video, timeline, config, stallDetector, onEvent) {
/** @private {HTMLMediaElement} */
this.video_ = video;

Expand All @@ -67,20 +72,13 @@ shaka.media.GapJumpingController = function(video, timeline, config, onEvent) {
this.didFireLargeGap_ = false;

/**
* Used to detect stalls and to "jump-start" playback when a stall is
* detected. We will update it manually to avoid conflicts with gap-jumping.
* The stall detector tries to keep the playhead moving forward. It is managed
* by the gap-jumping controller to avoid conflicts. On some platforms, the
* stall detector is not wanted, so it may be null.
*
* @private {shaka.media.StallDetector}
*/
this.stallDetector_ = new shaka.media.StallDetector(
new shaka.media.StallDetector.MediaElementImplementation(video),
/* thresholdSeconds= */ 1.0);
// When we see a stall, seek forward 1/10 of a second.
this.stallDetector_.onStall((at, duration) => {
shaka.log.debug('Stall detected at', at, 'for', duration, 'seconds.');
shaka.log.debug('Seeking forward 0.1 seconds because of stall.');
video.currentTime += 0.1;
});
this.stallDetector_ = stallDetector;

/** @private {boolean} */
this.hadSegmentAppended_ = false;
Expand Down Expand Up @@ -122,6 +120,11 @@ shaka.media.GapJumpingController.prototype.release = function() {
this.gapJumpTimer_ = null;
}

if (this.stallDetector_) {
this.stallDetector_.release();
this.stallDetector_ = null;
}

this.onEvent_ = null;
this.timeline_ = null;
this.video_ = null;
Expand Down Expand Up @@ -185,9 +188,13 @@ shaka.media.GapJumpingController.prototype.onPollGapJump_ = function() {

// The current time is unbuffered or is too far from a gap.
if (gapIndex == null) {
this.stallDetector_.poll();
if (this.stallDetector_) {
this.stallDetector_.poll();
}

return;
}

// If we are before the first buffered range, this could be an unbuffered
// seek. So wait until a segment is appended so we are sure it is a gap.
if (gapIndex == 0 && !this.hadSegmentAppended_) {
Expand Down
46 changes: 45 additions & 1 deletion lib/media/playhead.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ shaka.media.Playhead = function(

/** @private {shaka.media.GapJumpingController} */
this.gapController_ = new shaka.media.GapJumpingController(
video, timeline, config, onEvent);
video,
timeline,
config,
this.createStallDetector_(video, config),
onEvent);

/** @private {shaka.media.VideoWrapper} */
this.videoWrapper_ = new shaka.media.VideoWrapper(
Expand Down Expand Up @@ -405,3 +409,43 @@ shaka.media.Playhead.prototype.clampTime_ = function(time) {

return time;
};


/**
* Create and configure a stall detector using the player's streaming
* configuration settings. If the player is configured to have no stall
* detector, this will return |null|.
*
* @param {!HTMLMediaElement} mediaElement
* @param {shaka.extern.StreamingConfiguration} config
* @return {shaka.media.StallDetector}
* @private
*/
shaka.media.Playhead.prototype.createStallDetector_ = function(
mediaElement, config) {
if (!config.stallEnabled) {
return null;
}

// Cache the values from the config so that changes to the config won't change
// the initialized behaviour.
const threshold = config.stallThreshold;
const skip = config.stallSkip;

// When we see a stall, we will try to "jump-start" playback by moving the
// playhead forward.
const detector = new shaka.media.StallDetector(
new shaka.media.StallDetector.MediaElementImplementation(mediaElement),
threshold);

detector.onStall((at, duration) => {
shaka.log.debug([
'Stall detected at', at, 'for', duration, 'seconds. Seeking forward',
skip, 'seconds.',
].join(' '));

mediaElement.currentTime += skip;
});

return detector;
};
14 changes: 8 additions & 6 deletions lib/media/stall_detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
goog.provide('shaka.media.StallDetector');
goog.provide('shaka.media.StallDetector.Implementation');
goog.provide('shaka.media.StallDetector.MediaElementImplementation');
goog.require('shaka.util.Platform');

/**
* 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 {
Expand Down Expand Up @@ -55,6 +55,13 @@ shaka.media.StallDetector = class {
this.onStall_ = () => {};
}

/** @override */
release() {
// Drop external references to make things easier on the GC.
this.implementation_ = null;
this.onStall_ = () => {};
}

/**
* Set the callback that should be called when a stall is detected. Calling
* this will override any previous calls to |onStall|.
Expand Down Expand Up @@ -142,11 +149,6 @@ shaka.media.StallDetector.MediaElementImplementation = class {

/** @override */
shouldBeMakingProgress() {
// If we are in WebOS platform disable StallDetector. This is because WebOS
// TVs tend to have long hardware pipelines that respond slowly, so we tend
// to mistake slow operation for a stall.
if (shaka.util.Platform.isWebOS()) { return false; }

// If we are not trying to play, the lack of change could be misidentified
// as a stall.
if (this.mediaElement_.paused) { return false; }
Expand Down
10 changes: 10 additions & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,18 @@ shaka.util.PlayerConfiguration = class {
// Offset by 5 seconds since Chromecast takes a few seconds to start
// playing after a seek, even when buffered.
safeSeekOffset: 5,
stallEnabled: true,
stallThreshold: 1 /* seconds */,
stallSkip: 0.1 /* seconds */,
};

// WebOS has a long hardware pipeline that responds slowly, making it easy
// to misidentify stalls. To avoid this, by default disable stall detection
// on WebOS.
if (shaka.util.Platform.isWebOS()) {
streaming.stallEnabled = false;
}

const offline = {
// We need to set this to a throw-away implementation for now as our
// default implementation will need to reference other fields in the
Expand Down

0 comments on commit fd2d61d

Please sign in to comment.