From ba8b1d114a6cd965583b5909648d3f9f2dc45c5a Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Wed, 26 Jul 2017 14:48:57 -0400 Subject: [PATCH 01/17] Select lowest rendition available --- src/master-playlist-controller.js | 33 ++---------------------------- src/playlist-selectors.js | 25 +++++++++++++++++++++++ src/util/codecs.js | 34 +++++++++++++++++++++++++++++++ src/videojs-contrib-hls.js | 6 ++++++ 4 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 src/util/codecs.js diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index a1c711cb7..c488c022a 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -12,6 +12,7 @@ import { translateLegacyCodecs } from 'videojs-contrib-media-sources/es5/codec-u import worker from 'webworkify'; import Decrypter from './decrypter-worker'; import Config from './config'; +import { parseCodecs } from './util/codecs.js'; let Hls; @@ -65,36 +66,6 @@ const objectChanged = function(a, b) { return false; }; -/** - * Parses a codec string to retrieve the number of codecs specified, - * the video codec and object type indicator, and the audio profile. - * - * @private - */ -const parseCodecs = function(codecs) { - let result = { - codecCount: 0 - }; - let parsed; - - result.codecCount = codecs.split(',').length; - result.codecCount = result.codecCount || 2; - - // parse the video codec - parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs); - if (parsed) { - result.videoCodec = parsed[2]; - result.videoObjectTypeIndicator = parsed[3]; - } - - // parse the last field of the audio codec - result.audioProfile = - (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs); - result.audioProfile = result.audioProfile && result.audioProfile[2]; - - return result; -}; - /** * Replace codecs in the codec string with the old apple-style `avc1.
.
` to the * standard `avc1.`. @@ -430,7 +401,7 @@ export class MasterPlaylistController extends videojs.EventTarget { if (!updatedPlaylist) { // select the initial variant - this.initialMedia_ = this.selectPlaylist(); + this.initialMedia_ = this.initialSelectPlaylist(); this.masterPlaylistLoader_.media(this.initialMedia_); return; } diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 888ec13b1..2068b4ee5 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -1,5 +1,6 @@ import Config from './config'; import Playlist from './playlist'; +import { parseCodecs } from './util/codecs.js'; // Utilities @@ -356,3 +357,27 @@ export const minRebufferMaxBandwidthSelector = function(settings) { return rebufferingEstimates[0] || null; }; + +/** + * Chooses the appropriate media playlist, which in this case is the lowest bitrate + * one with video. If no renditions with video exist, return the lowest audio rendition. + * + * @return {Object|null} + * {Object} return.playlist + * The lowest bitrate playlist that contains a video codec. If no such rendition + * exists pick the lowest audio rendition. + */ +export const lowestBitrateCompatibleVariantSelector = function() { + const { playlists } = this.playlists.master; + + const playlistsWithVideo = + playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec); + + // If there are any items in this playlist with a video codec, return + // the one with the lowest bitrate, otherwise return the lowest audio rendition + if (playlistsWithVideo.length !== 0) { + return playlistsWithVideo[0]; + } + + return playlists[0]; +}; diff --git a/src/util/codecs.js b/src/util/codecs.js new file mode 100644 index 000000000..ba4f48642 --- /dev/null +++ b/src/util/codecs.js @@ -0,0 +1,34 @@ + +/** + * @file - codecs.js - Handles tasks regarding codec strings such as translating them to + * codec strings, or translating codec strings into objects that can be examined. + */ + +/** + * Parses a codec string to retrieve the number of codecs specified, + * the video codec and object type indicator, and the audio profile. + */ + +export const parseCodecs = function(codecs = '') { + let result = { + codecCount: 0 + }; + let parsed; + + result.codecCount = codecs.split(',').length; + result.codecCount = result.codecCount || 2; + + // parse the video codec + parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs); + if (parsed) { + result.videoCodec = parsed[2]; + result.videoObjectTypeIndicator = parsed[3]; + } + + // parse the last field of the audio codec + result.audioProfile = + (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs); + result.audioProfile = result.audioProfile && result.audioProfile[2]; + + return result; +}; diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index 9f58f29b5..f686571b6 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -21,6 +21,7 @@ import PlaybackWatcher from './playback-watcher'; import reloadSourceOnError from './reload-source-on-error'; import { lastBandwidthSelector, + lowestBitrateCompatibleVariantSelector, comparePlaylistBandwidth, comparePlaylistResolution } from './playlist-selectors.js'; @@ -39,6 +40,7 @@ const Hls = { utils, STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, + INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, comparePlaylistBandwidth, comparePlaylistResolution, @@ -328,6 +330,10 @@ class HlsHandler extends Component { this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this); + this.masterPlaylistController_.initialSelectPlaylist = + this.selectPlaylist ? + this.selectPlaylist.bind(this) : Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); + // re-expose some internal objects for backwards compatibility with < v2 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; this.mediaSource = this.masterPlaylistController_.mediaSource; From 01bc92867e94ca4bc1953d759be745c9e2ad04f6 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Wed, 26 Jul 2017 17:37:00 -0400 Subject: [PATCH 02/17] Code review updates --- src/master-playlist-controller.js | 2 +- src/videojs-contrib-hls.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index c488c022a..161b59a42 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -401,7 +401,7 @@ export class MasterPlaylistController extends videojs.EventTarget { if (!updatedPlaylist) { // select the initial variant - this.initialMedia_ = this.initialSelectPlaylist(); + this.initialMedia_ = this.selectInitialPlaylist(); this.masterPlaylistLoader_.media(this.initialMedia_); return; } diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index f686571b6..f49eb76c6 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -330,9 +330,9 @@ class HlsHandler extends Component { this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this); - this.masterPlaylistController_.initialSelectPlaylist = - this.selectPlaylist ? - this.selectPlaylist.bind(this) : Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); + this.masterPlaylistController_.selectInitialPlaylist = + this.selectInitialPlaylist ? + this.selectInitialPlaylist.bind(this) : Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; From 6a5bd11efc220b1fcaee0c7c63ce7ca68b124a4c Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Wed, 26 Jul 2017 17:51:23 -0400 Subject: [PATCH 03/17] Sort playlist selector descending by bitrate --- src/playlist-selectors.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 2068b4ee5..83f945ee8 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -370,14 +370,14 @@ export const minRebufferMaxBandwidthSelector = function(settings) { export const lowestBitrateCompatibleVariantSelector = function() { const { playlists } = this.playlists.master; + // Sort ascending by bitrate + stableSort(playlists, + (a, b) => comparePlaylistBandwidth(a, b)); + const playlistsWithVideo = playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec); - // If there are any items in this playlist with a video codec, return - // the one with the lowest bitrate, otherwise return the lowest audio rendition - if (playlistsWithVideo.length !== 0) { - return playlistsWithVideo[0]; - } + const selectedPlaylists = playlistsWithVideo.length !== 0 ? playlistsWithVideo : playlists; - return playlists[0]; + return selectedPlaylists[0] || null; }; From 382ad9a3ac86c766e7c06e59fdebf437ded9d1e0 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Thu, 27 Jul 2017 14:39:58 -0400 Subject: [PATCH 04/17] Add option to enable low rendition selection, set to false by default --- src/master-playlist-controller.js | 10 ++++++++-- src/videojs-contrib-hls.js | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 161b59a42..784bf5782 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -254,7 +254,8 @@ export class MasterPlaylistController extends videojs.EventTarget { bandwidth, externHls, useCueTags, - blacklistDuration + blacklistDuration, + enableLowInitialPlaylist } = options; if (!url) { @@ -269,6 +270,7 @@ export class MasterPlaylistController extends videojs.EventTarget { this.mode_ = mode; this.useCueTags_ = useCueTags; this.blacklistDuration = blacklistDuration; + this.enableLowInitialPlaylist = enableLowInitialPlaylist; if (this.useCueTags_) { this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); @@ -401,7 +403,11 @@ export class MasterPlaylistController extends videojs.EventTarget { if (!updatedPlaylist) { // select the initial variant - this.initialMedia_ = this.selectInitialPlaylist(); + const initialPlaylistSelector = + this.enableLowInitialPlaylist ? + this.selectInitialPlaylist : this.selectPlaylist; + + this.initialMedia_ = initialPlaylistSelector(); this.masterPlaylistLoader_.media(this.initialMedia_); return; } diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index f49eb76c6..005562120 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -273,6 +273,7 @@ class HlsHandler extends Component { setOptions_() { // defaults this.options_.withCredentials = this.options_.withCredentials || false; + this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist || false; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; From 06d481482f6ff78d6f2bce9f572fb944133439c5 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Thu, 27 Jul 2017 16:58:20 -0400 Subject: [PATCH 05/17] Add test for triggering loadedplaylist --- test/master-playlist-controller.test.js | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index e800650a7..ec4c7609c 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -212,6 +212,50 @@ QUnit.test('resets SegmentLoader when seeking in flash for both in and out of bu }); +QUnit.only('selects lowest bitrate rendition when enableLowInitialPlaylist is set', + function(assert) { + this.player = createPlayer({ html5: { hls: { enableLowInitialPlaylist: true } } }); + + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_; + const masterPlaylistLoader = masterPlaylistController.masterPlaylistLoader_; + + let numCallsToSelectInitialPlaylistCalls = 0; + let numCallsToSelectPlaylist = 0; + + // master + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + // This shouldn't get called, but if it does we should track it so that + // we can catch this failure case. + masterPlaylistController.selectPlaylist = () => { + numCallsToSelectPlaylist++; + return masterPlaylistController.master().playlists[0]; + }; + + masterPlaylistController.selectInitialPlaylist = () => { + numCallsToSelectInitialPlaylistCalls++; + return masterPlaylistController.master().playlists[0]; + }; + + masterPlaylistController.mediaSource.trigger('sourceopen'); + + // Trigger playlist event which should utilize selectInitialPlaylist and + // not selectPlaylist + masterPlaylistLoader.trigger('loadedplaylist'); + + assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); + assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); + }); + QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) { let resyncs = 0; From b4a89b2e23758150deb960aefe98c6cfdd82cff1 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Fri, 28 Jul 2017 15:12:44 -0400 Subject: [PATCH 06/17] Update test so that it checks live reload case and works correctly --- src/master-playlist-controller.js | 6 ++-- src/playlist-selectors.js | 6 ++-- test/master-playlist-controller.test.js | 40 ++++++++++++++----------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 784bf5782..260cd1d7f 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -403,11 +403,9 @@ export class MasterPlaylistController extends videojs.EventTarget { if (!updatedPlaylist) { // select the initial variant - const initialPlaylistSelector = - this.enableLowInitialPlaylist ? - this.selectInitialPlaylist : this.selectPlaylist; + this.initialMedia_ = this.enableLowInitialPlaylist ? + this.selectInitialPlaylist() : this.selectPlaylist(); - this.initialMedia_ = initialPlaylistSelector(); this.masterPlaylistLoader_.media(this.initialMedia_); return; } diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 83f945ee8..e9d212667 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -368,7 +368,7 @@ export const minRebufferMaxBandwidthSelector = function(settings) { * exists pick the lowest audio rendition. */ export const lowestBitrateCompatibleVariantSelector = function() { - const { playlists } = this.playlists.master; + const playlists = this.playlists.master.playlists.slice(); // Sort ascending by bitrate stableSort(playlists, @@ -377,7 +377,5 @@ export const lowestBitrateCompatibleVariantSelector = function() { const playlistsWithVideo = playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec); - const selectedPlaylists = playlistsWithVideo.length !== 0 ? playlistsWithVideo : playlists; - - return selectedPlaylists[0] || null; + return playlistsWithVideo[0] || playlists[0]; }; diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index ec4c7609c..fc9a3d55e 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -212,8 +212,11 @@ QUnit.test('resets SegmentLoader when seeking in flash for both in and out of bu }); -QUnit.only('selects lowest bitrate rendition when enableLowInitialPlaylist is set', +QUnit.test('selects lowest bitrate rendition when enableLowInitialPlaylist is set', function(assert) { + // Set requests.length to 0, otherwise it will use the requests generated in the + // beforeEach function + this.requests.length = 0; this.player = createPlayer({ html5: { hls: { enableLowInitialPlaylist: true } } }); this.player.src({ @@ -223,34 +226,37 @@ QUnit.only('selects lowest bitrate rendition when enableLowInitialPlaylist is se this.clock.tick(1); - const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_; - const masterPlaylistLoader = masterPlaylistController.masterPlaylistLoader_; + this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_; let numCallsToSelectInitialPlaylistCalls = 0; let numCallsToSelectPlaylist = 0; - // master - this.standardXHRResponse(this.requests.shift()); - // media - this.standardXHRResponse(this.requests.shift()); - - // This shouldn't get called, but if it does we should track it so that - // we can catch this failure case. - masterPlaylistController.selectPlaylist = () => { + this.masterPlaylistController.selectPlaylist = () => { numCallsToSelectPlaylist++; - return masterPlaylistController.master().playlists[0]; + return this.masterPlaylistController.master().playlists[0]; }; - masterPlaylistController.selectInitialPlaylist = () => { + this.masterPlaylistController.selectInitialPlaylist = () => { numCallsToSelectInitialPlaylistCalls++; - return masterPlaylistController.master().playlists[0]; + return this.masterPlaylistController.master().playlists[0]; }; - masterPlaylistController.mediaSource.trigger('sourceopen'); + this.masterPlaylistController.mediaSource.trigger('sourceopen'); + // master + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + this.clock.tick(1); // Trigger playlist event which should utilize selectInitialPlaylist and // not selectPlaylist - masterPlaylistLoader.trigger('loadedplaylist'); + assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); + assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); + + // Simulate a live reload + this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist'); + this.clock.tick(5); assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); @@ -259,11 +265,11 @@ QUnit.only('selects lowest bitrate rendition when enableLowInitialPlaylist is se QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) { let resyncs = 0; + this.masterPlaylistController.mediaSource.trigger('sourceopen'); // master this.standardXHRResponse(this.requests.shift()); // media this.standardXHRResponse(this.requests.shift()); - this.masterPlaylistController.mediaSource.trigger('sourceopen'); let segmentLoader = this.masterPlaylistController.mainSegmentLoader_; From a2ca2fa309b367c806fbda41dfadbda40c167d9c Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Fri, 28 Jul 2017 15:56:36 -0400 Subject: [PATCH 07/17] Code review updates --- src/playlist-selectors.js | 2 ++ test/master-playlist-controller.test.js | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index e9d212667..7c0b172f1 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -362,6 +362,8 @@ export const minRebufferMaxBandwidthSelector = function(settings) { * Chooses the appropriate media playlist, which in this case is the lowest bitrate * one with video. If no renditions with video exist, return the lowest audio rendition. * + * Expects to be called within the context of an instance of HlsHandler + * * @return {Object|null} * {Object} return.playlist * The lowest bitrate playlist that contains a video codec. If no such rendition diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index fc9a3d55e..51efaf85f 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -249,14 +249,11 @@ QUnit.test('selects lowest bitrate rendition when enableLowInitialPlaylist is se this.clock.tick(1); - // Trigger playlist event which should utilize selectInitialPlaylist and - // not selectPlaylist assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); // Simulate a live reload this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist'); - this.clock.tick(5); assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); From 8ccd7e2a0e029fa2b7bdddaa6e1837f7c57e1c51 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Mon, 31 Jul 2017 14:02:28 -0400 Subject: [PATCH 08/17] Add tests that check lowest bitrate rendition selection algorithm --- src/videojs-contrib-hls.js | 3 ++- test/playlist-selectors.test.js | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index 005562120..d49159f8c 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -273,7 +273,8 @@ class HlsHandler extends Component { setOptions_() { // defaults this.options_.withCredentials = this.options_.withCredentials || false; - this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist || false; + this.options_.enableLowInitialPlaylist = + this.options_.enableLowInitialPlaylist || false; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index a3aab164c..f190864fd 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -1,7 +1,8 @@ import { module, test } from 'qunit'; import { movingAverageBandwidthSelector, - minRebufferMaxBandwidthSelector + minRebufferMaxBandwidthSelector, + lowestBitrateCompatibleVariantSelector } from '../src/playlist-selectors'; import Config from '../src/config'; @@ -121,3 +122,35 @@ function(assert) { assert.equal(result.playlist, master.playlists[0], 'selected the correct playlist'); assert.equal(result.rebufferingImpact, 1, 'impact on rebuffering is 1 second'); }); + +test('lowestBitrateCompatibleVariantSelector picks lowest non-audio playlist', + function(assert) { + // Set this up out of order to make sure that the function sorts all + // playlists by bandwidth + this.hls.playlists.master.playlists = [ + { attributes: { BANDWIDTH: 10, CODECS: 'mp4a.40.2' } }, + { attributes: { BANDWIDTH: 100, CODECS: 'mp4a.40.2, avc1.4d400d' } }, + { attributes: { BANDWIDTH: 50, CODECS: 'mp4a.40.2, avc1.4d400d' } } + ]; + + const expectedPlaylist = this.hls.playlists.master.playlists[2]; + const testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); + + assert.equal(expectedPlaylist, testPlaylist, + 'Selected lowest compatible playlist with video assets'); + }); + +test('lowestBitrateCompatibleVariantSelector picks lowest audio rendition if no video exists', + function(assert) { + this.hls.playlists.master.playlists = [ + { attributes: { BANDWIDTH: 50, CODECS: 'mp4a.40.2' } }, + { attributes: { BANDWIDTH: 10, CODECS: 'mp4a.40.2' } }, + { attributes: { BANDWIDTH: 100, CODECS: 'mp4a.40.2' } } + ]; + + const expectedPlaylist = this.hls.playlists.master.playlists[1]; + const testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); + + assert.equal(expectedPlaylist, testPlaylist, + 'Selected lowest compatible playlist since no video assets exist'); + }); From da8b0c5da41e706e2221c374e0a03d36fb25ae65 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 1 Aug 2017 15:17:51 -0400 Subject: [PATCH 09/17] Add enableLowInitialPlaylist to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d39293892..f307297bb 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,14 @@ When the `bandwidth` property is set (bits per second), it will be used in the calculation for initial playlist selection, before more bandwidth information is seen by the player. +##### enableLowInitialPlaylist +* Type: `boolean` +* can be used as an initialization option + +When `enableLowInitialPlaylist` is set to true, it will be used to select +the lowest bitrate playlist initially. This helps to decrease playback start time. +This setting is `false` by default. + ### 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: From 362c2461cc15636338c8b0149e7d32e4ace61b9e Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Wed, 2 Aug 2017 11:33:57 -0400 Subject: [PATCH 10/17] Additional link to enableLowIinitalPlaylist --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f307297bb..b9da427fb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Maintenance Status: Stable - [overrideNative](#overridenative) - [blacklistDuration](#blacklistduration) - [bandwidth](#bandwidth) + - [enableLowInitialPlaylist](#enablelowinitialplaylist) - [Runtime Properties](#runtime-properties) - [hls.playlists.master](#hlsplaylistsmaster) - [hls.playlists.media](#hlsplaylistsmedia) From 4f431fe8ce21de6e1a473f6ea8023d991cbdc073 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Thu, 3 Aug 2017 16:59:56 -0400 Subject: [PATCH 11/17] Fix issues with codec-less but valid manifests --- src/master-playlist-controller.js | 13 ++++++++++--- src/playlist-selectors.js | 2 +- src/videojs-contrib-hls.js | 21 ++++++++++----------- test/playlist-selectors.test.js | 5 ++--- test/videojs-contrib-hls.test.js | 5 +++-- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 6abf40ad9..77f1e83ff 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -402,10 +402,17 @@ export class MasterPlaylistController extends videojs.EventTarget { let updatedPlaylist = this.masterPlaylistLoader_.media(); if (!updatedPlaylist) { - // select the initial variant - this.initialMedia_ = this.enableLowInitialPlaylist ? - this.selectInitialPlaylist() : this.selectPlaylist(); + let selectedMedia; + if (this.enableLowInitialPlaylist) { + selectedMedia = this.selectInitialPlaylist(); + } + + if (!selectedMedia) { + selectedMedia = this.selectPlaylist(); + } + + this.initialMedia_ = selectedMedia; this.masterPlaylistLoader_.media(this.initialMedia_); return; } diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 78362038e..fb01c265f 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -375,5 +375,5 @@ export const lowestBitrateCompatibleVariantSelector = function() { const playlistsWithVideo = playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec); - return playlistsWithVideo[0] || playlists[0]; + return playlistsWithVideo[0] || null; }; diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index d49159f8c..a6ecf3c55 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -26,11 +26,6 @@ import { comparePlaylistResolution } from './playlist-selectors.js'; -// 0.5 MB/s -const INITIAL_BANDWIDTH_DESKTOP = 4194304; -// 0.0625 MB/s -const INITIAL_BANDWIDTH_MOBILE = 500000; - const Hls = { PlaylistLoader, Playlist, @@ -47,6 +42,9 @@ const Hls = { xhr: xhrFactory() }; +// 0.5 MB/s +const INITIAL_BANDWIDTH = 4194304; + // Define getter/setters for config properites [ 'GOAL_BUFFER_LENGTH', @@ -273,8 +271,6 @@ class HlsHandler extends Component { setOptions_() { // defaults this.options_.withCredentials = this.options_.withCredentials || false; - this.options_.enableLowInitialPlaylist = - this.options_.enableLowInitialPlaylist || false; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; @@ -283,12 +279,15 @@ class HlsHandler extends Component { // start playlist selection at a reasonable bandwidth for // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s) if (typeof this.options_.bandwidth !== 'number') { - // only use Android for mobile because iOS does not support MSE (and uses - // native HLS) - this.options_.bandwidth = - videojs.browser.IS_ANDROID ? INITIAL_BANDWIDTH_MOBILE : INITIAL_BANDWIDTH_DESKTOP; + this.options_.bandwidth = INITIAL_BANDWIDTH; } + // If the bandwidth number is unchanged from the initial setting + // then this takes precedence over the enableLowInitialPlaylist option + this.options_.enableLowInitialPlaylist = + (this.options_.enableLowInitialPlaylist && + this.options_.bandwidth === INITIAL_BANDWIDTH) || false; + // grab options passed to player.src ['withCredentials', 'bandwidth'].forEach((option) => { if (typeof this.source_[option] !== 'undefined') { diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index f190864fd..5dd1f8549 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -136,7 +136,7 @@ test('lowestBitrateCompatibleVariantSelector picks lowest non-audio playlist', const expectedPlaylist = this.hls.playlists.master.playlists[2]; const testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); - assert.equal(expectedPlaylist, testPlaylist, + assert.equal(testPlaylist, expectedPlaylist, 'Selected lowest compatible playlist with video assets'); }); @@ -148,9 +148,8 @@ test('lowestBitrateCompatibleVariantSelector picks lowest audio rendition if no { attributes: { BANDWIDTH: 100, CODECS: 'mp4a.40.2' } } ]; - const expectedPlaylist = this.hls.playlists.master.playlists[1]; const testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); - assert.equal(expectedPlaylist, testPlaylist, + assert.equal(testPlaylist, null, 'Selected lowest compatible playlist since no video assets exist'); }); diff --git a/test/videojs-contrib-hls.test.js b/test/videojs-contrib-hls.test.js index 5610442d6..88e7c8046 100644 --- a/test/videojs-contrib-hls.test.js +++ b/test/videojs-contrib-hls.test.js @@ -1291,6 +1291,7 @@ QUnit.test('playlist 404 should blacklist media', function(assert) { // continue loading the final remaining playlist after it wasn't blacklisted // when half the segment duaration passed assert.strictEqual(4, this.requests.length, 'one more request was made'); + assert.strictEqual(this.requests[3].url, absoluteUrl('manifest/media1.m3u8'), 'media playlist requested'); @@ -1756,7 +1757,7 @@ QUnit.test('uses default bandwidth option if non-numerical value provided', func assert.equal(this.player.tech_.hls.bandwidth, 4194304, 'set bandwidth to default'); }); -QUnit.test('uses mobile default bandwidth if browser is Android', function(assert) { +QUnit.test('uses default bandwidth if browser is Android', function(assert) { this.player.dispose(); const origIsAndroid = videojs.browser.IS_ANDROID; @@ -1786,7 +1787,7 @@ QUnit.test('uses mobile default bandwidth if browser is Android', function(asser openMediaSource(this.player, this.clock); assert.equal(this.player.tech_.hls.bandwidth, - 500000, + 4194304, 'set bandwidth to mobile default'); videojs.browser.IS_ANDROID = origIsAndroid; From 8afae790c6b00401e0cd9b74363fc5961acffce5 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 8 Aug 2017 10:49:11 -0400 Subject: [PATCH 12/17] Handle code review comment to take care of disabled playlists --- src/playlist-selectors.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index fb01c265f..28d15e9ae 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -372,8 +372,20 @@ export const lowestBitrateCompatibleVariantSelector = function() { stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); - const playlistsWithVideo = - playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec); + // filter out any playlists that have been excluded due to + // incompatible configurations or playback errors + const enabledPlaylists = playlists.filter( + rep => Playlist.isEnabled(rep.playlist) + ); + + // Parse and assume that playlists with no video codec have no video + // (this is not necessarily true, although it is generally true). + // + // If an entire manifest has no valid videos everything will get filtered + // out. + const playlistsWithVideo = enabledPlaylists.filter( + playlist => parseCodecs(playlist.attributes.CODECS).videoCodec + ); return playlistsWithVideo[0] || null; }; From 024f74aa037c1629fc23a0f878f49edeffdcac61 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 8 Aug 2017 11:04:43 -0400 Subject: [PATCH 13/17] Fix copy/paste error --- src/playlist-selectors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 28d15e9ae..919ce433b 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -375,7 +375,7 @@ export const lowestBitrateCompatibleVariantSelector = function() { // filter out any playlists that have been excluded due to // incompatible configurations or playback errors const enabledPlaylists = playlists.filter( - rep => Playlist.isEnabled(rep.playlist) + playlist => Playlist.isEnabled(playlist) ); // Parse and assume that playlists with no video codec have no video From c51469a8ccf6a3c641acd3de82e42cf9f4304667 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 8 Aug 2017 13:35:08 -0400 Subject: [PATCH 14/17] Code review updates --- src/playlist-selectors.js | 4 +--- src/videojs-contrib-hls.js | 8 +++----- test/playlist-selectors.test.js | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 919ce433b..0bac0add8 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -374,9 +374,7 @@ export const lowestBitrateCompatibleVariantSelector = function() { // filter out any playlists that have been excluded due to // incompatible configurations or playback errors - const enabledPlaylists = playlists.filter( - playlist => Playlist.isEnabled(playlist) - ); + const enabledPlaylists = playlists.filter(Playlist.isEnabled); // Parse and assume that playlists with no video codec have no video // (this is not necessarily true, although it is generally true). diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index a6ecf3c55..ce917560e 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -285,8 +285,8 @@ class HlsHandler extends Component { // If the bandwidth number is unchanged from the initial setting // then this takes precedence over the enableLowInitialPlaylist option this.options_.enableLowInitialPlaylist = - (this.options_.enableLowInitialPlaylist && - this.options_.bandwidth === INITIAL_BANDWIDTH) || false; + this.options_.enableLowInitialPlaylist && + this.options_.bandwidth === INITIAL_BANDWIDTH; // grab options passed to player.src ['withCredentials', 'bandwidth'].forEach((option) => { @@ -331,9 +331,7 @@ class HlsHandler extends Component { this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this); - this.masterPlaylistController_.selectInitialPlaylist = - this.selectInitialPlaylist ? - this.selectInitialPlaylist.bind(this) : Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); + this.masterPlaylistController_.selectInitialPlaylist = Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index 5dd1f8549..2a44477c5 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -140,7 +140,7 @@ test('lowestBitrateCompatibleVariantSelector picks lowest non-audio playlist', 'Selected lowest compatible playlist with video assets'); }); -test('lowestBitrateCompatibleVariantSelector picks lowest audio rendition if no video exists', +test('lowestBitrateCompatibleVariantSelector return null if no video exists', function(assert) { this.hls.playlists.master.playlists = [ { attributes: { BANDWIDTH: 50, CODECS: 'mp4a.40.2' } }, @@ -151,5 +151,5 @@ test('lowestBitrateCompatibleVariantSelector picks lowest audio rendition if no const testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); assert.equal(testPlaylist, null, - 'Selected lowest compatible playlist since no video assets exist'); + 'Returned null playlist since no video assets exist'); }); From c08c11483d4c23495f6eeec83307b5ce028fed59 Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 8 Aug 2017 17:01:48 -0400 Subject: [PATCH 15/17] Add logic to minRebufferMaxBandwidthSelector to ignore blacklisted playlists --- src/playlist-selectors.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 0bac0add8..7a2a3696e 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -317,7 +317,9 @@ export const minRebufferMaxBandwidthSelector = function(settings) { } = settings; const bandwidthPlaylists = - master.playlists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); + master.playlists.filter(playlist => + Playlist.isEnabled(playlist) && Playlist.hasAttribute('BANDWIDTH', playlist) + ); const rebufferingEstimates = bandwidthPlaylists.map((playlist) => { const syncPoint = syncController.getSyncPoint(playlist, From 4139386c1966f47e690fff330ba3e4c81d82d75d Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Tue, 8 Aug 2017 17:27:13 -0400 Subject: [PATCH 16/17] Remove slice in favor of an existing filter --- src/playlist-selectors.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 7a2a3696e..319cb440a 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -368,22 +368,20 @@ export const minRebufferMaxBandwidthSelector = function(settings) { * exists pick the lowest audio rendition. */ export const lowestBitrateCompatibleVariantSelector = function() { - const playlists = this.playlists.master.playlists.slice(); + // filter out any playlists that have been excluded due to + // incompatible configurations or playback errors + const playlists = this.playlists.master.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); - // filter out any playlists that have been excluded due to - // incompatible configurations or playback errors - const enabledPlaylists = playlists.filter(Playlist.isEnabled); - // Parse and assume that playlists with no video codec have no video // (this is not necessarily true, although it is generally true). // // If an entire manifest has no valid videos everything will get filtered // out. - const playlistsWithVideo = enabledPlaylists.filter( + const playlistsWithVideo = playlists.filter( playlist => parseCodecs(playlist.attributes.CODECS).videoCodec ); From 39f8e444023b5858cd0d77d0c89d968f811c9f4d Mon Sep 17 00:00:00 2001 From: Oshin Karamian Date: Wed, 16 Aug 2017 14:03:32 -0400 Subject: [PATCH 17/17] Add back newline after merge conflict --- test/playlist-selectors.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index 00620f578..2fb31cab9 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -166,4 +166,4 @@ test('simpleSelector switches up even without resolution information', function( const selectedPlaylist = simpleSelector(master, 2000, 1, 1); assert.equal(selectedPlaylist, master.playlists[1], 'selected the correct playlist'); -}); \ No newline at end of file +});