UNPKG

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