1 | import window from 'global/window';
|
2 | import Config from './config';
|
3 | import Playlist from './playlist';
|
4 | import { codecsForPlaylist } from './util/codecs.js';
|
5 | import logger from './util/logger';
|
6 |
|
7 | const logFn = logger('PlaylistSelector');
|
8 | const 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 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | const 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 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | const 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 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | export 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 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | export 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 |
|
124 |
|
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 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | export let simpleSelector = function(
|
153 | master,
|
154 | playerBandwidth,
|
155 | playerWidth,
|
156 | playerHeight,
|
157 | limitRenditionByPlayerDimensions,
|
158 | masterPlaylistController
|
159 | ) {
|
160 |
|
161 |
|
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 |
|
176 | if (Playlist.isAudioOnly(master)) {
|
177 | playlists = masterPlaylistController.getAudioTrackPlaylists_();
|
178 |
|
179 |
|
180 | options.audioOnly = true;
|
181 | }
|
182 |
|
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 |
|
203 |
|
204 | sortedPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isIncompatible(rep.playlist));
|
205 |
|
206 |
|
207 |
|
208 | let enabledPlaylistReps = sortedPlaylistReps.filter((rep) => Playlist.isEnabled(rep.playlist));
|
209 |
|
210 | if (!enabledPlaylistReps.length) {
|
211 |
|
212 |
|
213 |
|
214 | enabledPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isDisabled(rep.playlist));
|
215 | }
|
216 |
|
217 |
|
218 |
|
219 | const bandwidthPlaylistReps = enabledPlaylistReps.filter((rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth);
|
220 |
|
221 | let highestRemainingBandwidthRep =
|
222 | bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
|
223 |
|
224 |
|
225 |
|
226 | const bandwidthBestRep = bandwidthPlaylistReps.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
227 |
|
228 |
|
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 |
|
255 | const haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height);
|
256 |
|
257 |
|
258 | stableSort(haveResolution, (left, right) => left.width - right.width);
|
259 |
|
260 |
|
261 | const resolutionBestRepList = haveResolution.filter((rep) => rep.width === playerWidth && rep.height === playerHeight);
|
262 |
|
263 | highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
|
264 |
|
265 | const resolutionBestRep = resolutionBestRepList.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
266 |
|
267 | let resolutionPlusOneList;
|
268 | let resolutionPlusOneSmallest;
|
269 | let resolutionPlusOneRep;
|
270 |
|
271 |
|
272 |
|
273 | if (!resolutionBestRep) {
|
274 | resolutionPlusOneList = haveResolution.filter((rep) => rep.width > playerWidth || rep.height > playerHeight);
|
275 |
|
276 |
|
277 | resolutionPlusOneSmallest = resolutionPlusOneList.filter((rep) => rep.width === resolutionPlusOneList[0].width &&
|
278 | rep.height === resolutionPlusOneList[0].height);
|
279 |
|
280 |
|
281 |
|
282 | highestRemainingBandwidthRep =
|
283 | resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
|
284 | resolutionPlusOneRep = resolutionPlusOneSmallest.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
285 | }
|
286 |
|
287 | let leastPixelDiffRep;
|
288 |
|
289 |
|
290 |
|
291 |
|
292 | if (masterPlaylistController.experimentalLeastPixelDiffSelector) {
|
293 |
|
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 |
|
300 | stableSort(leastPixelDiffList, (left, right) => {
|
301 |
|
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 |
|
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 |
|
344 | export 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 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 | export 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 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 | export 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 |
|
410 |
|
411 |
|
412 |
|
413 |
|
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 |
|
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 | export 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 |
|
472 |
|
473 | const compatiblePlaylists = master.playlists.filter(playlist => !Playlist.isIncompatible(playlist));
|
474 |
|
475 |
|
476 |
|
477 | let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
|
478 |
|
479 | if (!enabledPlaylists.length) {
|
480 |
|
481 |
|
482 |
|
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 |
|
497 |
|
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 |
|
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 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 |
|
540 | export const lowestBitrateCompatibleVariantSelector = function() {
|
541 |
|
542 |
|
543 | const playlists = this.playlists.master.playlists.filter(Playlist.isEnabled);
|
544 |
|
545 |
|
546 | stableSort(
|
547 | playlists,
|
548 | (a, b) => comparePlaylistBandwidth(a, b)
|
549 | );
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 | const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.master, playlist).video);
|
557 |
|
558 | return playlistsWithVideo[0] || null;
|
559 | };
|