Skip to content

Commit

Permalink
Make Buffering Observer A Top-Level Component
Browse files Browse the repository at this point in the history
Before the buffering observer was a playhead observer, but with
supporting src=, changing how we interact with the buffering observer
was made easier if it was handled as a top-level component.

This meant moving it off the playhead observer interface and create its
own timer in player.

Coming off the playehead observer interface, the buffering observer did
not need to use callbacks, which made it easier to use. This will also
allow us to use it as the single source of "is buffering" in a later
change.

Change-Id: I8cad9bfde3309de7c2b8301b90aa8c40b6e4d247
  • Loading branch information
vaage committed Mar 29, 2019
1 parent 024f9de commit 1284dd4
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 384 deletions.
221 changes: 49 additions & 172 deletions lib/media/buffering_observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,195 +17,81 @@

goog.provide('shaka.media.BufferingObserver');

goog.require('shaka.media.IPlayheadObserver');


/**
* The buffering observer watches how much content the video element has
* buffered and raises events when the state changes (enough => not enough or
* vice versa).
*
* The one listening to the events should take action to avoid running out of
* content.
* The buffering observer watches how much content has been buffered and raises
* events when the state changes (enough => not enough or vice versa).
*
* @implements {shaka.media.IPlayheadObserver}
* @final
*/
shaka.media.BufferingObserver = class {
/**
* @param {number} thresholdWhenStarving
* The threshold for how many seconds worth of content must be buffered
* ahead of the playhead position to leave a STARVING state.
* @param {shaka.media.BufferingObserver.State} initialState
* The state that the observer starts in. We allow this so that it is
* easier to test, rather than having to "force" the observer into a
* particular state through simulation in the test.
* @param {function(number):number} getSecondsBufferedAfter
* Get the number of seconds after the given time (in seconds) that have
* buffered.
* @param {function():boolean} isBufferedToEnd
* When we call |poll|, we need to know if we are buffered to the end of
* the presentation. This method should return |true| when we have
* buffered to the end of the current presentation. In terms of live
* content, this will return |true| when we are buffered to the live edge.
* @param {number} thresholdWhenSatisfied
*/
constructor(thresholdWhenStarving,
initialState,
getSecondsBufferedAfter,
isBufferedToEnd) {
/**
* The state (SATISFIED vs STARVING) at last check. This value will always
* be "old", and we will compare it to what we evaluate in the "present" to
* see when the state has changed.
*
* @private {shaka.media.BufferingObserver.State}
*/
this.previousState_ = initialState;
constructor(thresholdWhenStarving, thresholdWhenSatisfied) {
const State = shaka.media.BufferingObserver.State;

/**
* The minimum amount of content that must be buffered ahead of the playhead
* to avoid a transition from SATISFIED to STARVING, i.e. to remain in
* SATISFIED. This will be used when we the previous state is SATISFIED.
*
* Combined with |thresholdWhenStarving_|, this adds hysteresis to the
* state machine to avoid frequent switches around a single threshold.
* https://bit.ly/2QLQNtG
*
* @private {number}
*/
this.thresholdWhenSatisfied_ = 0.5;
/** @private {shaka.media.BufferingObserver.State} */
this.previousState_ = State.SATISFIED;

/**
* The minimum amount of content that must be buffered ahead of the playhead
* to transition from STARVING to SATISFIED. This will be used when the
* previous state is STARVING.
*
* Combined with |thresholdWhenSatisfied_|, this adds hysteresis to the
* state machine to avoid frequent switches around a single threshold.
* https://bit.ly/2QLQNtG
*
* @private {number}
*/
this.thresholdWhenStarving_ = thresholdWhenStarving;
/** @private {!Map.<shaka.media.BufferingObserver.State, number>} */
this.thresholds_ = new Map()
.set(State.SATISFIED, thresholdWhenSatisfied)
.set(State.STARVING, thresholdWhenStarving);
}

/**
* When we call |poll|, we need to know if we are buffered to the end of
* the presentation. This method should return |true| when we have
* buffered to the end of the current presentation. In terms of live
* content, this will return |true| when we are buffered to the live edge.
*
* Checking if we are buffered to the end of the presentation relies on a
* number of factors. Which factors can even depend on what it loaded. To
* avoid having all those factors here, we use an external callback so that
* this implementation can be move flexible and easier to test.
*
* @private {function():boolean}
*/
this.isBufferedToEnd_ = isBufferedToEnd;
/**
* Update the observer by telling it how much content has been buffered (in
* seconds) and if we are buffered to the end of the presentation. If the
* controller believes the state has changed, it will return |true|.
*
* @param {number} bufferLead
* @param {boolean} bufferedToEnd
* @return {boolean}
*/
update(bufferLead, bufferedToEnd) {
const State = shaka.media.BufferingObserver.State;

/**
* A callback to get the number of seconds of buffered content that comes
* after the given presentation time (in seconds).
* Our threshold for how much we need before we declare ourselves as
* starving is based on whether or not we were just starving. If we
* were just starving, we are more likely to starve again, so we require
* more content to be buffered than if we were not just starving.
*
* @private {function(number):number}
* @type {number}
*/
this.getSecondsBufferedAfter_ = getSecondsBufferedAfter;

/** @private {function()} */
this.onStarving_ = () => {};
const threshold = this.thresholds_.get(this.previousState_);

/** @private {function()} */
this.onSatisfied_ = () => {};

/**
* A series of rules that we will use to determine what callback to use
* when the playhead moves.
*
* @private {!Array.<shaka.media.BufferingObserver.Rule_>}
*/
this.rules_ = [
{
was: shaka.media.BufferingObserver.State.STARVING,
is: shaka.media.BufferingObserver.State.SATISFIED,
doThis: () => this.onSatisfied_(),
},
{
was: shaka.media.BufferingObserver.State.SATISFIED,
is: shaka.media.BufferingObserver.State.STARVING,
doThis: () => this.onStarving_(),
},
];
const oldState = this.previousState_;
const newState = (bufferedToEnd || bufferLead >= threshold) ?
(State.SATISFIED) :
(State.STARVING);

// If the thresholds are inverted, it could be possible that we miss a
// transition from SATISFIED to STARVING in some cases. This could have
// serious consequences for playback, preventing us from entering a
// buffering state, and causing interruptions in playback with no cause
// obvious to the user.
if (this.thresholdWhenSatisfied_ >= this.thresholdWhenStarving_) {
// If this happens, warn the user and reduce |thresholdWhenSatisfied_| to
// restore the correct mathematical relationship between the two. The
// behavior may still be poor, since the difference between the two
// thresholds will be small and the hysteresis will be less effective.
shaka.log.alwaysWarn(
'Rebuffering threshold is set too low! This could cause poor ' +
'buffering behavior during playback!');
this.thresholdWhenSatisfied_ = this.thresholdWhenStarving_ / 2;
}
}
// Save the new state now so that calls to |getState| from any callbacks
// will be accurate.
this.previousState_ = newState;

/** @override */
release() {
// Clear the callbacks so that we don't hold references to parts of the
// listeners.
this.onStarving_ = () => {};
this.onSatisfied_ = () => {};
// Return |true| only when the state has changed.
return oldState != newState;
}

/** @override */
poll(positionInSeconds, wasSeeking) {
const State = shaka.media.BufferingObserver.State;
// Our threshold for how much we need before we declare ourselves as
// starving is based on whether or not we were just starving. If we
// were just starving, we are more likely to starve again, so we require
// more content to be buffered than if we were not just starving.
const threshold = this.previousState_ == State.SATISFIED ?
this.thresholdWhenSatisfied_ :
this.thresholdWhenStarving_;

// Check how far ahead of |currentTime| we have buffered. The most we have,
// the better off we are.
const amountBuffered = this.getSecondsBufferedAfter_(positionInSeconds);

/** @type {boolean} */
const isBufferedToEnd = this.isBufferedToEnd_();

const currentState = (isBufferedToEnd || amountBuffered >= threshold) ?
(State.SATISFIED) :
(State.STARVING);

// Execute all the rules that apply to the current state.
for (const rule of this.rules_) {
if (this.previousState_ == rule.was && currentState == rule.is) {
rule.doThis();
}
}

// Store the current state so that we can detect a change in state next
// time |applyNewPlayheadPosition| is called.
this.previousState_ = currentState;
/**
* Set which state that the observer should think playback was in.
*
* @param {shaka.media.BufferingObserver.State} state
*/
setState(state) {
this.previousState_ = state;
}

/**
* Set the listeners. This will override any previous calls to |setListeners|.
* Get the state that the observer last thought playback was in.
*
* @param {function()} onStarving
* The callback for when we change from "satisfied" to "starving".
* @param {function()} onSatisfied
* The callback for when we change from "starving" to "satisfied".
* @return {shaka.media.BufferingObserver.State}
*/
setListeners(onStarving, onSatisfied) {
this.onStarving_ = onStarving;
this.onSatisfied_ = onSatisfied;
getState() {
return this.previousState_;
}
};

Expand All @@ -219,12 +105,3 @@ shaka.media.BufferingObserver.State = {
STARVING: 0,
SATISFIED: 1,
};

/**
* @typedef {{
* was: shaka.media.BufferingObserver.State,
* is: shaka.media.BufferingObserver.State,
* doThis: function()
* }}
*/
shaka.media.BufferingObserver.Rule_;
Loading

0 comments on commit 1284dd4

Please sign in to comment.