Skip to content

Commit

Permalink
feat: add listenable events for playback stall detection and gap jump…
Browse files Browse the repository at this point in the history
…ing (#4249)

An event `stalldetected` can be dispatched when Shaka Player detects a stall based on the value of stallThreshold through [StreamingConfiguration](https://shaka-player-demo.appspot.com/docs/api/externs_shaka_player.js.html#line920).

A second event `gapjumped` could also be dispatched when Shaka performs a jump in a media gap.

Related to issue #4227
  • Loading branch information
JulianDomingo committed Jun 2, 2022
1 parent 9171f73 commit 5987458
Show file tree
Hide file tree
Showing 18 changed files with 427 additions and 116 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS
Expand Up @@ -66,6 +66,7 @@ John Bowers <john.bowers@verizondigitalmedia.com>
Jonas Birmé <jonas.birme@eyevinn.se>
Jono Ward <jonoward@gmail.com>
Jozef Chúťka <jozefchutka@gmail.com>
Julian Domingo <juliandomingo@google.com>
Jun Hong Chong <chongjunhong@gmail.com>
Leandro Ribeiro Moreira <leandro.ribeiro.moreira@gmail.com>
Lucas Gabriel Sánchez <unkiwii@gmail.com>
Expand Down
8 changes: 8 additions & 0 deletions externs/shaka/player.js
Expand Up @@ -82,6 +82,9 @@ shaka.extern.StateChange;
*
* maxSegmentDuration: number,
*
* gapsJumped: number,
* stallsDetected: number,
*
* switchHistory: !Array.<shaka.extern.TrackChoice>,
* stateHistory: !Array.<shaka.extern.StateChange>
* }}
Expand Down Expand Up @@ -112,6 +115,11 @@ shaka.extern.StateChange;
* @property {number} estimatedBandwidth
* The current estimated network bandwidth (in bit/sec).
*
* @property {number} gapsJumped
* The total number of playback gaps jumped by the GapJumpingController.
* @property {number} stallsDetected
* The total number of playback stalls detected by the StallDetector.
*
* @property {number} completionPercent
* This is the greatest completion percent that the user has experienced in
* playback. Also known as the "high water mark". Is NaN when there is no
Expand Down
6 changes: 3 additions & 3 deletions lib/cast/cast_proxy.js
Expand Up @@ -278,8 +278,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
(event) => this.videoProxyLocalEvent_(event));
}

for (const key in shaka.Player.EventName) {
const name = shaka.Player.EventName[key];
for (const key in shaka.util.FakeEvent.EventName) {
const name = shaka.util.FakeEvent.EventName[key];
this.eventManager_.listen(this.localPlayer_, name,
(event) => this.playerProxyLocalEvent_(event));
}
Expand Down Expand Up @@ -537,7 +537,7 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
// Pass any errors through to the app.
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
const eventType = shaka.Player.EventName.Error;
const eventType = shaka.util.FakeEvent.EventName.Error;
const data = (new Map()).set('detail', error);
const event = new shaka.util.FakeEvent(eventType, data);
this.localPlayer_.dispatchEvent(event);
Expand Down
6 changes: 3 additions & 3 deletions lib/cast/cast_receiver.js
Expand Up @@ -281,8 +281,8 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
this.video_, name, (event) => this.proxyEvent_('video', event));
}

for (const key in shaka.Player.EventName) {
const name = shaka.Player.EventName[key];
for (const key in shaka.util.FakeEvent.EventName) {
const name = shaka.util.FakeEvent.EventName[key];
this.eventManager_.listen(
this.player_, name, (event) => this.proxyEvent_('player', event));
}
Expand Down Expand Up @@ -409,7 +409,7 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
// Pass any errors through to the app.
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
const eventType = shaka.Player.EventName.Error;
const eventType = shaka.util.FakeEvent.EventName.Error;
const data = (new Map()).set('detail', error);
const event = new shaka.util.FakeEvent(eventType, data);
// Only dispatch the event if the player still exists.
Expand Down
24 changes: 23 additions & 1 deletion lib/media/gap_jumping_controller.js
Expand Up @@ -11,6 +11,7 @@ goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.StallDetector');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.Timer');

