b6ad6cc9 by Jon-Carlos Rivera

Manual rendition selection API (#723)

* Adds a manual rendition selection api to an instance of HlsHandler
* Update the doc with information about the representation api
1 parent 49e02ea7
......@@ -31,6 +31,7 @@ Play back HLS with video.js, even where it's not natively supported.
- [hls.bandwidth](#hlsbandwidth)
- [hls.bytesReceived](#hlsbytesreceived)
- [hls.selectPlaylist](#hlsselectplaylist)
- [hls.representations](#hlsrepresentations)
- [hls.xhr](#hlsxhr)
- [Events](#events)
- [loadedmetadata](#loadedmetadata)
......@@ -278,6 +279,34 @@ segment is downloaded. You can override this function to provide your
adaptive streaming logic. You must, however, be sure to return a valid
media playlist object that is present in `player.hls.master`.
Overridding this function with your own is very powerful but is overkill
for many purposes. Most of the time, you should use the much simpler
function below to selectively enable or disable a playlist from the
adaptive streaming logic.
#### hls.representations
Type: `function`
To get all of the available representations, call the `representations()` method on `player.hls`. This will return a list of plain objects, each with `width`, `height`, `bandwidth`, and `id` properties, and an `enabled()` method.
```javascript
player.hls.representations();
```
To see whether the representation is enabled or disabled, call its `enabled()` method with no arguments. To set whether it is enabled/disabled, call its `enabled()` method and pass in a boolean value. Calling `<representation>.enabled(true)` will allow the adaptive bitrate algorithm to select the representation while calling `<representation>.enabled(false)` will disallow any selection of that representation.
Example, only enabling representations with a width greater than or equal to 720:
```javascript
player.hls.representations().forEach(function(rep) {
if (rep.width >= 720) {
rep.enabled(true);
} else {
rep.enabled(false);
}
});
```
#### hls.xhr
Type: `function`
......
/**
* Enable/disable playlist function. It is intended to have the first two
* arguments partially-applied in order to create the final per-playlist
* function.
*
* @param {PlaylistLoader} playlist - The rendition or media-playlist
* @param {Function} changePlaylistFn - A function to be called after a
* playlist's enabled-state has been changed. Will NOT be called if a
* playlist's enabled-state is unchanged
* @param {Boolean=} enable - Value to set the playlist enabled-state to
* or if undefined returns the current enabled-state for the playlist
* @return {Boolean} The current enabled-state of the playlist
*/
let enableFunction = (playlist, changePlaylistFn, enable) => {
let currentlyEnabled = typeof playlist.excludeUntil === 'undefined' ||
playlist.excludeUntil <= Date.now();
if (typeof enable === 'undefined') {
return currentlyEnabled;
}
if (enable !== currentlyEnabled) {
if (enable) {
delete playlist.excludeUntil;
} else {
playlist.excludeUntil = Infinity;
}
// Ensure the outside world knows about our changes
changePlaylistFn();
}
return enable;
};
/**
* The representation object encapsulates the publicly visible information
* in a media playlist along with a setter/getter-type function (enabled)
* for changing the enabled-state of a particular playlist entry
*
* @class Representation
*/
class Representation {
constructor(hlsHandler, playlist, id) {
// Get a reference to a bound version of fastQualityChange_
let fastChangeFunction = hlsHandler
.masterPlaylistController_
.fastQualityChange_
.bind(hlsHandler.masterPlaylistController_);
// Carefully descend into the playlist's attributes since most
// properties are optional
if (playlist.attributes) {
let attributes = playlist.attributes;
if (attributes.RESOLUTION) {
let resolution = attributes.RESOLUTION;
this.width = resolution.width;
this.height = resolution.height;
}
this.bandwidth = attributes.BANDWIDTH;
}
// The id is simply the ordinality of the media playlist
// within the master playlist
this.id = id;
// Partially-apply the enableFunction to create a playlist-
// specific variant
this.enabled = enableFunction.bind(this, playlist, fastChangeFunction);
}
}
/**
* A mixin function that adds the `representations` api to an instance
* of the HlsHandler class
* @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the
* representation API into
*/
let renditionSelectionMixin = function(hlsHandler) {
let playlists = hlsHandler.playlists;
// Add a single API-specific function to the HlsHandler instance
hlsHandler.representations = () => {
return playlists
.master
.playlists
.map((e, i) => new Representation(hlsHandler, e, i));
};
};
export default renditionSelectionMixin;
......@@ -15,6 +15,7 @@ import m3u8 from './m3u8';
import videojs from 'video.js';
import MasterPlaylistController from './master-playlist-controller';
import Config from './config';
import renditionSelectionMixin from './rendition-mixin';
/**
* determine if an object a is differnt from
......@@ -475,6 +476,9 @@ class HlsHandler extends Component {
this.masterPlaylistController_.audioTracks_.forEach((track) => {
this.tech_.audioTracks().addTrack(track);
});
// Add the manual rendition mix-in to HlsHandler
renditionSelectionMixin(this);
});
// the bandwidth of the primary segment loader is our best
......
/* eslint-disable max-len */
import QUnit from 'qunit';
import RenditionMixin from '../src/rendition-mixin.js';
const makeMockPlaylist = function(options) {
options = options || {};
let playlist = {
segments: []
};
if ('bandwidth' in options) {
playlist.attributes = playlist.attributes || {};
playlist.attributes.BANDWIDTH = options.bandwidth;
}
if ('width' in options) {
playlist.attributes = playlist.attributes || {};
playlist.attributes.RESOLUTION = playlist.attributes.RESOLUTION || {};
playlist.attributes.RESOLUTION.width = options.width;
}
if ('height' in options) {
playlist.attributes = playlist.attributes || {};
playlist.attributes.RESOLUTION = playlist.attributes.RESOLUTION || {};
playlist.attributes.RESOLUTION.height = options.height;
}
if ('excludeUntil' in options) {
playlist.excludeUntil = options.excludeUntil;
}
return playlist;
};
const makeMockHlsHandler = function(playlistOptions) {
let mcp = {
fastQualityChange_: () => {
mcp.fastQualityChange_.calls++;
}
};
mcp.fastQualityChange_.calls = 0;
let hlsHandler = {
masterPlaylistController_: mcp,
playlists: {
master: {
playlists: []
}
}
};
hlsHandler.playlists.master.playlists = playlistOptions.map(makeMockPlaylist);
return hlsHandler;
};
QUnit.module('Rendition Selector API Mixin');
QUnit.test('adds the representations API to HlsHandler', function() {
let hlsHandler = makeMockHlsHandler([
{}
]);
RenditionMixin(hlsHandler);
QUnit.equal(typeof hlsHandler.representations, 'function', 'added the representations API');
});
QUnit.test('returns proper number of representations', function() {
let hlsHandler = makeMockHlsHandler([
{}, {}, {}
]);
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
QUnit.equal(renditions.length, 3, 'number of renditions is 3');
});
QUnit.test('returns representations in playlist order', function() {
let hlsHandler = makeMockHlsHandler([
{
bandwidth: 10
},
{
bandwidth: 20
},
{
bandwidth: 30
}
]);
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
QUnit.equal(renditions[0].bandwidth, 10, 'rendition has bandwidth 10');
QUnit.equal(renditions[1].bandwidth, 20, 'rendition has bandwidth 20');
QUnit.equal(renditions[2].bandwidth, 30, 'rendition has bandwidth 30');
});
QUnit.test('returns representations with width and height if present', function() {
let hlsHandler = makeMockHlsHandler([
{
bandwidth: 10,
width: 100,
height: 200
},
{
bandwidth: 20,
width: 500,
height: 600
},
{
bandwidth: 30
}
]);
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
QUnit.equal(renditions[0].width, 100, 'rendition has a width of 100');
QUnit.equal(renditions[0].height, 200, 'rendition has a height of 200');
QUnit.equal(renditions[1].width, 500, 'rendition has a width of 500');
QUnit.equal(renditions[1].height, 600, 'rendition has a height of 600');
QUnit.equal(renditions[2].width, undefined, 'rendition has a width of undefined');
QUnit.equal(renditions[2].height, undefined, 'rendition has a height of undefined');
});
QUnit.test('representations are disabled if their excludeUntil is after Date.now', function() {
let hlsHandler = makeMockHlsHandler([
{
bandwidth: 0,
excludeUntil: Infinity
},
{
bandwidth: 0,
excludeUntil: 0
}
]);
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
QUnit.equal(renditions[0].enabled(), false, 'rendition is not enabled');
QUnit.equal(renditions[1].enabled(), true, 'rendition is enabled');
});
QUnit.test('setting a representation to disabled sets excludeUntil to Infinity', function() {
let hlsHandler = makeMockHlsHandler([
{
bandwidth: 0,
excludeUntil: 0
},
{
bandwidth: 0,
excludeUntil: 0
}
]);
let playlists = hlsHandler.playlists.master.playlists;
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
renditions[0].enabled(false);
QUnit.equal(playlists[0].excludeUntil, Infinity, 'rendition has an infinite excludeUntil');
QUnit.equal(playlists[1].excludeUntil, 0, 'rendition has an excludeUntil of zero');
});
QUnit.test('changing the enabled state of a representation calls fastQualityChange_', function() {
let hlsHandler = makeMockHlsHandler([
{
bandwidth: 0,
excludeUntil: Infinity
},
{
bandwidth: 0,
excludeUntil: 0
}
]);
let mpc = hlsHandler.masterPlaylistController_;
RenditionMixin(hlsHandler);
let renditions = hlsHandler.representations();
QUnit.equal(mpc.fastQualityChange_.calls, 0, 'fastQualityChange_ was never called');
renditions[0].enabled(true);
QUnit.equal(mpc.fastQualityChange_.calls, 1, 'fastQualityChange_ was called once');
renditions[1].enabled(false);
QUnit.equal(mpc.fastQualityChange_.calls, 2, 'fastQualityChange_ was called twice');
});