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
Showing
4 changed files
with
334 additions
and
0 deletions
... | @@ -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 | ... | ... |
src/rendition-mixin.js
0 → 100644
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 | ... | ... |
test/rendition-mixin.test.js
0 → 100644
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 | }); |
-
Please register or sign in to post a comment