Browse Source

feat: do fast rendition changes on fullscreen changes and user actions (#1074)

Deprecate smoothQualityChange_ on the MPC, but otherwise, always do fast quality change whenever a user requests a rendition change and on fullscreen change
pull/1142/head
Garrett Singer 4 years ago
committed by GitHub
parent
commit
5405c1887d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      README.md
  2. 2
      scripts/index-demo-page.js
  3. 10
      src/master-playlist-controller.js
  4. 11
      src/videojs-http-streaming.js
  5. 5
      test/configuration.test.js
  6. 148
      test/master-playlist-controller.test.js
  7. 3
      test/segment-loader.test.js
  8. 13
      test/videojs-http-streaming.test.js

14
README.md

@ -402,20 +402,16 @@ If true, this will take the device pixel ratio into account when doing rendition
This setting is `false` by default.
##### smoothQualityChange
* NOTE: DEPRECATED
* Type: `boolean`
* can be used as a source option
* can be used as an initialization option
When the `smoothQualityChange` property is set to `true`, a manual quality
change triggered via the [representations API](#vhsrepresentations) will use
smooth quality switching rather than the default fast (buffer-ejecting)
quality switching. Using smooth quality switching will mean no loading spinner
will appear during quality switches, but will cause quality switches to only
be visible after a few seconds.
smoothQualityChange is deprecated and will be removed in the next major version of VHS.
Note that this _only_ affects quality changes triggered via the representations
API; automatic quality switches based on available bandwidth will always be
smooth switches.
Instead of its prior behavior, smoothQualityChange will now call fastQualityChange, which
clears the buffer, chooses a new rendition, and starts loading content from the current
playhead position.
##### allowSeeksWithinUnsafeLiveWindow
* Type: `boolean`

2
scripts/index-demo-page.js

@ -21,7 +21,7 @@
rep.playlist.disabled = rep.id !== id;
});
window.mpc.smoothQualityChange_();
window.mpc.fastQualityChange_();
});
var hlsOptGroup = document.querySelector('[label="hls"]');
var dashOptGroup = document.querySelector('[label="dash"]');

10
src/master-playlist-controller.js

