diff --git a/README.md b/README.md index d39293892..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) @@ -309,6 +310,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: diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index a96c5d870..8e4748ede 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'; const ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2; @@ -67,36 +68,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.`. @@ -285,7 +256,8 @@ export class MasterPlaylistController extends videojs.EventTarget { bandwidth, externHls, useCueTags, - blacklistDuration + blacklistDuration, + enableLowInitialPlaylist } = options; if (!url) { @@ -300,6 +272,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'); @@ -431,8 +404,17 @@ export class MasterPlaylistController extends videojs.EventTarget { let updatedPlaylist = this.masterPlaylistLoader_.media(); if (!updatedPlaylist) { - // select the initial variant - this.initialMedia_ = 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 3253a2004..0f54cd213 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 @@ -319,7 +320,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, @@ -355,3 +358,35 @@ 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. + * + * 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 + * exists pick the lowest audio rendition. + */ +export const lowestBitrateCompatibleVariantSelector = function() { + // 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)); + + // 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 = playlists.filter( + playlist => parseCodecs(playlist.attributes.CODECS).videoCodec + ); + + return playlistsWithVideo[0] || null; +}; 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..ce917560e 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -21,15 +21,11 @@ import PlaybackWatcher from './playback-watcher'; import reloadSourceOnError from './reload-source-on-error'; import { lastBandwidthSelector, + lowestBitrateCompatibleVariantSelector, comparePlaylistBandwidth, 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, @@ -39,12 +35,16 @@ const Hls = { utils, STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, + INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, comparePlaylistBandwidth, comparePlaylistResolution, xhr: xhrFactory() }; +// 0.5 MB/s +const INITIAL_BANDWIDTH = 4194304; + // Define getter/setters for config properites [ 'GOAL_BUFFER_LENGTH', @@ -279,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; + // grab options passed to player.src ['withCredentials', 'bandwidth'].forEach((option) => { if (typeof this.source_[option] !== 'undefined') { @@ -328,6 +331,8 @@ class HlsHandler extends Component { this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls.STANDARD_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_; this.mediaSource = this.masterPlaylistController_.mediaSource; diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index c26eb978e..ad8b39841 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -212,14 +212,61 @@ QUnit.test('resets SegmentLoader when seeking in flash for both in and out of bu }); +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({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_; + + let numCallsToSelectInitialPlaylistCalls = 0; + let numCallsToSelectPlaylist = 0; + + this.masterPlaylistController.selectPlaylist = () => { + numCallsToSelectPlaylist++; + return this.masterPlaylistController.master().playlists[0]; + }; + + this.masterPlaylistController.selectInitialPlaylist = () => { + numCallsToSelectInitialPlaylistCalls++; + return this.masterPlaylistController.master().playlists[0]; + }; + + this.masterPlaylistController.mediaSource.trigger('sourceopen'); + // master + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + this.clock.tick(1); + + assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist'); + assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist'); + + // Simulate a live reload + this.masterPlaylistController.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; + 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_; diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index 220bbb5e6..2fb31cab9 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -2,7 +2,8 @@ import { module, test } from 'qunit'; import { simpleSelector, movingAverageBandwidthSelector, - minRebufferMaxBandwidthSelector + minRebufferMaxBandwidthSelector, + lowestBitrateCompatibleVariantSelector } from '../src/playlist-selectors'; import Config from '../src/config'; @@ -123,6 +124,37 @@ function(assert) { 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(testPlaylist, expectedPlaylist, + 'Selected lowest compatible playlist with video assets'); + }); + +test('lowestBitrateCompatibleVariantSelector return null 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 testPlaylist = lowestBitrateCompatibleVariantSelector.call(this.hls); + + assert.equal(testPlaylist, null, + 'Returned null playlist since no video assets exist'); + }); + test('simpleSelector switches up even without resolution information', function(assert) { let master = this.hls.playlists.master; 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;