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;