mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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_();
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user