Browse Source

fix: Remove buffered data on fast quality switches (#113)

pull/194/merge
Alex Barstow 7 years ago
committed by Joe Forbes
parent
commit
bc94fbbb6e
  1. 38
      src/master-playlist-controller.js
  2. 12
      src/segment-loader.js
  3. 6
      src/source-updater.js
  4. 2
      src/videojs-http-streaming.js
  5. 65
      test/master-playlist-controller.test.js
  6. 4
      test/videojs-http-streaming.test.js

38
src/master-playlist-controller.js

@ -314,6 +314,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
// update the SegmentLoader instead of doing it twice here and // update the SegmentLoader instead of doing it twice here and
// on `loadedplaylist` // on `loadedplaylist`
this.mainSegmentLoader_.playlist(media, this.requestOptions_); this.mainSegmentLoader_.playlist(media, this.requestOptions_);
this.mainSegmentLoader_.load(); this.mainSegmentLoader_.load();
this.tech_.trigger({ this.tech_.trigger({
@ -496,13 +497,12 @@ export class MasterPlaylistController extends videojs.EventTarget {
/** /**
* Re-tune playback quality level for the current player * 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 * @private
*/ */
fastQualityChange_() {
smoothQualityChange_() {
let media = this.selectPlaylist(); let media = this.selectPlaylist();
if (media !== this.masterPlaylistLoader_.media()) { if (media !== this.masterPlaylistLoader_.media()) {
@ -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. * Begin playback.
*/ */

12
src/segment-loader.js

@ -547,11 +547,13 @@ export default class SegmentLoader extends videojs.EventTarget {
/** /**
* Delete all the buffered data and reset the SegmentLoader * 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.ended_ = false;
this.resetLoader(); this.resetLoader();
this.remove(0, this.duration_());
this.remove(0, this.duration_(), done);
// clears fmp4 captions // clears fmp4 captions
this.captionParser_.clearAllCaptions(); this.captionParser_.clearAllCaptions();
this.trigger('reseteverything'); this.trigger('reseteverything');
@ -582,10 +584,12 @@ export default class SegmentLoader extends videojs.EventTarget {
* Remove any data in the source buffer between start and end times * 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} 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 {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_) { if (this.sourceUpdater_) {
this.sourceUpdater_.remove(start, end);
this.sourceUpdater_.remove(start, end, done);
} }
removeCuesFromTrack(start, end, this.segmentMetadataTrack_); removeCuesFromTrack(start, end, this.segmentMetadataTrack_);

6
src/source-updater.js

@ -131,14 +131,16 @@ export default class SourceUpdater {
* *
* @param {Number} start where to start the removal * @param {Number} start where to start the removal
* @param {Number} end where to end 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 * @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_) { if (this.processedAppend_) {
this.queueCallback_(() => { this.queueCallback_(() => {
this.logger_(`remove [${start} => ${end}]`); this.logger_(`remove [${start} => ${end}]`);
this.sourceBuffer_.remove(start, end); this.sourceBuffer_.remove(start, end);
}, noop);
}, done);
} }
} }

2
src/videojs-http-streaming.js

@ -320,7 +320,7 @@ class HlsHandler extends Component {
document.msFullscreenElement; document.msFullscreenElement;
if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) { if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
this.masterPlaylistController_.fastQualityChange_();
this.masterPlaylistController_.smoothQualityChange_();
} }
}); });
this.on(this.tech_, 'error', function() { this.on(this.tech_, 'error', function() {

65
test/master-playlist-controller.test.js

@ -221,7 +221,7 @@ QUnit.test('selects lowest bitrate rendition when enableLowInitialPlaylist is se
assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); 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; let resyncs = 0;
this.masterPlaylistController.mediaSource.trigger('sourceopen'); this.masterPlaylistController.mediaSource.trigger('sourceopen');
@ -240,7 +240,7 @@ QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
return this.masterPlaylistController.master().playlists[0]; return this.masterPlaylistController.master().playlists[0];
}; };
this.masterPlaylistController.fastQualityChange_();
this.masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 1, 'resynced the segmentLoader'); assert.equal(resyncs, 1, 'resynced the segmentLoader');
@ -248,7 +248,7 @@ QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth'); 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) { function(assert) {
let resyncs = 0; let resyncs = 0;
@ -264,14 +264,14 @@ QUnit.test('does not resync the segmentLoader when no fast quality change occurs
resyncs++; resyncs++;
}; };
this.masterPlaylistController.fastQualityChange_();
this.masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 0, 'did not resync the segmentLoader'); assert.equal(resyncs, 0, 'did not resync the segmentLoader');
// verify stats // verify stats
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth'); 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.requests.length = 0;
this.player = createPlayer(); this.player = createPlayer();
this.player.src({ this.player.src({
@ -306,7 +306,7 @@ QUnit.test('fast quality change resyncs audio segment loader', function(assert)
}; };
masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++; masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
masterPlaylistController.fastQualityChange_();
masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same'); assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same');
// force different media // force different media
@ -315,7 +315,7 @@ QUnit.test('fast quality change resyncs audio segment loader', function(assert)
}; };
assert.equal(this.requests.length, 1, 'one request'); 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(this.requests.length, 2, 'added a request for new media');
assert.equal(resyncs, 0, 'does not resync the audio segment loader yet'); assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
// new media request // new media request
@ -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'); 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) { QUnit.test('audio segment loader is reset on audio track change', function(assert) {
this.requests.length = 0; this.requests.length = 0;
this.player = createPlayer(); this.player = createPlayer();

4
test/videojs-http-streaming.test.js

@ -3040,7 +3040,7 @@ QUnit.test('stats are reset on dispose', function(assert) {
assert.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset'); 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 qualityChanges = 0;
let hls = HlsSourceHandler.handleSource({ let hls = HlsSourceHandler.handleSource({
src: 'manifest/master.m3u8', src: 'manifest/master.m3u8',
@ -3056,7 +3056,7 @@ QUnit.test('detects fullscreen and triggers a quality change', function(assert)
} }
}); });
hls.masterPlaylistController_.fastQualityChange_ = function() {
hls.masterPlaylistController_.smoothQualityChange_ = function() {
qualityChanges++; qualityChanges++;
}; };

Loading…
Cancel
Save