Make buffer clearing behavior explicit

- Replace both boolean clear and number-of-seconds args with an enum
   indicating a specific type of clear behavior: none, all, or most
   (all but 2 segments, currently).
 - Calculate segment size from the content in StreamingEngine, rather
   than assuming a common segment size in AbrManager.

Change-Id: I9e4fee0945d5e50fd7da86bb8467911e60c4575e
This commit is contained in:
Joey Parrish
2016-09-27 10:22:14 -07:00
parent 4f8dd3bf82
commit a8ac3142ef
7 changed files with 133 additions and 65 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ shakaDemo.onTrackSelected_ = function(event) {
var track = option.track;
var player = shakaDemo.player_;
player.selectTrack(track, /* clearBuffer */ true);
player.selectTrack(track, shaka.Player.ClearMethod.ALL);
// Adaptation might have been changed by calling selectTrack().
// Update the adaptation checkbox.
+12 -8
View File
@@ -30,16 +30,20 @@ shakaExtern.AbrManager = function() {};
/**
* @typedef {function(!Object.<string, !shakaExtern.Stream>, number=)}
* A callback which implementations call to switch streams.
* A callback which implementations call to switch streams.
*
* The first argument is a map of content types to chosen streams.
* The first argument is a map of content types to chosen streams.
*
* The second argument is an optional number of seconds of content to leave in
* the buffer ahead of the playhead. Anything beyond that will be cleared.
* This is used to make a resolution change take effect sooner, at the cost of
* wasting previously downloaded segments. If undefined, nothing will be
* cleared.
* The second argument is an optional clear method as defined by the player.
* (Clear none, clear all, leave one segment, etc.)
*
* @typedef {function(
* !Object.<string, !shakaExtern.Stream>,
* shaka.Player.ClearMethod=
* )}
*
* @see {shaka.Player.ClearMethod}
* @exportDoc
*/
shakaExtern.AbrManager.SwitchCallback;
+8 -16
View File
@@ -105,16 +105,6 @@ shaka.abr.SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85;
shaka.abr.SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95;
/**
* The number of seconds of content to leave in buffer ahead of the playhead
* when upgrading video. This makes video upgrades visible sooner.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_ = 10;
/** @override */
shaka.abr.SimpleAbrManager.prototype.stop = function() {
this.switch_ = null;
@@ -247,12 +237,14 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
var chosen = this.chooseStreams_();
// Do not clear the buffer.
var opt_leaveInBuffer = undefined;
var clearBuffer = shaka.Player.ClearMethod.NONE;
if (oldVideo && chosen.video &&
chosen.video.bandwidth > oldVideo.bandwidth) {
// We're upgrading video.
// Leave some in buffer, but clear ahead of that.
opt_leaveInBuffer = shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_;
// We're upgrading video. Clear most of the content from the buffer, but
// leave enough content in buffer to ensure playback is not interrupted.
// This will get us a smooth transition and the upgrade will take effect
// quickly.
clearBuffer = shaka.Player.ClearMethod.MOST;
}
var currentBandwidthKbps =
@@ -260,10 +252,10 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
shaka.log.debug(
'Calling switch_()...',
'bandwidth=' + currentBandwidthKbps + ' kbps',
'opt_leaveInBuffer=', opt_leaveInBuffer);
'clearBuffer=', clearBuffer);
// If any of these chosen streams are already chosen, Player will filter them
// out before passing the choices on to StreamingEngine.
this.switch_(chosen, opt_leaveInBuffer);
this.switch_(chosen, clearBuffer);
};
+62 -15
View File
@@ -406,10 +406,10 @@ shaka.media.StreamingEngine.prototype.notifyNewStream = function(type, stream) {
*
* @param {string} contentType |stream|'s content type.
* @param {shakaExtern.Stream} stream
* @param {number=} opt_leaveInBuffer
* @param {shaka.Player.ClearMethod} clear
*/
shaka.media.StreamingEngine.prototype.switch = function(
contentType, stream, opt_leaveInBuffer) {
contentType, stream, clear) {
var mediaState = this.mediaStates_[contentType];
if (!mediaState && contentType == 'text' &&
this.config_.ignoreTextStreamFailures) {
@@ -445,19 +445,66 @@ shaka.media.StreamingEngine.prototype.switch = function(
var streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.debug('switch: switching to Stream ' + streamTag);
if (opt_leaveInBuffer != undefined) {
// Ignore if we are already clearing the buffer.
if (!mediaState.waitingToClearBuffer && !mediaState.clearingBuffer) {
if (mediaState.performingUpdate) {
// We are performing an update, so we have to wait until it's finished.
// onUpdate_() will call clearBuffer_() when the update has
// finished.
this.waitToClearBuffer_(mediaState, opt_leaveInBuffer);
} else {
this.cancelUpdate_(mediaState);
this.clearBuffer_(mediaState, opt_leaveInBuffer);
}
this.clear_(mediaState, clear);
};
/**
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {shaka.Player.ClearMethod} clear
* @private
*/
shaka.media.StreamingEngine.prototype.clear_ = function(mediaState, clear) {
if (clear == shaka.Player.ClearMethod.NONE) {
// Nothing to clear.
return;
}
// Ignore if we are already clearing the buffer.
if (mediaState.clearingBuffer) {
return;
}
var leaveInBuffer = 0; // assumed ClearMethod.ALL
if (clear == shaka.Player.ClearMethod.MOST) {
// Calculate how much to leave in buffer. We don't have any general
// knowledge about segment size, so just measure the size of the segment
// under the playhead.
var playheadTime = this.playhead_.getTime();
var periodIndex = this.findPeriodContainingTime_(playheadTime);
var position = this.lookupSegmentPosition_(
mediaState, playheadTime, periodIndex);
goog.asserts.assert(position != null,
'No segment under the playhead! This should not happen!');
if (!position) {
return;
}
var reference = mediaState.stream.getSegmentReference(position);
goog.asserts.assert(reference != null,
'No reference for current position! This should not happen!');
if (!reference) {
return;
}
// Leave two segments worth of data, without regard for segment boundaries.
// It is too complex to handle all the edge cases involved in clearing
// exactly at a segment boundary.
leaveInBuffer = (reference.endTime - reference.startTime) * 2;
} // clear == shaka.Player.ClearMethod.MOST
if (mediaState.performingUpdate) {
// We are performing an update, so we have to wait until it's finished.
// onUpdate_() will call clearBuffer_() when the update has
// finished.
this.waitToClearBuffer_(mediaState, leaveInBuffer);
} else {
// Cancel the update timer, if any.
this.cancelUpdate_(mediaState);
// Clear right away.
this.clearBuffer_(mediaState, leaveInBuffer);
}
};
@@ -1531,7 +1578,7 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
for (var type in this.mediaStates_) {
var stream = streamsByType[type];
this.switch(type, stream);
this.switch(type, stream, shaka.Player.ClearMethod.NONE);
var mediaState = this.mediaStates_[type];
if (shaka.media.StreamingEngine.isIdle_(mediaState)) {
+40 -17
View File
@@ -123,7 +123,10 @@ shaka.Player = function(video, opt_dependencyInjector) {
this.loadChain_ = null;
/**
* @private {!Object.<string, {stream: shakaExtern.Stream, clear: boolean}>}
* @private {!Object.<string, {
* stream: shakaExtern.Stream,
* clear: shaka.Player.ClearMethod
* }>}
*/
this.deferredSwitches_ = {};
@@ -827,12 +830,26 @@ shaka.Player.prototype.getTracks = function() {
};
/**
* @enum {number}
* @export
*/
shaka.Player.ClearMethod = {
/** Do not clear the buffer when switching tracks. */
'NONE': 0,
/** Clear all data from buffer when switching tracks. */
'ALL': 1,
/** Leave a small amount of data in buffer when switching tracks. */
'MOST': 2
};
/**
* Select a specific track. For audio or video, this disables adaptation.
* Note that AdaptationEvents are not fired for manual track selections.
*
* @param {shakaExtern.Track} track
* @param {boolean=} opt_clearBuffer
* @param {shaka.Player.ClearMethod=} opt_clearBuffer
* @export
*/
shaka.Player.prototype.selectTrack = function(track, opt_clearBuffer) {
@@ -881,7 +898,6 @@ shaka.Player.prototype.selectTrack = function(track, opt_clearBuffer) {
streamsToSwitch['text'] = currentTextStream;
}
this.deferredSwitch_(streamsToSwitch, opt_clearBuffer);
};
@@ -1294,19 +1310,21 @@ shaka.Player.prototype.filterPeriod_ = function(period) {
/**
* Switches to the given streams, deferring switches if needed.
* @param {!Object.<string, shakaExtern.Stream>} streamsByType
* @param {boolean=} opt_clearBuffer
* @param {shaka.Player.ClearMethod=} opt_clearBuffer
* @private
*/
shaka.Player.prototype.deferredSwitch_ = function(
streamsByType, opt_clearBuffer) {
for (var type in streamsByType) {
var stream = streamsByType[type];
var clear = opt_clearBuffer || type == 'text';
var clear = opt_clearBuffer || shaka.Player.ClearMethod.NONE;
// TODO: consider adding a cue replacement algorithm to TextEngine to remove
// this special case for text:
if (type == 'text') clear = shaka.Player.ClearMethod.ALL;
if (this.switchingPeriods_) {
this.deferredSwitches_[type] = {stream: stream, clear: clear};
} else {
var opt_leaveInBuffer = clear ? 0 : undefined;
this.streamingEngine_.switch(type, stream, opt_leaveInBuffer);
this.streamingEngine_.switch(type, stream, clear);
}
}
};
@@ -1439,6 +1457,7 @@ shaka.Player.prototype.chooseStreams_ =
/**
* Chooses streams from the given Period and switches to them.
* Called after a config change, a new text stream, or a key status event.
*
* @param {!shakaExtern.Period} period
* @private
@@ -1448,6 +1467,8 @@ shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) {
var languageMatches = { 'audio': false, 'text': false };
var streamSetsByType = shaka.util.StreamUtils.chooseStreamSets(
period, this.config_, languageMatches);
// chooseStreams_ filters out choices which are already active.
var chosen = this.chooseStreams_(period, streamSetsByType);
for (var type in chosen) {
@@ -1460,8 +1481,11 @@ shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) {
});
}
// TODO: are these cases where we should avoid clearBuffer at this point?
this.deferredSwitch_(chosen, /* clearBuffer */ true);
// Because we're running this after a config change (manual language change),
// a new text stream, or a key status event, and because active streams have
// been filtered out already, it is always okay to clear the buffer for what
// remains.
this.deferredSwitch_(chosen, shaka.Player.ClearMethod.ALL);
// Send an adaptation event so that the UI can show the new language/tracks.
this.onAdaptation_();
@@ -1512,7 +1536,8 @@ shaka.Player.prototype.onChooseStreams_ = function(period) {
// transition if any of these deferred selections are from the wrong period.
for (var type in this.deferredSwitches_) {
// We are choosing initial tracks, so no segments from this Period have
// been downloaded yet.
// been downloaded yet. Therefore, it is okay to ignore the .clear member
// of this structure.
chosen[type] = this.deferredSwitches_[type].stream;
}
this.deferredSwitches_ = {};
@@ -1551,8 +1576,7 @@ shaka.Player.prototype.canSwitch_ = function() {
// If we still have deferred switches, switch now.
for (var type in this.deferredSwitches_) {
var info = this.deferredSwitches_[type];
var opt_leaveInBuffer = info.clear ? 0 : undefined;
this.streamingEngine_.switch(type, info.stream, opt_leaveInBuffer);
this.streamingEngine_.switch(type, info.stream, info.clear);
}
this.deferredSwitches_ = {};
};
@@ -1562,10 +1586,10 @@ shaka.Player.prototype.canSwitch_ = function() {
* Callback from AbrManager.
*
* @param {!Object.<string, !shakaExtern.Stream>} streamsByType
* @param {number=} opt_leaveInBuffer
* @param {shaka.Player.ClearMethod=} opt_clearBuffer
* @private
*/
shaka.Player.prototype.switch_ = function(streamsByType, opt_leaveInBuffer) {
shaka.Player.prototype.switch_ = function(streamsByType, opt_clearBuffer) {
shaka.log.debug('switch_');
goog.asserts.assert(this.config_.abr.enabled,
'AbrManager should not call switch while disabled!');
@@ -1602,9 +1626,8 @@ shaka.Player.prototype.switch_ = function(streamsByType, opt_leaveInBuffer) {
}
for (var type in streamsByType) {
this.streamingEngine_.switch(type, streamsByType[type],
// Only apply opt_leaveInBuffer to video streams.
type == 'video' ? opt_leaveInBuffer : undefined);
var clearBuffer = opt_clearBuffer || shaka.Player.ClearMethod.NONE;
this.streamingEngine_.switch(type, streamsByType[type], clearBuffer);
}
this.onAdaptation_();
};
+2 -3
View File
@@ -263,9 +263,8 @@ describe('SimpleAbrManager', function() {
// called to upgrade.
abrManager.segmentDownloaded(3000, 4000, bytesPerSecond);
// The second parameter is the number of seconds to leave in buffer.
expect(switchCallback).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Number));
jasmine.any(Object), shaka.Player.ClearMethod.MOST);
});
it('does not clear ahead on downgrade', function() {
@@ -293,6 +292,6 @@ describe('SimpleAbrManager', function() {
// The second parameter is undefined to indicate that the buffer should
// not be cleared
expect(switchCallback).toHaveBeenCalledWith(
jasmine.any(Object), undefined);
jasmine.any(Object), shaka.Player.ClearMethod.NONE);
});
});
+8 -5
View File
@@ -26,6 +26,7 @@ describe('Player', function() {
var networkingEngine;
var streamingEngine;
var video;
var ClearMethod;
beforeAll(function() {
originalLogError = shaka.log.error;
@@ -35,6 +36,8 @@ describe('Player', function() {
shaka.log.error = logErrorSpy;
logWarnSpy = jasmine.createSpy('shaka.log.warning');
shaka.log.warning = logWarnSpy;
ClearMethod = shaka.Player.ClearMethod;
});
beforeEach(function() {
@@ -791,7 +794,7 @@ describe('Player', function() {
expect(tracks[1].id).toBe(stream.id);
player.selectTrack(tracks[1]);
expect(streamingEngine.switch)
.toHaveBeenCalledWith('audio', stream, undefined);
.toHaveBeenCalledWith('audio', stream, ClearMethod.NONE);
});
it('still switches streams if called during startup', function() {
@@ -816,7 +819,7 @@ describe('Player', function() {
var period = manifest.periods[0];
var stream = period.streamSets[0].streams[1];
expect(streamingEngine.switch)
.toHaveBeenCalledWith('audio', stream, undefined);
.toHaveBeenCalledWith('audio', stream, ClearMethod.NONE);
});
it('switching audio doesn\'t change selected text track', function() {
@@ -833,7 +836,7 @@ describe('Player', function() {
var textStream = period.streamSets[3].streams[0];
expect(streamingEngine.switch)
.toHaveBeenCalledWith('text', textStream, 0);
.toHaveBeenCalledWith('text', textStream, ClearMethod.ALL);
streamingEngine.switch.calls.reset();
@@ -841,9 +844,9 @@ describe('Player', function() {
expect(tracks[1].id).toBe(audioStream.id);
player.selectTrack(tracks[1]);
expect(streamingEngine.switch)
.toHaveBeenCalledWith('text', textStream, 0);
.toHaveBeenCalledWith('text', textStream, ClearMethod.ALL);
expect(streamingEngine.switch)
.toHaveBeenCalledWith('audio', audioStream, undefined);
.toHaveBeenCalledWith('audio', audioStream, ClearMethod.NONE);
});
});