Expand All @@ -32,8 +33,13 @@ shaka.media.GapJumpingController = class {
* 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(video, timeline, config, stallDetector) {
constructor(video, timeline, config, stallDetector, onEvent) {
/** @private {?function(!Event)} */
this.onEvent_ = onEvent;

/** @private {HTMLMediaElement} */
this.video_ = video;

Expand All @@ -52,6 +58,9 @@ shaka.media.GapJumpingController = class {
/** @private {number} */
this.prevReadyState_ = video.readyState;

/** @private {number} */
this.gapsJumped_ = 0;

/**
* The stall detector tries to keep the playhead moving forward. It is
* managed by the gap-jumping controller to avoid conflicts. On some
Expand Down Expand Up @@ -98,6 +107,7 @@ shaka.media.GapJumpingController = class {
this.stallDetector_ = null;
}

this.onEvent_ = null;
this.timeline_ = null;
this.video_ = null;
}
Expand All @@ -121,6 +131,15 @@ shaka.media.GapJumpingController = class {
}


/**
* Returns the total number of playback gaps jumped.
* @return {number}
*/
getGapsJumped() {
return this.gapsJumped_;
}


/**
* Called on a recurring timer to check for gaps in the media. This is also
* called in a 'waiting' event.
Expand Down Expand Up @@ -209,6 +228,9 @@ shaka.media.GapJumpingController = class {
}

this.video_.currentTime = jumpTo;
this.gapsJumped_++;
this.onEvent_(
new shaka.util.FakeEvent(shaka.util.FakeEvent.EventName.GapJumped));
}
};

Expand Down
51 changes: 47 additions & 4 deletions lib/media/playhead.js
Expand Up @@ -41,6 +41,20 @@ shaka.media.Playhead = class {
*/
setStartTime(startTime) {}

/**
* Get the number of playback stalls detected by the StallDetector.
*
* @return {number}
*/
getStallsDetected() {}

/**
* Get the number of playback gaps jumped by the GapJumpingController.
*
* @return {number}
*/
getGapsJumped() {}

/**
* Get the current playhead position. The position will be restricted to valid
* time ranges.
Expand Down Expand Up @@ -133,6 +147,16 @@ shaka.media.SrcEqualsPlayhead = class {
return time || 0;
}

/** @override */
getStallsDetected() {
return 0;
}

/** @override */
getGapsJumped() {
return 0;
}

/** @override */
notifyOfBufferingChange() {}
};
Expand Down Expand Up @@ -160,8 +184,10 @@ shaka.media.MediaSourcePlayhead = class {
* @param {function()} onSeek
* Called when the user agent seeks to a time within the presentation
* timeline.
* @param {function(!Event)} onEvent
* Called when an event is raised to be sent to the application.
*/
constructor(mediaElement, manifest, config, startTime, onSeek) {
constructor(mediaElement, manifest, config, startTime, onSeek, onEvent) {
/**
* The seek range must be at least this number of seconds long. If it is
* smaller than this, change it to be this big so we don't repeatedly seek
Expand Down Expand Up @@ -192,12 +218,17 @@ shaka.media.MediaSourcePlayhead = class {
/** @private {?number} */
this.lastCorrectiveSeek_ = null;

/** @private {shaka.media.StallDetector} */
this.stallDetector_ =
this.createStallDetector_(mediaElement, config, onEvent);

/** @private {shaka.media.GapJumpingController} */
this.gapController_ = new shaka.media.GapJumpingController(
mediaElement,
manifest.presentationTimeline,
config,
this.createStallDetector_(mediaElement, config));
this.stallDetector_,
onEvent);

/** @private {shaka.media.VideoWrapper} */
this.videoWrapper_ = new shaka.media.VideoWrapper(
Expand Down Expand Up @@ -261,6 +292,16 @@ shaka.media.MediaSourcePlayhead = class {
return time;
}

/** @override */
getStallsDetected() {
return this.stallDetector_.getStallsDetected();
}

/** @override */
getGapsJumped() {
return this.gapController_.getGapsJumped();
}

/**
* Gets the playhead's initial position in seconds.
*
Expand Down Expand Up @@ -475,10 +516,12 @@ shaka.media.MediaSourcePlayhead = class {
*
* @param {!HTMLMediaElement} mediaElement
* @param {shaka.extern.StreamingConfiguration} config
* @param {function(!Event)} onEvent
* Called when an event is raised to be sent to the application.
* @return {shaka.media.StallDetector}
* @private
*/
createStallDetector_(mediaElement, config) {
createStallDetector_(mediaElement, config, onEvent) {
if (!config.stallEnabled) {
return null;
}
Expand All @@ -492,7 +535,7 @@ shaka.media.MediaSourcePlayhead = class {
// playhead forward.
const detector = new shaka.media.StallDetector(
new shaka.media.StallDetector.MediaElementImplementation(mediaElement),
threshold);
threshold, onEvent);

detector.onStall((at, duration) => {
shaka.log.debug(`Stall detected at ${at} for ${duration} seconds.`);
Expand Down
21 changes: 19 additions & 2 deletions lib/media/stall_detector.js
Expand Up @@ -9,6 +9,7 @@ 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');

/**
Expand All @@ -23,11 +24,14 @@ 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) {
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} */
Expand All @@ -36,6 +40,8 @@ shaka.media.StallDetector = class {
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
Expand All @@ -53,6 +59,7 @@ shaka.media.StallDetector = class {
release() {
// Drop external references to make things easier on the GC.
this.implementation_ = null;
this.onEvent_ = null;
this.onStall_ = () => {};
}

Expand All @@ -66,6 +73,13 @@ shaka.media.StallDetector = class {
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.
Expand Down Expand Up @@ -100,6 +114,9 @@ shaka.media.StallDetector = class {
// 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;
Expand Down
2 changes: 1 addition & 1 deletion lib/media/streaming_engine.js
Expand Up @@ -1701,7 +1701,7 @@ shaka.media.StreamingEngine = class {
};

// Dispatch an event to notify the application about the emsg box.
const eventName = shaka.Player.EventName.Emsg;
const eventName = shaka.util.FakeEvent.EventName.Emsg;
const data = (new Map()).set('detail', emsg);
const event = new shaka.util.FakeEvent(eventName, data);
this.playerInterface_.onEvent(event);
Expand Down

0 comments on commit 5987458

Please sign in to comment.