diff --git a/README.md b/README.md index e420d1a7f..ae7d46e16 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Video.js Compatibility: 6.0, 7.0 - [blacklistDuration](#blacklistduration) - [bandwidth](#bandwidth) - [enableLowInitialPlaylist](#enablelowinitialplaylist) + - [limitRenditionByPlayerDimensions](#limitrenditionbyplayerdimensions) + - [smoothQualityChange](#smoothqualitychange) - [Runtime Properties](#runtime-properties) - [hls.playlists.master](#hlsplaylistsmaster) - [hls.playlists.media](#hlsplaylistsmedia) @@ -342,6 +344,22 @@ selection logic will take into account the player size and rendition resolutions when making a decision. This setting is `true` by default. +##### smoothQualityChange +* 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](#hlsrepresentations) 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. + +Note that this _only_ affects quality changes triggered via the representations +API; automatic quality switches based on available bandwidth will always be +smooth switches. + ### Runtime Properties Runtime properties are attached to the tech object when HLS is in use. You can get a reference to the HLS source handler like this: diff --git a/src/rendition-mixin.js b/src/rendition-mixin.js index e1b701f18..7b77edfd2 100644 --- a/src/rendition-mixin.js +++ b/src/rendition-mixin.js @@ -48,11 +48,13 @@ const enableFunction = (loader, playlistUri, changePlaylistFn) => (enable) => { */ class Representation { constructor(hlsHandler, playlist, id) { - // Get a reference to a bound version of fastQualityChange_ - let fastChangeFunction = hlsHandler - .masterPlaylistController_ - .fastQualityChange_ - .bind(hlsHandler.masterPlaylistController_); + const { + masterPlaylistController_: mpc, + options_: { smoothQualityChange } + } = hlsHandler; + // Get a reference to a bound version of the quality change function + const changeType = smoothQualityChange ? 'smooth' : 'fast'; + const qualityChangeFunction = mpc[`${changeType}QualityChange_`].bind(mpc); // some playlist attributes are optional if (playlist.attributes.RESOLUTION) { @@ -72,7 +74,7 @@ class Representation { // specific variant this.enabled = enableFunction(hlsHandler.playlists, playlist.uri, - fastChangeFunction); + qualityChangeFunction); } } diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index dfa98d48c..b89cf8a2a 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -341,6 +341,7 @@ class HlsHandler extends Component { // defaults this.options_.withCredentials = this.options_.withCredentials || false; this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true; + this.options_.smoothQualityChange = this.options_.smoothQualityChange || false; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; @@ -359,7 +360,7 @@ class HlsHandler extends Component { this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src - ['withCredentials', 'limitRenditionByPlayerDimensions', 'bandwidth'].forEach((option) => { + ['withCredentials', 'limitRenditionByPlayerDimensions', 'bandwidth', 'smoothQualityChange'].forEach((option) => { if (typeof this.source_[option] !== 'undefined') { this.options_[option] = this.source_[option]; } diff --git a/test/configuration.test.js b/test/configuration.test.js index e85110fba..645ef256b 100644 --- a/test/configuration.test.js +++ b/test/configuration.test.js @@ -33,6 +33,11 @@ const options = [{ default: 4194304, test: 5, alt: 555 +}, { + name: 'smoothQualityChange', + default: false, + test: true, + alt: false }]; const CONFIG_KEYS = Object.keys(Config); diff --git a/test/rendition-mixin.test.js b/test/rendition-mixin.test.js index bfa9a0f52..60a320dde 100644 --- a/test/rendition-mixin.test.js +++ b/test/rendition-mixin.test.js @@ -41,17 +41,22 @@ const makeMockPlaylist = function(options) { return playlist; }; -const makeMockHlsHandler = function(playlistOptions) { +const makeMockHlsHandler = function(playlistOptions, handlerOptions) { let mcp = { fastQualityChange_: () => { mcp.fastQualityChange_.calls++; + }, + smoothQualityChange_: () => { + mcp.smoothQualityChange_.calls++; } }; mcp.fastQualityChange_.calls = 0; + mcp.smoothQualityChange_.calls = 0; let hlsHandler = { - masterPlaylistController_: mcp + masterPlaylistController_: mcp, + options_: handlerOptions || {} }; hlsHandler.playlists = new videojs.EventTarget(); @@ -222,7 +227,7 @@ function(assert) { 'excludeUntil not touched when disabling a rendition'); }); -QUnit.test('changing the enabled state of a representation calls fastQualityChange_', +QUnit.test('changing the enabled state of a representation calls fastQualityChange_ by default', function(assert) { let renditionEnabledEvents = 0; let hlsHandler = makeMockHlsHandler([ @@ -260,3 +265,45 @@ function(assert) { assert.equal(mpc.fastQualityChange_.calls, 2, 'fastQualityChange_ was called twice'); }); + +QUnit.test('changing the enabled state of a representation calls smoothQualityChange_ ' + + 'when the flag is set', +function(assert) { + let renditionEnabledEvents = 0; + let hlsHandler = makeMockHlsHandler([ + { + bandwidth: 0, + disabled: true, + uri: 'media0.m3u8' + }, + { + bandwidth: 0, + uri: 'media1.m3u8' + } + ], { + smoothQualityChange: true + }); + let mpc = hlsHandler.masterPlaylistController_; + + hlsHandler.playlists.on('renditionenabled', function() { + renditionEnabledEvents++; + }); + + RenditionMixin(hlsHandler); + + let renditions = hlsHandler.representations(); + + assert.equal(mpc.smoothQualityChange_.calls, 0, 'smoothQualityChange_ was never called'); + assert.equal(renditionEnabledEvents, 0, + 'renditionenabled event has not been triggered'); + + renditions[0].enabled(true); + + assert.equal(mpc.smoothQualityChange_.calls, 1, 'smoothQualityChange_ was called once'); + assert.equal(renditionEnabledEvents, 1, + 'renditionenabled event has been triggered once'); + + renditions[1].enabled(false); + + assert.equal(mpc.smoothQualityChange_.calls, 2, 'smoothQualityChange_ was called twice'); +});