Skip to content

Commit

Permalink
feat: do fast rendition changes on fullscreen changes and user actions (
Browse files Browse the repository at this point in the history
#1074)

Deprecate smoothQualityChange_ on the MPC, but otherwise, always do fast quality change whenever a user requests a rendition change and on fullscreen change
  • Loading branch information
gesinger committed Jun 9, 2021
1 parent 3124fbc commit 5405c18
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 159 deletions.
14 changes: 5 additions & 9 deletions README.md
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion scripts/index-demo-page.js
Expand Up @@ -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"]');
Expand Down
10 changes: 2 additions & 8 deletions src/master-playlist-controller.js
Expand Up @@ -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);
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/videojs-http-streaming.js
Expand Up @@ -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_();
}
});

Expand Down Expand Up @@ -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(
Expand Down
5 changes: 0 additions & 5 deletions test/configuration.test.js
Expand Up @@ -36,11 +36,6 @@ const options = [{
default: 4194304,
test: 5,
alt: 555
}, {
name: 'smoothQualityChange',
default: false,
test: true,
alt: false
}, {
name: 'useBandwidthFromLocalStorage',
default: false,
Expand Down
148 changes: 16 additions & 132 deletions test/master-playlist-controller.test.js
Expand Up @@ -430,144 +430,26 @@ 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
this.standardXHRResponse(this.requests.shift());
// 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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
});

Expand Down
3 changes: 3 additions & 0 deletions test/segment-loader.test.js
Expand Up @@ -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);

Expand Down
13 changes: 10 additions & 3 deletions test/videojs-http-streaming.test.js
Expand Up @@ -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'
Expand All @@ -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++;
};

Expand All @@ -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();
});

Expand Down

0 comments on commit 5405c18

Please sign in to comment.