@ -848,16 +848,10 @@ export class MasterPlaylistController extends videojs.EventTarget {
* removing already buffered content
*
* @private
* @deprecated
*/
smoothQualityChange_(media = this.selectPlaylist()) {
if (media === this.masterPlaylistLoader_.media()) {
return;
}
this.switchMedia_(media, 'smooth-quality');
this.mainSegmentLoader_.resetLoader();
// don't need to reset audio as it is reset when media changes
this.fastQualityChange_(media);
}
/**

11
src/videojs-http-streaming.js

@ -590,7 +590,12 @@ class VhsHandler extends Component {
document.msFullscreenElement;
if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
this.masterPlaylistController_.smoothQualityChange_();
this.masterPlaylistController_.fastQualityChange_();
} else {
// When leaving fullscreen, since the in page pixel dimensions should be smaller
// than full screen, see if there should be a rendition switch down to preserve
// bandwidth.
this.masterPlaylistController_.checkABR_();
}
});
@ -708,6 +713,10 @@ class VhsHandler extends Component {
this.tech_.setCurrentTime(time);
};
if (this.options_.smoothQualityChange) {
videojs.log.warn('smoothQualityChange is deprecated and will be removed in the next major version');
}
this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
const playbackWatcherOptions = videojs.mergeOptions(

5
test/configuration.test.js

@ -36,11 +36,6 @@ const options = [{
default: 4194304,
test: 5,
alt: 555
}, {
name: 'smoothQualityChange',
default: false,
test: true,
alt: false
}, {
name: 'useBandwidthFromLocalStorage',
default: false,

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

@ -430,8 +430,10 @@ QUnit.test(
}
);
QUnit.test('resyncs SegmentLoader for a smooth quality change', function(assert) {
let resyncs = 0;
// Since smoothQualityChange is deprecated, calls to smoothQualityChange_ should call
// fastQualityChange_.
QUnit.test('smoothQualityChange_ calls fastQualityChange_', function(assert) {
let fastQualityChangeCalls = 0;
this.masterPlaylistController.mediaSource.trigger('sourceopen');
// master
@ -439,135 +441,15 @@ QUnit.test('resyncs SegmentLoader for a smooth quality change', function(assert)
// media
this.standardXHRResponse(this.requests.shift());
const segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
const originalResync = segmentLoader.resyncLoader;
segmentLoader.resyncLoader = function() {
resyncs++;
originalResync.call(segmentLoader);
};
this.masterPlaylistController.selectPlaylist = () => {
return this.masterPlaylistController.master().playlists[0];
};
this.masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 1, 'resynced the segmentLoader');
// verify stats
assert.equal(this.player.tech_.vhs.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test(
'does not resync the segmentLoader when no smooth quality change occurs',
function(assert) {
let resyncs = 0;
// master
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mediaSource.trigger('sourceopen');
const segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
const originalResync = segmentLoader.resyncLoader;
segmentLoader.resyncLoader = function() {
resyncs++;
originalResync.call(segmentLoader);
};
this.masterPlaylistController.fastQualityChange_ = () => fastQualityChangeCalls++;
this.masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 0, 'did not resync the segmentLoader');
// verify stats
assert.equal(this.player.tech_.vhs.stats.bandwidth, 4194304, 'default bandwidth');
}
);
QUnit.test('smooth quality change resyncs audio segment loader', function(assert) {
this.requests.length = 0;
this.player.dispose();
this.player = createPlayer();
this.player.src({
src: 'alternate-audio-multiple-groups.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
const masterPlaylistController = this.player.tech_.vhs.masterPlaylistController_;
masterPlaylistController.selectPlaylist = () => {
return masterPlaylistController.master().playlists[0];
};
// master
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
masterPlaylistController.mediaSource.trigger('sourceopen');
this.clock.tick(1);
this.player.audioTracks()[0].enabled = true;
let resyncs = 0;
let resets = 0;
const realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
resets++;
realReset.call(this);
};
const originalResync = masterPlaylistController.audioSegmentLoader_.resyncLoader;
masterPlaylistController.audioSegmentLoader_.resyncLoader = function() {
resyncs++;
originalResync.call(masterPlaylistController.audioSegmentLoader_);
};
masterPlaylistController.smoothQualityChange_();
assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same');
// force different media
masterPlaylistController.selectPlaylist = () => {
return masterPlaylistController.master().playlists[1];
};
this.masterPlaylistController.smoothQualityChange_();
assert.equal(this.requests.length, 3, 'three requests');
assert.ok(
this.requests[0].url.endsWith('eng/prog_index.m3u8'),
'requests eng playlist'
);
assert.ok(this.requests[1].url.endsWith('lo/main.mp4'), 'correct segment url');
assert.equal(
this.requests[1].requestHeaders.Range,
'bytes=0-603',
'requests init segment byte range'
);
assert.ok(this.requests[2].url.endsWith('lo/main.mp4'), 'correct segment url');
assert.equal(
this.requests[2].requestHeaders.Range,
'bytes=604-118754',
'requests segment byte range'
);
assert.notOk(this.requests[0].aborted, 'did not abort alt audio playlist request');
assert.notOk(this.requests[1].aborted, 'did not abort init request');
assert.notOk(this.requests[2].aborted, 'did not abort segment request');
masterPlaylistController.smoothQualityChange_();
assert.equal(this.requests.length, 4, 'added a request for new media');
assert.notOk(this.requests[0].aborted, 'did not abort alt audio playlist request');
assert.ok(this.requests[1].aborted, 'aborted init segment request');
assert.ok(this.requests[2].aborted, 'aborted segment request');
assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
// new media request
this.standardXHRResponse(this.requests[3]);
assert.equal(resyncs, 1, 'resyncs the audio segment loader when media changes');
assert.equal(resets, 0, 'does not reset the audio segment loader when media changes');
assert.equal(fastQualityChangeCalls, 1, 'called fastQualityChange_');
});
QUnit.test('resets everything for a fast quality change', function(assert) {
@ -995,11 +877,11 @@ QUnit.test('audio segment loader is reset on audio track change', function(asser
let resyncs = 0;
let resets = 0;
const realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
const realReset = masterPlaylistController.audioSegmentLoader_.resetEverything;
masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
masterPlaylistController.audioSegmentLoader_.resetEverything = function(done) {
resets++;
realReset.call(this);
realReset.call(this, done);
};
const originalResync = masterPlaylistController.audioSegmentLoader_.resyncLoader;
@ -1032,6 +914,7 @@ QUnit.test('audio segment loader is reset on audio track change', function(asser
assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
this.player.audioTracks()[1].enabled = true;
this.clock.tick(1);
assert.equal(this.requests.length, 4, 'added a request for new media');
assert.ok(this.requests[0].aborted, 'aborted old alt audio playlist request');
@ -4801,18 +4684,19 @@ QUnit.test('can pass or select a playlist for smoothQualityChange_', function(as
mpc.smoothQualityChange_(mpc.master().playlists[1]);
assert.deepEqual(calls, {
resetEverything: 0,
// should reset everything since smoothQualityChange_ calls fastQualityChange_
resetEverything: 1,
media: 1,
selectPlaylist: 0,
resyncLoader: 1
resyncLoader: 0
}, 'calls expected function when passed a playlist');
mpc.smoothQualityChange_();
assert.deepEqual(calls, {
resetEverything: 0,
resetEverything: 2,
media: 2,
selectPlaylist: 1,
resyncLoader: 2
resyncLoader: 0
}, 'calls expected function when not passed a playlist');
});

3
test/segment-loader.test.js

@ -4344,6 +4344,9 @@ QUnit.module('SegmentLoader', function(hooks) {
// smoothQualityChange will reset loader after changing renditions, so need to
// mimic that behavior here in order for content to be overlayed over already
// buffered content.
//
// Now that smoothQualityChange is removed, this behavior can be mimicked by
// calling resetLoader.
loader.resetLoader();
this.clock.tick(1);

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

@ -5389,7 +5389,7 @@ QUnit.test('stats are reset on dispose', function(assert) {
// mocking the fullscreenElement no longer works, find another way to mock
// fullscreen behavior(without user gesture)
QUnit.skip('detects fullscreen and triggers a smooth quality change', function(assert) {
QUnit.skip('detects fullscreen and triggers a fast quality change', function(assert) {
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
@ -5405,7 +5405,7 @@ QUnit.skip('detects fullscreen and triggers a smooth quality change', function(a
}
});
vhs.masterPlaylistController_.smoothQualityChange_ = function() {
vhs.masterPlaylistController_.fastQualityChange_ = function() {
qualityChanges++;
};
@ -5415,12 +5415,19 @@ QUnit.skip('detects fullscreen and triggers a smooth quality change', function(a
assert.equal(qualityChanges, 1, 'made a fast quality change');
let checkABRCalls = 0;
vhs.masterPlaylistController_.checkABR_ = () => checkABRCalls++;
// don't do a fast quality change when returning from fullscreen;
// allow the video element to rescale the already buffered video
//
// do check the current rendition to see if it should be changed for the next
// segment loaded
document[fullscreenElementName] = null;
Events.trigger(document, 'fullscreenchange');
assert.equal(qualityChanges, 1, 'did not make another quality change');
assert.equal(checkABRCalls, 1, 'called to check the ABR');
vhs.dispose();
});

Loading…
Cancel
Save