1 | import Config from './config';
|
2 | import Playlist from './playlist';
|
3 | import { parseCodecs } from './util/codecs.js';
|
4 |
|
5 | // Utilities
|
6 |
|
7 | /**
|
8 | * Returns the CSS value for the specified property on an element
|
9 | * using `getComputedStyle`. Firefox has a long-standing issue where
|
10 | * getComputedStyle() may return null when running in an iframe with
|
11 | * `display: none`.
|
12 | *
|
13 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
14 | * @param {HTMLElement} el the htmlelement to work on
|
15 | * @param {string} the proprety to get the style for
|
16 | */
|
17 | const safeGetComputedStyle = function(el, property) {
|
18 | let result;
|
19 |
|
20 | if (!el) {
|
21 | return '';
|
22 | }
|
23 |
|
24 | result = window.getComputedStyle(el);
|
25 | if (!result) {
|
26 | return '';
|
27 | }
|
28 |
|
29 | return result[property];
|
30 | };
|
31 |
|
32 | /**
|
33 | * Resuable stable sort function
|
34 | *
|
35 | * @param {Playlists} array
|
36 | * @param {Function} sortFn Different comparators
|
37 | * @function stableSort
|
38 | */
|
39 | const stableSort = function(array, sortFn) {
|
40 | let newArray = array.slice();
|
41 |
|
42 | array.sort(function(left, right) {
|
43 | let cmp = sortFn(left, right);
|
44 |
|
45 | if (cmp === 0) {
|
46 | return newArray.indexOf(left) - newArray.indexOf(right);
|
47 | }
|
48 | return cmp;
|
49 | });
|
50 | };
|
51 |
|
52 | /**
|
53 | * A comparator function to sort two playlist object by bandwidth.
|
54 | *
|
55 | * @param {Object} left a media playlist object
|
56 | * @param {Object} right a media playlist object
|
57 | * @return {Number} Greater than zero if the bandwidth attribute of
|
58 | * left is greater than the corresponding attribute of right. Less
|
59 | * than zero if the bandwidth of right is greater than left and
|
60 | * exactly zero if the two are equal.
|
61 | */
|
62 | export const comparePlaylistBandwidth = function(left, right) {
|
63 | let leftBandwidth;
|
64 | let rightBandwidth;
|
65 |
|
66 | if (left.attributes.BANDWIDTH) {
|
67 | leftBandwidth = left.attributes.BANDWIDTH;
|
68 | }
|
69 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
|
70 | if (right.attributes.BANDWIDTH) {
|
71 | rightBandwidth = right.attributes.BANDWIDTH;
|
72 | }
|
73 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
|
74 |
|
75 | return leftBandwidth - rightBandwidth;
|
76 | };
|
77 |
|
78 | /**
|
79 | * A comparator function to sort two playlist object by resolution (width).
|
80 | * @param {Object} left a media playlist object
|
81 | * @param {Object} right a media playlist object
|
82 | * @return {Number} Greater than zero if the resolution.width attribute of
|
83 | * left is greater than the corresponding attribute of right. Less
|
84 | * than zero if the resolution.width of right is greater than left and
|
85 | * exactly zero if the two are equal.
|
86 | */
|
87 | export const comparePlaylistResolution = function(left, right) {
|
88 | let leftWidth;
|
89 | let rightWidth;
|
90 |
|
91 | if (left.attributes.RESOLUTION &&
|
92 | left.attributes.RESOLUTION.width) {
|
93 | leftWidth = left.attributes.RESOLUTION.width;
|
94 | }
|
95 |
|
96 | leftWidth = leftWidth || window.Number.MAX_VALUE;
|
97 |
|
98 | if (right.attributes.RESOLUTION &&
|
99 | right.attributes.RESOLUTION.width) {
|
100 | rightWidth = right.attributes.RESOLUTION.width;
|
101 | }
|
102 |
|
103 | rightWidth = rightWidth || window.Number.MAX_VALUE;
|
104 |
|
105 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
|
106 | // have the same media dimensions/ resolution
|
107 | if (leftWidth === rightWidth &&
|
108 | left.attributes.BANDWIDTH &&
|
109 | right.attributes.BANDWIDTH) {
|
110 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
|
111 | }
|
112 | return leftWidth - rightWidth;
|
113 | };
|
114 |
|
115 | /**
|
116 | * Chooses the appropriate media playlist based on bandwidth and player size
|
117 | *
|
118 | * @param {Object} master
|
119 | * Object representation of the master manifest
|
120 | * @param {Number} playerBandwidth
|
121 | * Current calculated bandwidth of the player
|
122 | * @param {Number} playerWidth
|
123 | * Current width of the player element
|
124 | * @param {Number} playerHeight
|
125 | * Current height of the player element
|
126 | * @return {Playlist} the highest bitrate playlist less than the
|
127 | * currently detected bandwidth, accounting for some amount of
|
128 | * bandwidth variance
|
129 | */
|
130 | export const simpleSelector = function(master,
|
131 | playerBandwidth,
|
132 | playerWidth,
|
133 | playerHeight) {
|
134 | // convert the playlists to an intermediary representation to make comparisons easier
|
135 | let sortedPlaylistReps = master.playlists.map((playlist) => {
|
136 | let width;
|
137 | let height;
|
138 | let bandwidth;
|
139 |
|
140 | width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
|
141 | height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
|
142 | bandwidth = playlist.attributes.BANDWIDTH;
|
143 |
|
144 | bandwidth = bandwidth || window.Number.MAX_VALUE;
|
145 |
|
146 | return {
|
147 | bandwidth,
|
148 | width,
|
149 | height,
|
150 | playlist
|
151 | };
|
152 | });
|
153 |
|
154 | stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth);
|
155 |
|
156 | // filter out any playlists that have been excluded due to
|
157 | // incompatible configurations
|
158 | sortedPlaylistReps = sortedPlaylistReps.filter(
|
159 | (rep) => !Playlist.isIncompatible(rep.playlist)
|
160 | );
|
161 |
|
162 | // filter out any playlists that have been disabled manually through the representations
|
163 | // api or blacklisted temporarily due to playback errors.
|
164 | let enabledPlaylistReps = sortedPlaylistReps.filter(
|
165 | (rep) => Playlist.isEnabled(rep.playlist)
|
166 | );
|
167 |
|
168 | if (!enabledPlaylistReps.length) {
|
169 | // if there are no enabled playlists, then they have all been blacklisted or disabled
|
170 | // by the user through the representations api. In this case, ignore blacklisting and
|
171 | // fallback to what the user wants by using playlists the user has not disabled.
|
172 | enabledPlaylistReps = sortedPlaylistReps.filter(
|
173 | (rep) => !Playlist.isDisabled(rep.playlist)
|
174 | );
|
175 | }
|
176 |
|
177 | // filter out any variant that has greater effective bitrate
|
178 | // than the current estimated bandwidth
|
179 | let bandwidthPlaylistReps = enabledPlaylistReps.filter(
|
180 | (rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth
|
181 | );
|
182 |
|
183 | let highestRemainingBandwidthRep =
|
184 | bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
|
185 |
|
186 | // get all of the renditions with the same (highest) bandwidth
|
187 | // and then taking the very first element
|
188 | let bandwidthBestRep = bandwidthPlaylistReps.filter(
|
189 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
|
190 | )[0];
|
191 |
|
192 | // filter out playlists without resolution information
|
193 | let haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height);
|
194 |
|
195 | // sort variants by resolution
|
196 | stableSort(haveResolution, (left, right) => left.width - right.width);
|
197 |
|
198 | // if we have the exact resolution as the player use it
|
199 | let resolutionBestRepList = haveResolution.filter(
|
200 | (rep) => rep.width === playerWidth && rep.height === playerHeight
|
201 | );
|
202 |
|
203 | highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
|
204 | // ensure that we pick the highest bandwidth variant that have exact resolution
|
205 | let resolutionBestRep = resolutionBestRepList.filter(
|
206 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
|
207 | )[0];
|
208 |
|
209 | let resolutionPlusOneList;
|
210 | let resolutionPlusOneSmallest;
|
211 | let resolutionPlusOneRep;
|
212 |
|
213 | // find the smallest variant that is larger than the player
|
214 | // if there is no match of exact resolution
|
215 | if (!resolutionBestRep) {
|
216 | resolutionPlusOneList = haveResolution.filter(
|
217 | (rep) => rep.width > playerWidth || rep.height > playerHeight
|
218 | );
|
219 |
|
220 | // find all the variants have the same smallest resolution
|
221 | resolutionPlusOneSmallest = resolutionPlusOneList.filter(
|
222 | (rep) => rep.width === resolutionPlusOneList[0].width &&
|
223 | rep.height === resolutionPlusOneList[0].height
|
224 | );
|
225 |
|
226 | // ensure that we also pick the highest bandwidth variant that
|
227 | // is just-larger-than the video player
|
228 | highestRemainingBandwidthRep =
|
229 | resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
|
230 | resolutionPlusOneRep = resolutionPlusOneSmallest.filter(
|
231 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
|
232 | )[0];
|
233 | }
|
234 |
|
235 | // fallback chain of variants
|
236 | let chosenRep = (
|
237 | resolutionPlusOneRep ||
|
238 | resolutionBestRep ||
|
239 | bandwidthBestRep ||
|
240 | enabledPlaylistReps[0] ||
|
241 | sortedPlaylistReps[0]
|
242 | );
|
243 |
|
244 | return chosenRep ? chosenRep.playlist : null;
|
245 | };
|
246 |
|
247 | // Playlist Selectors
|
248 |
|
249 | /**
|
250 | * Chooses the appropriate media playlist based on the most recent
|
251 | * bandwidth estimate and the player size.
|
252 | *
|
253 | * Expects to be called within the context of an instance of HlsHandler
|
254 | *
|
255 | * @return {Playlist} the highest bitrate playlist less than the
|
256 | * currently detected bandwidth, accounting for some amount of
|
257 | * bandwidth variance
|
258 | */
|
259 | export const lastBandwidthSelector = function() {
|
260 | return simpleSelector(this.playlists.master,
|
261 | this.systemBandwidth,
|
262 | parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10),
|
263 | parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
|
264 | };
|
265 |
|
266 | /**
|
267 | * Chooses the appropriate media playlist based on an
|
268 | * exponential-weighted moving average of the bandwidth after
|
269 | * filtering for player size.
|
270 | *
|
271 | * Expects to be called within the context of an instance of HlsHandler
|
272 | *
|
273 | * @param {Number} decay - a number between 0 and 1. Higher values of
|
274 | * this parameter will cause previous bandwidth estimates to lose
|
275 | * significance more quickly.
|
276 | * @return {Function} a function which can be invoked to create a new
|
277 | * playlist selector function.
|
278 | * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
279 | */
|
280 | export const movingAverageBandwidthSelector = function(decay) {
|
281 | let average = -1;
|
282 |
|
283 | if (decay < 0 || decay > 1) {
|
284 | throw new Error('Moving average bandwidth decay must be between 0 and 1.');
|
285 | }
|
286 |
|
287 | return function() {
|
288 | if (average < 0) {
|
289 | average = this.systemBandwidth;
|
290 | }
|
291 |
|
292 | average = decay * this.systemBandwidth + (1 - decay) * average;
|
293 | return simpleSelector(this.playlists.master,
|
294 | average,
|
295 | parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10),
|
296 | parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
|
297 | };
|
298 | };
|
299 |
|
300 | /**
|
301 | * Chooses the appropriate media playlist based on the potential to rebuffer
|
302 | *
|
303 | * @param {Object} settings
|
304 | * Object of information required to use this selector
|
305 | * @param {Object} settings.master
|
306 | * Object representation of the master manifest
|
307 | * @param {Number} settings.currentTime
|
308 | * The current time of the player
|
309 | * @param {Number} settings.bandwidth
|
310 | * Current measured bandwidth
|
311 | * @param {Number} settings.duration
|
312 | * Duration of the media
|
313 | * @param {Number} settings.segmentDuration
|
314 | * Segment duration to be used in round trip time calculations
|
315 | * @param {Number} settings.timeUntilRebuffer
|
316 | * Time left in seconds until the player has to rebuffer
|
317 | * @param {Number} settings.currentTimeline
|
318 | * The current timeline segments are being loaded from
|
319 | * @param {SyncController} settings.syncController
|
320 | * SyncController for determining if we have a sync point for a given playlist
|
321 | * @return {Object|null}
|
322 | * {Object} return.playlist
|
323 | * The highest bandwidth playlist with the least amount of rebuffering
|
324 | * {Number} return.rebufferingImpact
|
325 | * The amount of time in seconds switching to this playlist will rebuffer. A
|
326 | * negative value means that switching will cause zero rebuffering.
|
327 | */
|
328 | export const minRebufferMaxBandwidthSelector = function(settings) {
|
329 | const {
|
330 | master,
|
331 | currentTime,
|
332 | bandwidth,
|
333 | duration,
|
334 | segmentDuration,
|
335 | timeUntilRebuffer,
|
336 | currentTimeline,
|
337 | syncController
|
338 | } = settings;
|
339 |
|
340 | // filter out any playlists that have been excluded due to
|
341 | // incompatible configurations
|
342 | const compatiblePlaylists = master.playlists.filter(
|
343 | playlist => !Playlist.isIncompatible(playlist));
|
344 |
|
345 | // filter out any playlists that have been disabled manually through the representations
|
346 | // api or blacklisted temporarily due to playback errors.
|
347 | let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
|
348 |
|
349 | if (!enabledPlaylists.length) {
|
350 | // if there are no enabled playlists, then they have all been blacklisted or disabled
|
351 | // by the user through the representations api. In this case, ignore blacklisting and
|
352 | // fallback to what the user wants by using playlists the user has not disabled.
|
353 | enabledPlaylists = compatiblePlaylists.filter(
|
354 | playlist => !Playlist.isDisabled(playlist));
|
355 | }
|
356 |
|
357 | const bandwidthPlaylists =
|
358 | enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));
|
359 |
|
360 | const rebufferingEstimates = bandwidthPlaylists.map((playlist) => {
|
361 | const syncPoint = syncController.getSyncPoint(playlist,
|
362 | duration,
|
363 | currentTimeline,
|
364 | currentTime);
|
365 | // If there is no sync point for this playlist, switching to it will require a
|
366 | // sync request first. This will double the request time
|
367 | const numRequests = syncPoint ? 1 : 2;
|
368 | const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration,
|
369 | bandwidth,
|
370 | playlist);
|
371 | const rebufferingImpact = (requestTimeEstimate * numRequests) - timeUntilRebuffer;
|
372 |
|
373 | return {
|
374 | playlist,
|
375 | rebufferingImpact
|
376 | };
|
377 | });
|
378 |
|
379 | const noRebufferingPlaylists = rebufferingEstimates.filter(
|
380 | (estimate) => estimate.rebufferingImpact <= 0);
|
381 |
|
382 | // Sort by bandwidth DESC
|
383 | stableSort(noRebufferingPlaylists,
|
384 | (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist));
|
385 |
|
386 | if (noRebufferingPlaylists.length) {
|
387 | return noRebufferingPlaylists[0];
|
388 | }
|
389 |
|
390 | stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact);
|
391 |
|
392 | return rebufferingEstimates[0] || null;
|
393 | };
|
394 |
|
395 | /**
|
396 | * Chooses the appropriate media playlist, which in this case is the lowest bitrate
|
397 | * one with video. If no renditions with video exist, return the lowest audio rendition.
|
398 | *
|
399 | * Expects to be called within the context of an instance of HlsHandler
|
400 | *
|
401 | * @return {Object|null}
|
402 | * {Object} return.playlist
|
403 | * The lowest bitrate playlist that contains a video codec. If no such rendition
|
404 | * exists pick the lowest audio rendition.
|
405 | */
|
406 | export const lowestBitrateCompatibleVariantSelector = function() {
|
407 | // filter out any playlists that have been excluded due to
|
408 | // incompatible configurations or playback errors
|
409 | const playlists = this.playlists.master.playlists.filter(Playlist.isEnabled);
|
410 |
|
411 | // Sort ascending by bitrate
|
412 | stableSort(playlists,
|
413 | (a, b) => comparePlaylistBandwidth(a, b));
|
414 |
|
415 | // Parse and assume that playlists with no video codec have no video
|
416 | // (this is not necessarily true, although it is generally true).
|
417 | //
|
418 | // If an entire manifest has no valid videos everything will get filtered
|
419 | // out.
|
420 | const playlistsWithVideo = playlists.filter(
|
421 | playlist => parseCodecs(playlist.attributes.CODECS).videoCodec
|
422 | );
|
423 |
|
424 | return playlistsWithVideo[0] || null;
|
425 | };
|