UNPKG

19.5 kBJavaScriptView Raw
1import window from 'global/window';
2import Config from './config';
3import Playlist from './playlist';
4import { codecsForPlaylist } from './util/codecs.js';
5import logger from './util/logger';
6
7const logFn = logger('PlaylistSelector');
8const representationToString = function(representation) {
9 if (!representation || !representation.playlist) {
10 return;
11 }
12 const playlist = representation.playlist;
13
14 return JSON.stringify({
15 id: playlist.id,
16 bandwidth: representation.bandwidth,
17 width: representation.width,
18 height: representation.height,
19 codecs: playlist.attributes && playlist.attributes.CODECS || ''
20 });
21};
22
23// Utilities
24
25/**
26 * Returns the CSS value for the specified property on an element
27 * using `getComputedStyle`. Firefox has a long-standing issue where
28 * getComputedStyle() may return null when running in an iframe with
29 * `display: none`.
30 *
31 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
32 * @param {HTMLElement} el the htmlelement to work on
33 * @param {string} the proprety to get the style for
34 */
35const safeGetComputedStyle = function(el, property) {
36 if (!el) {
37 return '';
38 }
39
40 const result = window.getComputedStyle(el);
41
42 if (!result) {
43 return '';
44 }
45
46 return result[property];
47};
48
49/**
50 * Resuable stable sort function
51 *
52 * @param {Playlists} array
53 * @param {Function} sortFn Different comparators
54 * @function stableSort
55 */
56const stableSort = function(array, sortFn) {
57 const newArray = array.slice();
58
59 array.sort(function(left, right) {
60 const cmp = sortFn(left, right);
61
62 if (cmp === 0) {
63 return newArray.indexOf(left) - newArray.indexOf(right);
64 }
65 return cmp;
66 });
67};
68
69/**
70 * A comparator function to sort two playlist object by bandwidth.
71 *
72 * @param {Object} left a media playlist object
73 * @param {Object} right a media playlist object
74 * @return {number} Greater than zero if the bandwidth attribute of
75 * left is greater than the corresponding attribute of right. Less
76 * than zero if the bandwidth of right is greater than left and
77 * exactly zero if the two are equal.
78 */
79export const comparePlaylistBandwidth = function(left, right) {
80 let leftBandwidth;
81 let rightBandwidth;
82
83 if (left.attributes.BANDWIDTH) {
84 leftBandwidth = left.attributes.BANDWIDTH;
85 }
86 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
87 if (right.attributes.BANDWIDTH) {
88 rightBandwidth = right.attributes.BANDWIDTH;
89 }
90 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
91
92 return leftBandwidth - rightBandwidth;
93};
94
95/**
96 * A comparator function to sort two playlist object by resolution (width).
97 *
98 * @param {Object} left a media playlist object
99 * @param {Object} right a media playlist object
100 * @return {number} Greater than zero if the resolution.width attribute of
101 * left is greater than the corresponding attribute of right. Less
102 * than zero if the resolution.width of right is greater than left and
103 * exactly zero if the two are equal.
104 */
105export const comparePlaylistResolution = function(left, right) {
106 let leftWidth;
107 let rightWidth;
108
109 if (left.attributes.RESOLUTION &&
110 left.attributes.RESOLUTION.width) {
111 leftWidth = left.attributes.RESOLUTION.width;
112 }
113
114 leftWidth = leftWidth || window.Number.MAX_VALUE;
115
116 if (right.attributes.RESOLUTION &&
117 right.attributes.RESOLUTION.width) {
118 rightWidth = right.attributes.RESOLUTION.width;
119 }
120
121 rightWidth = rightWidth || window.Number.MAX_VALUE;
122
123 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
124 // have the same media dimensions/ resolution
125 if (leftWidth === rightWidth &&
126 left.attributes.BANDWIDTH &&
127 right.attributes.BANDWIDTH) {
128 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
129 }
130 return leftWidth - rightWidth;
131};
132
133/**
134 * Chooses the appropriate media playlist based on bandwidth and player size
135 *
136 * @param {Object} master
137 * Object representation of the master manifest
138 * @param {number} playerBandwidth
139 * Current calculated bandwidth of the player
140 * @param {number} playerWidth
141 * Current width of the player element (should account for the device pixel ratio)
142 * @param {number} playerHeight
143 * Current height of the player element (should account for the device pixel ratio)
144 * @param {boolean} limitRenditionByPlayerDimensions
145 * True if the player width and height should be used during the selection, false otherwise
146 * @param {Object} masterPlaylistController
147 * the current masterPlaylistController object
148 * @return {Playlist} the highest bitrate playlist less than the
149 * currently detected bandwidth, accounting for some amount of
150 * bandwidth variance
151 */
152export let simpleSelector = function(
153 master,
154 playerBandwidth,
155 playerWidth,
156 playerHeight,
157 limitRenditionByPlayerDimensions,
158 masterPlaylistController
159) {
160
161 // If we end up getting called before `master` is available, exit early
162 if (!master) {
163 return;
164 }
165
166 const options = {
167 bandwidth: playerBandwidth,
168 width: playerWidth,
169 height: playerHeight,
170 limitRenditionByPlayerDimensions
171 };
172
173 let playlists = master.playlists;
174
175 // if playlist is audio only, select between currently active audio group playlists.
176 if (Playlist.isAudioOnly(master)) {
177 playlists = masterPlaylistController.getAudioTrackPlaylists_();
178 // add audioOnly to options so that we log audioOnly: true
179 // at the buttom of this function for debugging.
180 options.audioOnly = true;
181 }
182 // convert the playlists to an intermediary representation to make comparisons easier
183 let sortedPlaylistReps = playlists.map((playlist) => {
184 let bandwidth;
185 const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
186 const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
187
188 bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH;
189
190 bandwidth = bandwidth || window.Number.MAX_VALUE;
191
192 return {
193 bandwidth,
194 width,
195 height,
196 playlist
197 };
198 });
199
200 stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth);
201
202 // filter out any playlists that have been excluded due to
203 // incompatible configurations
204 sortedPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isIncompatible(rep.playlist));
205
206 // filter out any playlists that have been disabled manually through the representations
207 // api or blacklisted temporarily due to playback errors.
208 let enabledPlaylistReps = sortedPlaylistReps.filter((rep) => Playlist.isEnabled(rep.playlist));
209
210 if (!enabledPlaylistReps.length) {
211 // if there are no enabled playlists, then they have all been blacklisted or disabled
212 // by the user through the representations api. In this case, ignore blacklisting and
213 // fallback to what the user wants by using playlists the user has not disabled.
214 enabledPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isDisabled(rep.playlist));
215 }
216
217 // filter out any variant that has greater effective bitrate
218 // than the current estimated bandwidth
219 const bandwidthPlaylistReps = enabledPlaylistReps.filter((rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth);
220
221 let highestRemainingBandwidthRep =
222 bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
223
224 // get all of the renditions with the same (highest) bandwidth
225 // and then taking the very first element
226 const bandwidthBestRep = bandwidthPlaylistReps.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
227
228 // if we're not going to limit renditions by player size, make an early decision.
229 if (limitRenditionByPlayerDimensions === false) {
230 const chosenRep = (
231 bandwidthBestRep ||
232 enabledPlaylistReps[0] ||
233 sortedPlaylistReps[0]
234 );
235
236 if (chosenRep && chosenRep.playlist) {
237 let type = 'sortedPlaylistReps';
238
239 if (bandwidthBestRep) {
240 type = 'bandwidthBestRep';
241 }
242 if (enabledPlaylistReps[0]) {
243 type = 'enabledPlaylistReps';
244 }
245 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
246
247 return chosenRep.playlist;
248 }
249
250 logFn('could not choose a playlist with options', options);
251 return null;
252 }
253
254 // filter out playlists without resolution information
255 const haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height);
256
257 // sort variants by resolution
258 stableSort(haveResolution, (left, right) => left.width - right.width);
259
260 // if we have the exact resolution as the player use it
261 const resolutionBestRepList = haveResolution.filter((rep) => rep.width === playerWidth && rep.height === playerHeight);
262
263 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
264 // ensure that we pick the highest bandwidth variant that have exact resolution
265 const resolutionBestRep = resolutionBestRepList.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
266
267 let resolutionPlusOneList;
268 let resolutionPlusOneSmallest;
269 let resolutionPlusOneRep;
270
271 // find the smallest variant that is larger than the player
272 // if there is no match of exact resolution
273 if (!resolutionBestRep) {
274 resolutionPlusOneList = haveResolution.filter((rep) => rep.width > playerWidth || rep.height > playerHeight);
275
276 // find all the variants have the same smallest resolution
277 resolutionPlusOneSmallest = resolutionPlusOneList.filter((rep) => rep.width === resolutionPlusOneList[0].width &&
278 rep.height === resolutionPlusOneList[0].height);
279
280 // ensure that we also pick the highest bandwidth variant that
281 // is just-larger-than the video player
282 highestRemainingBandwidthRep =
283 resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
284 resolutionPlusOneRep = resolutionPlusOneSmallest.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
285 }
286
287 let leastPixelDiffRep;
288
289 // If this selector proves to be better than others,
290 // resolutionPlusOneRep and resolutionBestRep and all
291 // the code involving them should be removed.
292 if (masterPlaylistController.experimentalLeastPixelDiffSelector) {
293 // find the variant that is closest to the player's pixel size
294 const leastPixelDiffList = haveResolution.map((rep) => {
295 rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight);
296 return rep;
297 });
298
299 // get the highest bandwidth, closest resolution playlist
300 stableSort(leastPixelDiffList, (left, right) => {
301 // sort by highest bandwidth if pixelDiff is the same
302 if (left.pixelDiff === right.pixelDiff) {
303 return right.bandwidth - left.bandwidth;
304 }
305
306 return left.pixelDiff - right.pixelDiff;
307 });
308
309 leastPixelDiffRep = leastPixelDiffList[0];
310 }
311
312 // fallback chain of variants
313 const chosenRep = (
314 leastPixelDiffRep ||
315 resolutionPlusOneRep ||
316 resolutionBestRep ||
317 bandwidthBestRep ||
318 enabledPlaylistReps[0] ||
319 sortedPlaylistReps[0]
320 );
321
322 if (chosenRep && chosenRep.playlist) {
323 let type = 'sortedPlaylistReps';
324
325 if (leastPixelDiffRep) {
326 type = 'leastPixelDiffRep';
327 } else if (resolutionPlusOneRep) {
328 type = 'resolutionPlusOneRep';
329 } else if (resolutionBestRep) {
330 type = 'resolutionBestRep';
331 } else if (bandwidthBestRep) {
332 type = 'bandwidthBestRep';
333 } else if (enabledPlaylistReps[0]) {
334 type = 'enabledPlaylistReps';
335 }
336
337 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
338 return chosenRep.playlist;
339 }
340 logFn('could not choose a playlist with options', options);
341 return null;
342};
343
344export const TEST_ONLY_SIMPLE_SELECTOR = (newSimpleSelector) => {
345 const oldSimpleSelector = simpleSelector;
346
347 simpleSelector = newSimpleSelector;
348
349 return function resetSimpleSelector() {
350 simpleSelector = oldSimpleSelector;
351 };
352};
353
354// Playlist Selectors
355
356/**
357 * Chooses the appropriate media playlist based on the most recent
358 * bandwidth estimate and the player size.
359 *
360 * Expects to be called within the context of an instance of VhsHandler
361 *
362 * @return {Playlist} the highest bitrate playlist less than the
363 * currently detected bandwidth, accounting for some amount of
364 * bandwidth variance
365 */
366export const lastBandwidthSelector = function() {
367 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
368
369 return simpleSelector(
370 this.playlists.master,
371 this.systemBandwidth,
372 parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio,
373 parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio,
374 this.limitRenditionByPlayerDimensions,
375 this.masterPlaylistController_
376 );
377};
378
379/**
380 * Chooses the appropriate media playlist based on an
381 * exponential-weighted moving average of the bandwidth after
382 * filtering for player size.
383 *
384 * Expects to be called within the context of an instance of VhsHandler
385 *
386 * @param {number} decay - a number between 0 and 1. Higher values of
387 * this parameter will cause previous bandwidth estimates to lose
388 * significance more quickly.
389 * @return {Function} a function which can be invoked to create a new
390 * playlist selector function.
391 * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
392 */
393export const movingAverageBandwidthSelector = function(decay) {
394 let average = -1;
395 let lastSystemBandwidth = -1;
396
397 if (decay < 0 || decay > 1) {
398 throw new Error('Moving average bandwidth decay must be between 0 and 1.');
399 }
400
401 return function() {
402 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
403
404 if (average < 0) {
405 average = this.systemBandwidth;
406 lastSystemBandwidth = this.systemBandwidth;
407 }
408
409 // stop the average value from decaying for every 250ms
410 // when the systemBandwidth is constant
411 // and
412 // stop average from setting to a very low value when the
413 // systemBandwidth becomes 0 in case of chunk cancellation
414
415 if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) {
416 average = decay * this.systemBandwidth + (1 - decay) * average;
417 lastSystemBandwidth = this.systemBandwidth;
418 }
419
420 return simpleSelector(
421 this.playlists.master,
422 average,
423 parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio,
424 parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio,
425 this.limitRenditionByPlayerDimensions,
426 this.masterPlaylistController_
427 );
428 };
429};
430
431/**
432 * Chooses the appropriate media playlist based on the potential to rebuffer
433 *
434 * @param {Object} settings
435 * Object of information required to use this selector
436 * @param {Object} settings.master
437 * Object representation of the master manifest
438 * @param {number} settings.currentTime
439 * The current time of the player
440 * @param {number} settings.bandwidth
441 * Current measured bandwidth
442 * @param {number} settings.duration
443 * Duration of the media
444 * @param {number} settings.segmentDuration
445 * Segment duration to be used in round trip time calculations
446 * @param {number} settings.timeUntilRebuffer
447 * Time left in seconds until the player has to rebuffer
448 * @param {number} settings.currentTimeline
449 * The current timeline segments are being loaded from
450 * @param {SyncController} settings.syncController
451 * SyncController for determining if we have a sync point for a given playlist
452 * @return {Object|null}
453 * {Object} return.playlist
454 * The highest bandwidth playlist with the least amount of rebuffering
455 * {Number} return.rebufferingImpact
456 * The amount of time in seconds switching to this playlist will rebuffer. A
457 * negative value means that switching will cause zero rebuffering.
458 */
459export const minRebufferMaxBandwidthSelector = function(settings) {
460 const {
461 master,
462 currentTime,
463 bandwidth,
464 duration,
465 segmentDuration,
466 timeUntilRebuffer,
467 currentTimeline,
468 syncController
469 } = settings;
470
471 // filter out any playlists that have been excluded due to
472 // incompatible configurations
473 const compatiblePlaylists = master.playlists.filter(playlist => !Playlist.isIncompatible(playlist));
474
475 // filter out any playlists that have been disabled manually through the representations
476 // api or blacklisted temporarily due to playback errors.
477 let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
478
479 if (!enabledPlaylists.length) {
480 // if there are no enabled playlists, then they have all been blacklisted or disabled
481 // by the user through the representations api. In this case, ignore blacklisting and
482 // fallback to what the user wants by using playlists the user has not disabled.
483 enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist));
484 }
485
486 const bandwidthPlaylists =
487 enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));
488
489 const rebufferingEstimates = bandwidthPlaylists.map((playlist) => {
490 const syncPoint = syncController.getSyncPoint(
491 playlist,
492 duration,
493 currentTimeline,
494 currentTime
495 );
496 // If there is no sync point for this playlist, switching to it will require a
497 // sync request first. This will double the request time
498 const numRequests = syncPoint ? 1 : 2;
499 const requestTimeEstimate = Playlist.estimateSegmentRequestTime(
500 segmentDuration,
501 bandwidth,
502 playlist
503 );
504 const rebufferingImpact = (requestTimeEstimate * numRequests) - timeUntilRebuffer;
505
506 return {
507 playlist,
508 rebufferingImpact
509 };
510 });
511
512 const noRebufferingPlaylists = rebufferingEstimates.filter((estimate) => estimate.rebufferingImpact <= 0);
513
514 // Sort by bandwidth DESC
515 stableSort(
516 noRebufferingPlaylists,
517 (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)
518 );
519
520 if (noRebufferingPlaylists.length) {
521 return noRebufferingPlaylists[0];
522 }
523
524 stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact);
525
526 return rebufferingEstimates[0] || null;
527};
528
529/**
530 * Chooses the appropriate media playlist, which in this case is the lowest bitrate
531 * one with video. If no renditions with video exist, return the lowest audio rendition.
532 *
533 * Expects to be called within the context of an instance of VhsHandler
534 *
535 * @return {Object|null}
536 * {Object} return.playlist
537 * The lowest bitrate playlist that contains a video codec. If no such rendition
538 * exists pick the lowest audio rendition.
539 */
540export const lowestBitrateCompatibleVariantSelector = function() {
541 // filter out any playlists that have been excluded due to
542 // incompatible configurations or playback errors
543 const playlists = this.playlists.master.playlists.filter(Playlist.isEnabled);
544
545 // Sort ascending by bitrate
546 stableSort(
547 playlists,
548 (a, b) => comparePlaylistBandwidth(a, b)
549 );
550
551 // Parse and assume that playlists with no video codec have no video
552 // (this is not necessarily true, although it is generally true).
553 //
554 // If an entire manifest has no valid videos everything will get filtered
555 // out.
556 const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.master, playlist).video);
557
558 return playlistsWithVideo[0] || null;
559};