mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-26 17:46:26 +03:00
Make Buffering Observer A Top-Level Component
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
This commit is contained in:
+58
-181
@@ -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 buffering observer watches how much content has been 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.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A callback to get the number of seconds of buffered content that comes
|
||||
* after the given presentation time (in seconds).
|
||||
*
|
||||
* @private {function(number):number}
|
||||
*/
|
||||
this.getSecondsBufferedAfter_ = getSecondsBufferedAfter;
|
||||
|
||||
/** @private {function()} */
|
||||
this.onStarving_ = () => {};
|
||||
|
||||
/** @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_(),
|
||||
},
|
||||
];
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
release() {
|
||||
// Clear the callbacks so that we don't hold references to parts of the
|
||||
// listeners.
|
||||
this.onStarving_ = () => {};
|
||||
this.onSatisfied_ = () => {};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
poll(positionInSeconds, wasSeeking) {
|
||||
constructor(thresholdWhenStarving, thresholdWhenSatisfied) {
|
||||
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);
|
||||
/** @private {shaka.media.BufferingObserver.State} */
|
||||
this.previousState_ = State.SATISFIED;
|
||||
|
||||
/** @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;
|
||||
/** @private {!Map.<shaka.media.BufferingObserver.State, number>} */
|
||||
this.thresholds_ = new Map()
|
||||
.set(State.SATISFIED, thresholdWhenSatisfied)
|
||||
.set(State.STARVING, thresholdWhenStarving);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listeners. This will override any previous calls to |setListeners|.
|
||||
* 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 {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".
|
||||
* @param {number} bufferLead
|
||||
* @param {boolean} bufferedToEnd
|
||||
* @return {boolean}
|
||||
*/
|
||||
setListeners(onStarving, onSatisfied) {
|
||||
this.onStarving_ = onStarving;
|
||||
this.onSatisfied_ = onSatisfied;
|
||||
update(bufferLead, bufferedToEnd) {
|
||||
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.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
const threshold = this.thresholds_.get(this.previousState_);
|
||||
|
||||
const oldState = this.previousState_;
|
||||
const newState = (bufferedToEnd || bufferLead >= threshold) ?
|
||||
(State.SATISFIED) :
|
||||
(State.STARVING);
|
||||
|
||||
// Save the new state now so that calls to |getState| from any callbacks
|
||||
// will be accurate.
|
||||
this.previousState_ = newState;
|
||||
|
||||
// Return |true| only when the state has changed.
|
||||
return oldState != newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set which state that the observer should think playback was in.
|
||||
*
|
||||
* @param {shaka.media.BufferingObserver.State} state
|
||||
*/
|
||||
setState(state) {
|
||||
this.previousState_ = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state that the observer last thought playback was in.
|
||||
*
|
||||
* @return {shaka.media.BufferingObserver.State}
|
||||
*/
|
||||
getState() {
|
||||
return this.previousState_;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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_;
|
||||
|
||||
+89
-27
@@ -112,6 +112,14 @@ shaka.Player = function(mediaElement, dependencyInjector) {
|
||||
*/
|
||||
this.playheadObservers_ = null;
|
||||
|
||||
// We use the buffering observer and timer to track when we move from having
|
||||
// enough buffered content to not enough. They only exist when content has
|
||||
// been loaded and are not re-used between loads.
|
||||
/** @private {shaka.util.Timer} */
|
||||
this.bufferPoller_ = null;
|
||||
/** @private {shaka.media.BufferingObserver} */
|
||||
this.bufferObserver_ = null;
|
||||
|
||||
/** @private {shaka.media.RegionTimeline} */
|
||||
this.regionTimeline_ = null;
|
||||
|
||||
@@ -1070,6 +1078,11 @@ shaka.Player.prototype.onUnload_ = async function(has, wants) {
|
||||
this.playheadObservers_ = null;
|
||||
}
|
||||
|
||||
if (this.bufferPoller_) {
|
||||
this.bufferPoller_.stop();
|
||||
this.bufferPoller_ = null;
|
||||
}
|
||||
|
||||
if (this.playhead_) {
|
||||
this.playhead_.release();
|
||||
this.playhead_ = null;
|
||||
@@ -1109,12 +1122,13 @@ shaka.Player.prototype.onUnload_ = async function(has, wants) {
|
||||
this.drmEngine_ = null;
|
||||
}
|
||||
|
||||
this.switchingPeriods_ = true;
|
||||
this.manifest_ = null;
|
||||
this.assetUri_ = null;
|
||||
this.activeStreams_.clear();
|
||||
this.assetUri_ = null;
|
||||
this.bufferObserver_ = null;
|
||||
this.loadingTextStreams_.clear();
|
||||
this.manifest_ = null;
|
||||
this.stats_ = null;
|
||||
this.switchingPeriods_ = true;
|
||||
|
||||
// Force an exit from the buffering state.
|
||||
this.onBuffering_(false);
|
||||
@@ -1477,6 +1491,14 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) {
|
||||
this.playhead_ = this.createPlayhead(has.startTime);
|
||||
this.playheadObservers_ = this.createPlayheadObservers_();
|
||||
|
||||
|
||||
// Initializing the buffering controller needs to be near the end because it
|
||||
// will set the initial buffering state and that depends on other components
|
||||
// being initialized.
|
||||
const rebufferThreshold = Math.max(
|
||||
this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal);
|
||||
this.startBufferManagement_(rebufferThreshold);
|
||||
|
||||
this.streamingEngine_ = this.createStreamingEngine();
|
||||
this.streamingEngine_.configure(this.config_.streaming);
|
||||
|
||||
@@ -1740,39 +1762,79 @@ shaka.Player.prototype.createPlayheadObservers_ = function() {
|
||||
};
|
||||
regionObserver.setListeners(onEnterRegion, onExitRegion, onSkipRegion);
|
||||
|
||||
// Create the buffering observer. This will allow us to notify the player when
|
||||
// we are falling behind and something needs to be done before we run out of
|
||||
// buffering.
|
||||
|
||||
// This is how much we need to buffer after we enter a starving state before
|
||||
// we can become satisfied again.
|
||||
const rebufferingThreshold = Math.max(
|
||||
this.manifest_.minBufferTime,
|
||||
this.config_.streaming.rebufferingGoal);
|
||||
const bufferingObserver = new shaka.media.BufferingObserver(
|
||||
/* starvingThreshold= */ rebufferingThreshold,
|
||||
/* startAs= */ shaka.media.BufferingObserver.State.STARVING,
|
||||
/* getSecondsBufferedAfter= */ (timeInSeconds) => {
|
||||
return shaka.media.TimeRangesUtils.bufferedAheadOf(
|
||||
this.video_.buffered, timeInSeconds);
|
||||
},
|
||||
/* isBufferedToEnd= */ () => {
|
||||
return this.isBufferedToEnd_();
|
||||
});
|
||||
const onBufferStarving = () => this.onBuffering_(/* isBuffering= */ true);
|
||||
const onBufferSatisfied = () => this.onBuffering_(/* isBuffering= */ false);
|
||||
bufferingObserver.setListeners(onBufferStarving, onBufferSatisfied);
|
||||
|
||||
// Now that we have all our observers, create a manager for them.
|
||||
const manager = new shaka.media.PlayheadObserverManager(this.video_);
|
||||
manager.manage(periodObserver);
|
||||
manager.manage(regionObserver);
|
||||
manager.manage(bufferingObserver);
|
||||
|
||||
return manager;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initialize and start the buffering system (observer and timer) so that we can
|
||||
* monitor our buffer lead during playback.
|
||||
*
|
||||
* @param {number} rebufferingGoal
|
||||
*/
|
||||
shaka.Player.prototype.startBufferManagement_ = function(rebufferingGoal) {
|
||||
goog.asserts.assert(
|
||||
!this.bufferObserver_,
|
||||
'No buffering observer should exist before initialization.');
|
||||
|
||||
goog.asserts.assert(
|
||||
!this.bufferPoller_,
|
||||
'No buffer timer should exist before initialization.');
|
||||
|
||||
this.bufferObserver_ = new shaka.media.BufferingObserver(
|
||||
/* starvingThreshold= */ rebufferingGoal,
|
||||
/* satisfiedThreshold= */ rebufferingGoal / 2);
|
||||
|
||||
// Force us back to a buffering state. This ensure everything is starting in
|
||||
// the same state.
|
||||
this.bufferObserver_.setState(shaka.media.BufferingObserver.State.STARVING);
|
||||
this.onBuffering_(true);
|
||||
|
||||
// TODO: We should take some time to look into the effects of our
|
||||
// quarter-second refresh practice. We often use a quarter-second
|
||||
// but we have no documentation about why.
|
||||
this.bufferPoller_ = new shaka.util.Timer(() => {
|
||||
this.pollBufferState_();
|
||||
}).tickEvery(/* seconds= */ 0.25);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* This method is called periodically to check what the buffering observer says
|
||||
* so that we can update the rest of the buffering behaviours.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
shaka.Player.prototype.pollBufferState_ = function() {
|
||||
goog.asserts.assert(
|
||||
this.video_,
|
||||
'Need a media element to update the buffering observer');
|
||||
|
||||
goog.asserts.assert(
|
||||
this.bufferObserver_,
|
||||
'Need a buffering observer to update');
|
||||
|
||||
const toEnd = this.isBufferedToEnd_();
|
||||
const lead = shaka.media.TimeRangesUtils.bufferedAheadOf(
|
||||
this.video_.buffered,
|
||||
this.video_.currentTime);
|
||||
|
||||
const stateChanged = this.bufferObserver_.update(lead, toEnd);
|
||||
|
||||
// If the state changed, we need to surface the event.
|
||||
if (stateChanged) {
|
||||
const state = this.bufferObserver_.getState();
|
||||
const buffering = state == shaka.media.BufferingObserver.State.STARVING;
|
||||
this.onBuffering_(buffering);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new media source engine. This will ONLY be replaced by tests as a
|
||||
* way to inject fake media source engine instances.
|
||||
|
||||
@@ -17,267 +17,138 @@
|
||||
|
||||
describe('BufferingObserver', () => {
|
||||
const BufferingObserver = shaka.media.BufferingObserver;
|
||||
const State = shaka.media.BufferingObserver.State;
|
||||
|
||||
const thresholdAfterStarving = 5;
|
||||
const thresholdAfterSatisfied = 2;
|
||||
|
||||
/** @type {!shaka.media.BufferingObserver} */
|
||||
let observer;
|
||||
|
||||
/** @type {!jasmine.Spy} */
|
||||
let onStarving;
|
||||
/** @type {!jasmine.Spy} */
|
||||
let onSatisfied;
|
||||
|
||||
/** @type {!jasmine.Spy} */
|
||||
let isBufferedToEndSpy;
|
||||
/** @type {!jasmine.Spy} */
|
||||
let getBufferedAfterSpy;
|
||||
let controller;
|
||||
|
||||
beforeEach(() => {
|
||||
onStarving = jasmine.createSpy('onStarving');
|
||||
onSatisfied = jasmine.createSpy('onSatisfied');
|
||||
|
||||
isBufferedToEndSpy = jasmine.createSpy('isBufferedToEnd');
|
||||
getBufferedAfterSpy = jasmine.createSpy('getBufferedSecondsAfter');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
observer.release();
|
||||
controller = new BufferingObserver(
|
||||
thresholdAfterStarving,
|
||||
thresholdAfterSatisfied);
|
||||
});
|
||||
|
||||
describe('when satisfied', () => {
|
||||
const Util = shaka.test.Util;
|
||||
|
||||
beforeEach(() => {
|
||||
observer = new BufferingObserver(
|
||||
/* thresholdAfterStarving= */ 5,
|
||||
/* initialState= */ BufferingObserver.State.SATISFIED,
|
||||
/* getSecondsBufferedAfter= */ Util.spyFunc(getBufferedAfterSpy),
|
||||
/* isBufferedToEnd= */ Util.spyFunc(isBufferedToEndSpy));
|
||||
observer.setListeners(
|
||||
/* onStarving= */ Util.spyFunc(onStarving),
|
||||
/* onSatisfied= */ Util.spyFunc(onSatisfied));
|
||||
controller.setState(State.SATISFIED);
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
});
|
||||
|
||||
it('is starving when approaching end of buffered region', () => {
|
||||
// Move the playhead to be just before the end of the buffered region.
|
||||
// This move us from "satisfied" to "starving", firing an event.
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
|
||||
poll(observer,
|
||||
/* currentTime= */ 59.9,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 60,
|
||||
/* bufferedToEnd= */ false);
|
||||
expect(onStarving).toHaveBeenCalledTimes(1);
|
||||
const changed = controller.update(/* lead= */ 0.1, /* toEnd= */ false);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
});
|
||||
|
||||
it('is starving when jumping to unbuffered region', () => {
|
||||
// Move the playhead to a state where it will be seen as starving. This
|
||||
// will cause us to change from "satisfied" to "starving", firing the
|
||||
// event.
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
poll(observer,
|
||||
/* currentTime= */ 100,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 60,
|
||||
/* bufferedToEnd= */ false);
|
||||
expect(onStarving).toHaveBeenCalledTimes(1);
|
||||
const changed = controller.update(/* lead= */ 0, /* toEnd= */ false);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
});
|
||||
|
||||
// Just because we say once that we have buffered to the end, it does not
|
||||
// mean that the "end" will not change. For example, with live content, we
|
||||
// say the live edge is the "end". The live edge can and will move.
|
||||
it('is starving if buffered to end changes back to false', () => {
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
/** @type {boolean} */
|
||||
let changed;
|
||||
|
||||
// Move us from satisfied to starving by positioning us at the end of the
|
||||
// buffered range but say we have not buffered to the end.
|
||||
poll(observer,
|
||||
/* currentTime= */ 30,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 30,
|
||||
/* bufferedToEnd= */ false);
|
||||
|
||||
expect(onStarving).toHaveBeenCalledTimes(1);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
changed = controller.update(/* lead= */ 0, /* toEnd= */ false);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
|
||||
// Move us from starving to satisfied by keeping us at the end of the
|
||||
// buffered range but now say that we have buffered to the end of the end.
|
||||
poll(observer,
|
||||
/* currentTime= */ 30,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 30,
|
||||
/* bufferedToEnd= */ true);
|
||||
|
||||
expect(onStarving).toHaveBeenCalledTimes(1);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(1);
|
||||
changed = controller.update(/* lead= */ 0, /* toEnd= */ true);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
|
||||
// Move us from satisfied to starving again by keeping us at the end of
|
||||
// the buffered range but "move" the end so that we are no longer buffered
|
||||
// to the end.
|
||||
poll(observer,
|
||||
/* currentTime= */ 30,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 30,
|
||||
/* bufferedToEnd= */ false);
|
||||
|
||||
expect(onStarving).toHaveBeenCalledTimes(2);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(1);
|
||||
changed = controller.update(/* lead= */ 0, /* toEnd= */ false);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
});
|
||||
|
||||
// As the playhead approaches the end of the buffered range, we should
|
||||
// remain satisfied since we will be buffered to the end of the
|
||||
// presentation.
|
||||
it('remains satisfied when content is buffered to the end', () => {
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
|
||||
// "Play" through the presentation with a very small step; this will
|
||||
// allow us to move into the starvation gap, but because we are buffered
|
||||
// to the end, we should never enter the starving state.
|
||||
for (let time = 0; time <= 30; time += 0.1) {
|
||||
poll(observer,
|
||||
time,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 30,
|
||||
/* bufferedToEnd= */ true);
|
||||
const bufferLead = 30 - time;
|
||||
const changed = controller.update(bufferLead, /* toEnd= */ true);
|
||||
expect(changed).toBeFalsy();
|
||||
}
|
||||
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
});
|
||||
|
||||
// Make sure that we stay satisfied while enough content is buffered. We
|
||||
// should not see |onStarving| while moving around the buffered range.
|
||||
it('remains satisfied when enough content is buffered', () => {
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
|
||||
// "Play" through the presentation with a very small step; this will
|
||||
// allow us to move into the starvation gap, but because we are buffered
|
||||
// to the end, we should never enter the starving state.
|
||||
for (let time = 10; time <= 20; time += 1) {
|
||||
poll(observer,
|
||||
time,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 30,
|
||||
/* bufferedToEnd= */ false);
|
||||
const bufferLead = 30 - time;
|
||||
const changed = controller.update(bufferLead, /* toEnd= */ false);
|
||||
expect(changed).toBeFalsy();
|
||||
}
|
||||
|
||||
expect(onStarving).toHaveBeenCalledTimes(0);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when starving', () => {
|
||||
beforeEach(() => {
|
||||
const Util = shaka.test.Util;
|
||||
|
||||
observer = new BufferingObserver(
|
||||
/* thresholdAfterStarving= */ 5,
|
||||
/* initialState= */ BufferingObserver.State.STARVING,
|
||||
/* getSecondsBufferedAfter= */ Util.spyFunc(getBufferedAfterSpy),
|
||||
/* isBufferedToEnd= */ Util.spyFunc(isBufferedToEndSpy));
|
||||
observer.setListeners(
|
||||
/* onStarving= */ Util.spyFunc(onStarving),
|
||||
/* onSatisfied= */ Util.spyFunc(onSatisfied));
|
||||
controller.setState(State.STARVING);
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
});
|
||||
|
||||
it('becomes satisfied when enough content is buffered', () => {
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
|
||||
// We will be at time=30 and buffered up to time=30. We will need to
|
||||
// buffer up to time=35 before we move to satisfied.
|
||||
const currentTime = 30;
|
||||
|
||||
// Since we are starving, we need to have 5 seconds of content buffered
|
||||
// before we will moved to satisfied. Move bit-by-bit as we approach 5
|
||||
// seconds. Until we reach 5 seconds worth of buffered lead, we should
|
||||
// not see a call to |onSatisfied|.
|
||||
for (let delta = 0; delta <= 4; delta++) {
|
||||
poll(observer,
|
||||
currentTime,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ currentTime + delta,
|
||||
/* bufferedToEnd= */ false);
|
||||
// seconds. Until we reach 5 seconds worth of buffered lead, we should not
|
||||
// see a change to satisfied.
|
||||
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
/** @type {boolean} */
|
||||
let changed;
|
||||
|
||||
for (let lead = 0; lead <= 4; lead++) {
|
||||
changed = controller.update(lead, /* toEnd= */ false);
|
||||
expect(changed).toBeFalsy();
|
||||
}
|
||||
|
||||
// Finally we will have 5 seconds worth of buffered content, we should
|
||||
// see a call to |onSatisfied|.
|
||||
poll(observer,
|
||||
currentTime,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ currentTime + 5,
|
||||
/* bufferedToEnd= */ false);
|
||||
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(1);
|
||||
changed = controller.update(/* lead= */ 5, /* toEnd= */ false);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
});
|
||||
|
||||
it('becomes satisfied when the end is buffered', () => {
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
/** @type {boolean} */
|
||||
let changed;
|
||||
|
||||
// We will be at time=0 and buffered up to time=3, which is less than
|
||||
// the 5 second threshold to be satisfied.
|
||||
poll(observer,
|
||||
/* currentTime= */ 0,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 3,
|
||||
/* bufferedToEnd= */ false);
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(0);
|
||||
changed = controller.update(/* lead= */ 3, /* toEnd= */ false);
|
||||
expect(changed).toBeFalsy();
|
||||
expect(controller.getState()).toBe(State.STARVING);
|
||||
|
||||
// If this is the end, though, we should be satisfied.
|
||||
poll(observer,
|
||||
/* currentTime= */ 0,
|
||||
/* startOfBufferInSeconds= */ 0,
|
||||
/* endOfBufferInSeconds= */ 3,
|
||||
/* bufferedToEnd= */ true);
|
||||
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// TODO: Move this test. This test focuses on the function that determines
|
||||
// how much content we have buffered ahead of the playhead rather.
|
||||
// That responsibility is now assigned in |Player|, and not part
|
||||
// of the observer.
|
||||
xit('becomes satisfied with small non-zero start time', () => {
|
||||
poll(observer,
|
||||
/* positionInSeconds= */ 0,
|
||||
/* startOfBufferInSeconds= */ 0.2,
|
||||
/* endOfBufferInSeconds= */ 20,
|
||||
/* bufferedToEnd= */ false);
|
||||
|
||||
expect(onSatisfied).toHaveBeenCalledTimes(1);
|
||||
changed = controller.update(/* lead= */ 3, /* toEnd= */ true);
|
||||
expect(changed).toBeTruthy();
|
||||
expect(controller.getState()).toBe(State.SATISFIED);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {!shaka.media.IPlayheadObserver} observer
|
||||
* @param {number} positionInSeconds
|
||||
* @param {number} startOfBufferInSeconds
|
||||
* @param {number} endOfBufferInSeconds
|
||||
* @param {boolean} isBufferedToEnd
|
||||
*/
|
||||
function poll(
|
||||
observer,
|
||||
positionInSeconds,
|
||||
startOfBufferInSeconds,
|
||||
endOfBufferInSeconds,
|
||||
isBufferedToEnd) {
|
||||
getBufferedAfterSpy.and.callFake((timeInSeconds) => {
|
||||
if (positionInSeconds < startOfBufferInSeconds) {
|
||||
return 0;
|
||||
}
|
||||
if (positionInSeconds > endOfBufferInSeconds) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return endOfBufferInSeconds - positionInSeconds;
|
||||
});
|
||||
|
||||
isBufferedToEndSpy.and.returnValue(isBufferedToEnd);
|
||||
|
||||
observer.poll(positionInSeconds, /* seeking = */ false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1779,6 +1779,10 @@ describe('Player', function() {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// The media element may be paused in a test, make sure that it is reset
|
||||
// to avoid cross-test contamination.
|
||||
video.paused = false;
|
||||
|
||||
// A manifest we can use to test stats.
|
||||
manifest = new shaka.test.ManifestGenerator()
|
||||
.addPeriod(0)
|
||||
@@ -1854,6 +1858,9 @@ describe('Player', function() {
|
||||
expect(stats.playTime).toBeCloseTo(0);
|
||||
expect(stats.bufferingTime).toBeCloseTo(0);
|
||||
|
||||
// Stop buffering and start "playing".
|
||||
buffering(false);
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
|
||||
stats = player.getStats();
|
||||
@@ -2022,6 +2029,11 @@ describe('Player', function() {
|
||||
video.on['ended']();
|
||||
|
||||
expect(player.getStats().stateHistory).toEqual([
|
||||
{
|
||||
timestamp: jasmine.any(Number),
|
||||
duration: jasmine.any(Number),
|
||||
state: 'buffering',
|
||||
},
|
||||
{
|
||||
timestamp: jasmine.any(Number),
|
||||
duration: jasmine.any(Number),
|
||||
|
||||
Reference in New Issue
Block a user