Skip to content

Commit

Permalink
Generalize State History
Browse files Browse the repository at this point in the history
This change makes state history track any state by moving the idea of
buffering, playing, etc to the player.

This allowed the stats and state history to drop they connection to the
media element.

Change-Id: Ieed198a09b3ade33e4ee850445b809f251cf2558
  • Loading branch information
vaage committed Mar 28, 2019
1 parent 4da7bcb commit 78aa575
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 88 deletions.
34 changes: 21 additions & 13 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ shaka.Player = function(mediaElement, dependencyInjector) {
this.loadingTextStreams_ = new Set();

/** @private {boolean} */
this.buffering_ = false;
this.isBuffering_ = false;

/** @private {boolean} */
this.switchingPeriods_ = true;
Expand Down Expand Up @@ -1433,7 +1433,7 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) {
this.assetUri_ = assetUri;

// Stats are for a single playback/load session.
this.stats_ = new shaka.util.Stats(mediaElement, wants.startTimeOfLoad);
this.stats_ = new shaka.util.Stats(wants.startTimeOfLoad);

const updateStateHistory = () => this.updateStateHistory_();
this.eventManager_.listen(mediaElement, 'playing', updateStateHistory);
Expand Down Expand Up @@ -2129,7 +2129,7 @@ shaka.Player.prototype.getExpiration = function() {
* @export
*/
shaka.Player.prototype.isBuffering = function() {
return this.buffering_;
return this.isBuffering_;
};


Expand Down Expand Up @@ -2630,7 +2630,7 @@ shaka.Player.prototype.getStats = function() {
return shaka.util.Stats.getEmptyBlob();
}

this.stats_.updateTime(this.buffering_);
this.stats_.updateTime(this.isBuffering_);
this.updateStateHistory_();

goog.asserts.assert(this.video_, 'If we have stats, we should have video_');
Expand Down Expand Up @@ -3200,10 +3200,10 @@ shaka.Player.prototype.onBuffering_ = function(buffering) {
// We must check |stats_| first because we call |onBuffering_| after
// unloading.
if (this.stats_) {
this.stats_.updateTime(this.buffering_);
this.stats_.updateTime(this.isBuffering_);
}

this.buffering_ = buffering;
this.isBuffering_ = buffering;
this.updateStateHistory_();

if (this.playhead_) {
Expand All @@ -3225,17 +3225,25 @@ shaka.Player.prototype.onChangePeriod_ = function() {


/**
* Called from potential initiators of state changes, or before returning stats
* to the user.
*
* This method decides if state has actually changed, updates the last entry,
* and adds a new one if needed.
* Try updating the state history. If the player has not finished initializing,
* this will be a no-op.
*
* @private
*/
shaka.Player.prototype.updateStateHistory_ = function() {
if (this.stats_) {
this.stats_.getStateHistory().update(this.buffering_);
// If we have not finish initializing, this will be a no-op.
if (!this.stats_) { return; }

const history = this.stats_.getStateHistory();

if (this.isBuffering_) {
history.update('buffering');
} else if (this.video_.paused) {
history.update('paused');
} else if (this.video_.ended) {
history.update('ended');
} else {
history.update('playing');
}
};

Expand Down
49 changes: 16 additions & 33 deletions lib/util/state_history.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@ goog.require('goog.asserts');


/**
* This class is used to track the changes in video state (playing, paused,
* buffering, and ended) while playing content.
* This class is used to track the time spent in arbitrary states. When told of
* a state, it will assume that state was active until a new state is provided.
* When provided with identical states back-to-back, the existing entry will be
* updated.
*
* @final
*/
shaka.util.StateHistory = class {
/**
* @param {!HTMLMediaElement} element
*/
constructor(element) {
/** @private {!HTMLMediaElement} */
this.element_ = element;

constructor() {
/**
* The state that we think is still the current change. It is "open" for
* updating.
Expand All @@ -52,14 +48,14 @@ shaka.util.StateHistory = class {
}

/**
* @param {boolean} isBuffering
* @param {string} state
*/
update(isBuffering) {
update(state) {
// |open_| will only be |null| when we first call |update|.
if (this.open_ == null) {
this.start_(isBuffering);
this.start_(state);
} else {
this.update_(isBuffering);
this.update_(state);
}
}

Expand Down Expand Up @@ -90,63 +86,50 @@ shaka.util.StateHistory = class {
}

/**
* @param {boolean} isBuffering
* @param {string} state
* @private
*/
start_(isBuffering) {
start_(state) {
goog.asserts.assert(
this.open_ == null,
'There must be no open entry in order when we start');

this.open_ = {
timestamp: this.getNowInSeconds_(),
state: this.getCurrentState_(isBuffering),
state: state,
duration: 0,
};
}

/**
* @param {boolean} isBuffering
* @param {string} state
* @private
*/
update_(isBuffering) {
update_(state) {
goog.asserts.assert(
this.open_,
'There must be an open entry in order to update it');

const currentTimeSeconds = this.getNowInSeconds_();
const currentState = this.getCurrentState_(isBuffering);

// Always update the duration so that it can always be as accurate as
// possible.
this.open_.duration = currentTimeSeconds - this.open_.timestamp;

// If the state has not changed, there is no need to add a new entry.
if (this.open_.state == currentState) {
if (this.open_.state == state) {
return;
}

// We have changed states, so "close" the open state.
this.closed_.push(this.open_);
this.open_ = {
timestamp: currentTimeSeconds,
state: currentState,
state: state,
duration: 0,
};
}

/**
* @param {boolean} isBuffering
* @return {string}
* @private
*/
getCurrentState_(isBuffering) {
if (isBuffering) { return 'buffering'; }
if (this.element_.ended) { return 'ended'; }
if (this.element_.paused) { return 'paused'; }
return 'playing';
}

/**
* Get the system time in seconds.
*
Expand Down
5 changes: 2 additions & 3 deletions lib/util/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ goog.require('shaka.util.SwitchHistory');
*/
shaka.util.Stats = class {
/**
* @param {!HTMLMediaElement} element
* @param {number} startOfLoadSeconds
*/
constructor(element, startOfLoadSeconds) {
constructor(startOfLoadSeconds) {
/** @private {number} */
this.width_ = NaN;
/** @private {number} */
Expand Down Expand Up @@ -65,7 +64,7 @@ shaka.util.Stats = class {
this.lastTimeUpdate_ = null;

/** @private {!shaka.util.StateHistory} */
this.stateHistory_ = new shaka.util.StateHistory(element);
this.stateHistory_ = new shaka.util.StateHistory();

/** @private {!shaka.util.SwitchHistory} */
this.switchHistory_ = new shaka.util.SwitchHistory();
Expand Down
39 changes: 0 additions & 39 deletions test/player_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2035,45 +2035,6 @@ describe('Player', function() {
]);
});

it('accumulates duration as time passes', function() {
// We are using a mock date, so this is not a race.
let bufferingStarts = Date.now() / 1000;
setBuffering(true);

expect(player.getStats().stateHistory).toEqual([
{
timestamp: bufferingStarts,
duration: 0,
state: 'buffering',
},
]);

jasmine.clock().tick(1500);
expect(player.getStats().stateHistory).toEqual([
{
timestamp: bufferingStarts,
duration: 1.5,
state: 'buffering',
},
]);

let playbackStarts = Date.now() / 1000;
setBuffering(false);
jasmine.clock().tick(9000);
expect(player.getStats().stateHistory).toEqual([
{
timestamp: bufferingStarts,
duration: 1.5,
state: 'buffering',
},
{
timestamp: playbackStarts,
duration: 9,
state: 'playing',
},
]);
});

/**
* @param {boolean} buffering
* @suppress {accessControls}
Expand Down
74 changes: 74 additions & 0 deletions test/util/state_history_unit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

describe('StateHistory', () => {
/** @type {!shaka.util.StateHistory} */
let history;

beforeAll(() => {
// Mock the clock so that the timing logic inside the state history will be
// controlled by the test and not the system.
jasmine.clock().install();
jasmine.clock().mockDate();
});

afterAll(() => {
jasmine.clock().uninstall();
});

beforeEach(() => {
history = new shaka.util.StateHistory();
});

// After a state change, the new state should have no duration. It must have
// an update after (regardless of state) to update the duration.
it('open entry have no duration', () => {
history.update('a');
jasmine.clock().tick(5000);

const entries = history.getCopy();
expect(entries.length).toBe(1);
expect(entries[0].state).toBe('a');
expect(entries[0].duration).toBe(0);
});

// Updating while in the same state should only add duration to the previous
// entry.
it('accumulates time', () => {
history.update('a');
jasmine.clock().tick(5000);
history.update('a');

const entries = history.getCopy();
expect(entries.length).toBe(1);
expect(entries[0].state).toBe('a');
expect(entries[0].duration).toBe(5);
});

it('state changes update duration of last entry', () => {
history.update('a');
jasmine.clock().tick(5000);
history.update('b');

const entries = history.getCopy();
expect(entries.length).toBe(2);
expect(entries[0].state).toBe('a');
expect(entries[0].duration).toBe(5);
expect(entries[1].state).toBe('b');
expect(entries[1].duration).toBe(0);
});
});

0 comments on commit 78aa575

Please sign in to comment.