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. ...@@ -31,6 +31,7 @@ Play back HLS with video.js, even where it's not natively supported.
31 - [hls.bandwidth](#hlsbandwidth) 31 - [hls.bandwidth](#hlsbandwidth)
32 - [hls.bytesReceived](#hlsbytesreceived) 32 - [hls.bytesReceived](#hlsbytesreceived)
33 - [hls.selectPlaylist](#hlsselectplaylist) 33 - [hls.selectPlaylist](#hlsselectplaylist)
34 - [hls.representations](#hlsrepresentations)
34 - [hls.xhr](#hlsxhr) 35 - [hls.xhr](#hlsxhr)
35 - [Events](#events) 36 - [Events](#events)
36 - [loadedmetadata](#loadedmetadata) 37 - [loadedmetadata](#loadedmetadata)
...@@ -278,6 +279,34 @@ segment is downloaded. You can override this function to provide your ...@@ -278,6 +279,34 @@ segment is downloaded. You can override this function to provide your
278 adaptive streaming logic. You must, however, be sure to return a valid 279 adaptive streaming logic. You must, however, be sure to return a valid
279 media playlist object that is present in `player.hls.master`. 280 media playlist object that is present in `player.hls.master`.
280 281
282 Overridding this function with your own is very powerful but is overkill
283 for many purposes. Most of the time, you should use the much simpler
284 function below to selectively enable or disable a playlist from the
285 adaptive streaming logic.
286
287 #### hls.representations
288 Type: `function`
289
290 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.
291
292 ```javascript
293 player.hls.representations();
294 ```
295
296 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.
297
298 Example, only enabling representations with a width greater than or equal to 720:
299
300 ```javascript
301 player.hls.representations().forEach(function(rep) {
302 if (rep.width >= 720) {
303 rep.enabled(true);
304 } else {
305 rep.enabled(false);
306 }
307 });
308 ```
309
281 #### hls.xhr 310 #### hls.xhr
282 Type: `function` 311 Type: `function`
283 312
......
1 /**
2 * Enable/disable playlist function. It is intended to have the first two
3 * arguments partially-applied in order to create the final per-playlist
4 * function.
5 *
6 * @param {PlaylistLoader} playlist - The rendition or media-playlist
7 * @param {Function} changePlaylistFn - A function to be called after a
8 * playlist's enabled-state has been changed. Will NOT be called if a
9 * playlist's enabled-state is unchanged
10 * @param {Boolean=} enable - Value to set the playlist enabled-state to
11 * or if undefined returns the current enabled-state for the playlist
12 * @return {Boolean} The current enabled-state of the playlist
13 */
14 let enableFunction = (playlist, changePlaylistFn, enable) => {
15 let currentlyEnabled = typeof playlist.excludeUntil === 'undefined' ||
16 playlist.excludeUntil <= Date.now();
17
18 if (typeof enable === 'undefined') {
19 return currentlyEnabled;
20 }
21
22 if (enable !== currentlyEnabled) {
23 if (enable) {
24 delete playlist.excludeUntil;
25 } else {
26 playlist.excludeUntil = Infinity;
27 }
28
29 // Ensure the outside world knows about our changes
30 changePlaylistFn();
31 }
32
33 return enable;
34 };
35
36 /**
37 * The representation object encapsulates the publicly visible information
38 * in a media playlist along with a setter/getter-type function (enabled)
39 * for changing the enabled-state of a particular playlist entry
40 *
41 * @class Representation
42 */
43 class Representation {
44 constructor(hlsHandler, playlist, id) {
45 // Get a reference to a bound version of fastQualityChange_
46 let fastChangeFunction = hlsHandler
47 .masterPlaylistController_
48 .fastQualityChange_
49 .bind(hlsHandler.masterPlaylistController_);
50
51 // Carefully descend into the playlist's attributes since most
52 // properties are optional
53 if (playlist.attributes) {
54 let attributes = playlist.attributes;
55
56 if (attributes.RESOLUTION) {
57 let resolution = attributes.RESOLUTION;
58
59 this.width = resolution.width;
60 this.height = resolution.height;
61 }
62
63 this.bandwidth = attributes.BANDWIDTH;
64 }
65
66 // The id is simply the ordinality of the media playlist
67 // within the master playlist
68 this.id = id;
69
70 // Partially-apply the enableFunction to create a playlist-
71 // specific variant
72 this.enabled = enableFunction.bind(this, playlist, fastChangeFunction);
73 }
74 }
75
76 /**
77 * A mixin function that adds the `representations` api to an instance
78 * of the HlsHandler class
79 * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the
80 * representation API into
81 */
82 let renditionSelectionMixin = function(hlsHandler) {
83 let playlists = hlsHandler.playlists;
84
85 // Add a single API-specific function to the HlsHandler instance
86 hlsHandler.representations = () => {
87 return playlists
88 .master
89 .playlists
90 .map((e, i) => new Representation(hlsHandler, e, i));
91 };
92 };
93
94 export default renditionSelectionMixin;
...@@ -15,6 +15,7 @@ import m3u8 from './m3u8'; ...@@ -15,6 +15,7 @@ import m3u8 from './m3u8';
15 import videojs from 'video.js'; 15 import videojs from 'video.js';
16 import MasterPlaylistController from './master-playlist-controller'; 16 import MasterPlaylistController from './master-playlist-controller';
17 import Config from './config'; 17 import Config from './config';
18 import renditionSelectionMixin from './rendition-mixin';
18 19
19 /** 20 /**
20 * determine if an object a is differnt from 21 * determine if an object a is differnt from
...@@ -475,6 +476,9 @@ class HlsHandler extends Component { ...@@ -475,6 +476,9 @@ class HlsHandler extends Component {
475 this.masterPlaylistController_.audioTracks_.forEach((track) => { 476 this.masterPlaylistController_.audioTracks_.forEach((track) => {
476 this.tech_.audioTracks().addTrack(track); 477 this.tech_.audioTracks().addTrack(track);
477 }); 478 });
479
480 // Add the manual rendition mix-in to HlsHandler
481 renditionSelectionMixin(this);
478 }); 482 });
479 483
480 // the bandwidth of the primary segment loader is our best 484 // the bandwidth of the primary segment loader is our best
......
1 /* eslint-disable max-len */
2
3 import QUnit from 'qunit';
4 import RenditionMixin from '../src/rendition-mixin.js';
5
6 const makeMockPlaylist = function(options) {
7 options = options || {};
8
9 let playlist = {
10 segments: []
11 };
12
13 if ('bandwidth' in options) {
14 playlist.attributes = playlist.attributes || {};
15
16 playlist.attributes.BANDWIDTH = options.bandwidth;
17 }
18
19 if ('width' in options) {
20 playlist.attributes = playlist.attributes || {};
21 playlist.attributes.RESOLUTION = playlist.attributes.RESOLUTION || {};
22
23 playlist.attributes.RESOLUTION.width = options.width;
24 }
25
26 if ('height' in options) {
27 playlist.attributes = playlist.attributes || {};
28 playlist.attributes.RESOLUTION = playlist.attributes.RESOLUTION || {};
29
30 playlist.attributes.RESOLUTION.height = options.height;
31 }
32
33 if ('excludeUntil' in options) {
34 playlist.excludeUntil = options.excludeUntil;
35 }
36
37 return playlist;
38 };
39
40 const makeMockHlsHandler = function(playlistOptions) {
41 let mcp = {
42 fastQualityChange_: () => {
43 mcp.fastQualityChange_.calls++;
44 }
45 };
46
47 mcp.fastQualityChange_.calls = 0;
48
49 let hlsHandler = {
50 masterPlaylistController_: mcp,
51 playlists: {
52 master: {
53 playlists: []
54 }
55 }
56 };
57
58 hlsHandler.playlists.master.playlists = playlistOptions.map(makeMockPlaylist);
59
60 return hlsHandler;
61 };
62
63 QUnit.module('Rendition Selector API Mixin');
64
65 QUnit.test('adds the representations API to HlsHandler', function() {
66 let hlsHandler = makeMockHlsHandler([
67 {}
68 ]);
69
70 RenditionMixin(hlsHandler);
71
72 QUnit.equal(typeof hlsHandler.representations, 'function', 'added the representations API');
73 });
74
75 QUnit.test('returns proper number of representations', function() {
76 let hlsHandler = makeMockHlsHandler([
77 {}, {}, {}
78 ]);
79
80 RenditionMixin(hlsHandler);
81
82 let renditions = hlsHandler.representations();
83
84 QUnit.equal(renditions.length, 3, 'number of renditions is 3');
85 });
86
87 QUnit.test('returns representations in playlist order', function() {
88 let hlsHandler = makeMockHlsHandler([
89 {
90 bandwidth: 10
91 },
92 {
93 bandwidth: 20
94 },
95 {
96 bandwidth: 30
97 }
98 ]);
99
100 RenditionMixin(hlsHandler);
101
102 let renditions = hlsHandler.representations();
103
104 QUnit.equal(renditions[0].bandwidth, 10, 'rendition has bandwidth 10');
105 QUnit.equal(renditions[1].bandwidth, 20, 'rendition has bandwidth 20');
106 QUnit.equal(renditions[2].bandwidth, 30, 'rendition has bandwidth 30');
107 });
108
109 QUnit.test('returns representations with width and height if present', function() {
110 let hlsHandler = makeMockHlsHandler([
111 {
112 bandwidth: 10,
113 width: 100,
114 height: 200
115 },
116 {
117 bandwidth: 20,
118 width: 500,
119 height: 600
120 },
121 {
122 bandwidth: 30
123 }
124 ]);
125
126 RenditionMixin(hlsHandler);
127
128 let renditions = hlsHandler.representations();
129
130 QUnit.equal(renditions[0].width, 100, 'rendition has a width of 100');
131 QUnit.equal(renditions[0].height, 200, 'rendition has a height of 200');
132 QUnit.equal(renditions[1].width, 500, 'rendition has a width of 500');
133 QUnit.equal(renditions[1].height, 600, 'rendition has a height of 600');
134 QUnit.equal(renditions[2].width, undefined, 'rendition has a width of undefined');
135 QUnit.equal(renditions[2].height, undefined, 'rendition has a height of undefined');
136 });
137
138 QUnit.test('representations are disabled if their excludeUntil is after Date.now', function() {
139 let hlsHandler = makeMockHlsHandler([
140 {
141 bandwidth: 0,
142 excludeUntil: Infinity
143 },
144 {
145 bandwidth: 0,
146 excludeUntil: 0
147 }
148 ]);
149
150 RenditionMixin(hlsHandler);
151
152 let renditions = hlsHandler.representations();
153
154 QUnit.equal(renditions[0].enabled(), false, 'rendition is not enabled');
155 QUnit.equal(renditions[1].enabled(), true, 'rendition is enabled');
156 });
157
158 QUnit.test('setting a representation to disabled sets excludeUntil to Infinity', function() {
159 let hlsHandler = makeMockHlsHandler([
160 {
161 bandwidth: 0,
162 excludeUntil: 0
163 },
164 {
165 bandwidth: 0,
166 excludeUntil: 0
167 }
168 ]);
169 let playlists = hlsHandler.playlists.master.playlists;
170
171 RenditionMixin(hlsHandler);
172
173 let renditions = hlsHandler.representations();
174
175 renditions[0].enabled(false);
176
177 QUnit.equal(playlists[0].excludeUntil, Infinity, 'rendition has an infinite excludeUntil');
178 QUnit.equal(playlists[1].excludeUntil, 0, 'rendition has an excludeUntil of zero');
179 });
180
181 QUnit.test('changing the enabled state of a representation calls fastQualityChange_', function() {
182 let hlsHandler = makeMockHlsHandler([
183 {
184 bandwidth: 0,
185 excludeUntil: Infinity
186 },
187 {
188 bandwidth: 0,
189 excludeUntil: 0
190 }
191 ]);
192 let mpc = hlsHandler.masterPlaylistController_;
193
194 RenditionMixin(hlsHandler);
195
196 let renditions = hlsHandler.representations();
197
198 QUnit.equal(mpc.fastQualityChange_.calls, 0, 'fastQualityChange_ was never called');
199
200 renditions[0].enabled(true);
201
202 QUnit.equal(mpc.fastQualityChange_.calls, 1, 'fastQualityChange_ was called once');
203
204 renditions[1].enabled(false);
205
206 QUnit.equal(mpc.fastQualityChange_.calls, 2, 'fastQualityChange_ was called twice');
207 });