Skip to content

Commit

Permalink
fix: Remove buffered data on fast quality switches (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-barstow authored and forbesjo committed Aug 13, 2018
1 parent 6fb9257 commit bc94fbb
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 20 deletions.
38 changes: 34 additions & 4 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
// update the SegmentLoader instead of doing it twice here and
// on `loadedplaylist`
this.mainSegmentLoader_.playlist(media, this.requestOptions_);

this.mainSegmentLoader_.load();

this.tech_.trigger({
Expand Down Expand Up @@ -496,13 +497,12 @@ export class MasterPlaylistController extends videojs.EventTarget {

/**
* Re-tune playback quality level for the current player
* conditions. This method may perform destructive actions, like
* removing already buffered content, to readjust the currently
* active playlist quickly.
* conditions without performing destructive actions, like
* removing already buffered content
*
* @private
*/
fastQualityChange_() {
smoothQualityChange_() {
let media = this.selectPlaylist();

if (media !== this.masterPlaylistLoader_.media()) {
Expand All @@ -513,6 +513,36 @@ export class MasterPlaylistController extends videojs.EventTarget {
}
}

/**
* Re-tune playback quality level for the current player
* conditions. This method will perform destructive actions like removing
* already buffered content in order to readjust the currently active
* playlist quickly. This is good for manual quality changes
*
* @private
*/
fastQualityChange_() {
const media = this.selectPlaylist();

if (media === this.masterPlaylistLoader_.media()) {
return;
}

this.masterPlaylistLoader_.media(media);

// delete all buffered data to allow an immediate quality switch, then seek
// in place to give the browser a kick to remove any cached frames from the
// previous rendition
this.mainSegmentLoader_.resetEverything(() => {
// Since this is not a typical seek, we avoid the seekTo method which can cause
// segments from the previously enabled rendition to load before the new playlist
// has finished loading
this.tech_.setCurrentTime(this.tech_.currentTime());
});

// don't need to reset audio as it is reset when media changes
}

/**
* Begin playback.
*/
Expand Down
12 changes: 8 additions & 4 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,11 +547,13 @@ export default class SegmentLoader extends videojs.EventTarget {

/**
* Delete all the buffered data and reset the SegmentLoader
* @param {Function} [done] an optional callback to be executed when the remove
* operation is complete
*/
resetEverything() {
resetEverything(done) {
this.ended_ = false;
this.resetLoader();
this.remove(0, this.duration_());
this.remove(0, this.duration_(), done);
// clears fmp4 captions
this.captionParser_.clearAllCaptions();
this.trigger('reseteverything');
Expand Down Expand Up @@ -582,10 +584,12 @@ export default class SegmentLoader extends videojs.EventTarget {
* Remove any data in the source buffer between start and end times
* @param {Number} start - the start time of the region to remove from the buffer
* @param {Number} end - the end time of the region to remove from the buffer
* @param {Function} [done] - an optional callback to be executed when the remove
* operation is complete
*/
remove(start, end) {
remove(start, end, done) {
if (this.sourceUpdater_) {
this.sourceUpdater_.remove(start, end);
this.sourceUpdater_.remove(start, end, done);
}
removeCuesFromTrack(start, end, this.segmentMetadataTrack_);

Expand Down
6 changes: 4 additions & 2 deletions src/source-updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,16 @@ export default class SourceUpdater {
*
* @param {Number} start where to start the removal
* @param {Number} end where to end the removal
* @param {Function} [done=noop] optional callback to be executed when the remove
* operation is complete
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
*/
remove(start, end) {
remove(start, end, done = noop) {
if (this.processedAppend_) {
this.queueCallback_(() => {
this.logger_(`remove [${start} => ${end}]`);
this.sourceBuffer_.remove(start, end);
}, noop);
}, done);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ class HlsHandler extends Component {
document.msFullscreenElement;

if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
this.masterPlaylistController_.fastQualityChange_();
this.masterPlaylistController_.smoothQualityChange_();
}
});
this.on(this.tech_, 'error', function() {
Expand Down
65 changes: 58 additions & 7 deletions test/master-playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ QUnit.test('selects lowest bitrate rendition when enableLowInitialPlaylist is se
assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist');
});

QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
QUnit.test('resyncs SegmentLoader for a smooth quality change', function(assert) {
let resyncs = 0;

this.masterPlaylistController.mediaSource.trigger('sourceopen');
Expand All @@ -240,15 +240,15 @@ QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
return this.masterPlaylistController.master().playlists[0];
};

this.masterPlaylistController.fastQualityChange_();
this.masterPlaylistController.smoothQualityChange_();

assert.equal(resyncs, 1, 'resynced the segmentLoader');

// verify stats
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});

QUnit.test('does not resync the segmentLoader when no fast quality change occurs',
QUnit.test('does not resync the segmentLoader when no smooth quality change occurs',
function(assert) {
let resyncs = 0;

Expand All @@ -264,14 +264,14 @@ QUnit.test('does not resync the segmentLoader when no fast quality change occurs
resyncs++;
};

this.masterPlaylistController.fastQualityChange_();
this.masterPlaylistController.smoothQualityChange_();

assert.equal(resyncs, 0, 'did not resync the segmentLoader');
// verify stats
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});

QUnit.test('fast quality change resyncs audio segment loader', function(assert) {
QUnit.test('smooth quality change resyncs audio segment loader', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
Expand Down Expand Up @@ -306,7 +306,7 @@ QUnit.test('fast quality change resyncs audio segment loader', function(assert)
};

masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
masterPlaylistController.fastQualityChange_();
masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same');

// force different media
Expand All @@ -315,7 +315,7 @@ QUnit.test('fast quality change resyncs audio segment loader', function(assert)
};

assert.equal(this.requests.length, 1, 'one request');
masterPlaylistController.fastQualityChange_();
masterPlaylistController.smoothQualityChange_();
assert.equal(this.requests.length, 2, 'added a request for new media');
assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
// new media request
Expand All @@ -324,6 +324,57 @@ QUnit.test('fast quality change resyncs audio segment loader', function(assert)
assert.equal(resets, 0, 'does not reset the audio segment loader when media changes');
});

QUnit.test('resets everything for a fast quality change', function(assert) {
let resyncs = 0;
let resets = 0;
let removeFuncArgs = {};

this.masterPlaylistController.mediaSource.trigger('sourceopen');
// master
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());

let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;

segmentLoader.resyncLoader = () => resyncs++;

segmentLoader.remove = function(start, end) {
removeFuncArgs = {
start,
end
};
};

segmentLoader.duration_ = () => 60;

segmentLoader.on('reseteverything', function() {
resets++;
});

// media is unchanged
this.masterPlaylistController.fastQualityChange_();

assert.equal(resyncs, 0, 'does not resync segment loader if media is unchanged');

assert.equal(resets, 0, 'reseteverything event not triggered if media is unchanged');

assert.deepEqual(removeFuncArgs, {}, 'remove() not called if media is unchanged');

// media is changed
this.masterPlaylistController.selectPlaylist = () => {
return this.masterPlaylistController.master().playlists[0];
};

this.masterPlaylistController.fastQualityChange_();

assert.equal(resyncs, 1, 'resynced segment loader if media is changed');

assert.equal(resets, 1, 'reseteverything event triggered if media is changed');

assert.deepEqual(removeFuncArgs, {start: 0, end: 60}, 'remove() called with correct arguments if media is changed');
});

QUnit.test('audio segment loader is reset on audio track change', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
Expand Down
4 changes: 2 additions & 2 deletions test/videojs-http-streaming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3040,7 +3040,7 @@ QUnit.test('stats are reset on dispose', function(assert) {
assert.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset');
});

QUnit.test('detects fullscreen and triggers a quality change', function(assert) {
QUnit.test('detects fullscreen and triggers a smooth quality change', function(assert) {
let qualityChanges = 0;
let hls = HlsSourceHandler.handleSource({
src: 'manifest/master.m3u8',
Expand All @@ -3056,7 +3056,7 @@ QUnit.test('detects fullscreen and triggers a quality change', function(assert)
}
});

hls.masterPlaylistController_.fastQualityChange_ = function() {
hls.masterPlaylistController_.smoothQualityChange_ = function() {
qualityChanges++;
};

Expand Down

0 comments on commit bc94fbb

Please sign in to comment.