UNPKG

15.1 kBJavaScriptView Raw
1import Config from './config';
2import Playlist from './playlist';
3import { 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 */
17const 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 */
39const 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 */
62export 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 */
87export 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 */
130export 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 */
259export 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 */
280export 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 */
328export 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 */
406export 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};