1 | /**
|
2 | * @file playlist.js
|
3 | *
|
4 | * Playlist related utilities.
|
5 | */
|
6 | import videojs from 'video.js';
|
7 | import window from 'global/window';
|
8 | import {isAudioCodec} from '@videojs/vhs-utils/es/codecs.js';
|
9 | import {TIME_FUDGE_FACTOR} from './ranges.js';
|
10 |
|
11 | const {createTimeRange} = videojs;
|
12 |
|
13 | /**
|
14 | * Get the duration of a segment, with special cases for
|
15 | * llhls segments that do not have a duration yet.
|
16 | *
|
17 | * @param {Object} playlist
|
18 | * the playlist that the segment belongs to.
|
19 | * @param {Object} segment
|
20 | * the segment to get a duration for.
|
21 | *
|
22 | * @return {number}
|
23 | * the segment duration
|
24 | */
|
25 | export const segmentDurationWithParts = (playlist, segment) => {
|
26 | // if this isn't a preload segment
|
27 | // then we will have a segment duration that is accurate.
|
28 | if (!segment.preload) {
|
29 | return segment.duration;
|
30 | }
|
31 |
|
32 | // otherwise we have to add up parts and preload hints
|
33 | // to get an up to date duration.
|
34 | let result = 0;
|
35 |
|
36 | (segment.parts || []).forEach(function(p) {
|
37 | result += p.duration;
|
38 | });
|
39 |
|
40 | // for preload hints we have to use partTargetDuration
|
41 | // as they won't even have a duration yet.
|
42 | (segment.preloadHints || []).forEach(function(p) {
|
43 | if (p.type === 'PART') {
|
44 | result += playlist.partTargetDuration;
|
45 | }
|
46 | });
|
47 |
|
48 | return result;
|
49 | };
|
50 | /**
|
51 | * A function to get a combined list of parts and segments with durations
|
52 | * and indexes.
|
53 | *
|
54 | * @param {Playlist} playlist the playlist to get the list for.
|
55 | *
|
56 | * @return {Array} The part/segment list.
|
57 | */
|
58 | export const getPartsAndSegments = (playlist) => (playlist.segments || []).reduce((acc, segment, si) => {
|
59 | if (segment.parts) {
|
60 | segment.parts.forEach(function(part, pi) {
|
61 | acc.push({duration: part.duration, segmentIndex: si, partIndex: pi, part, segment});
|
62 | });
|
63 | } else {
|
64 | acc.push({duration: segment.duration, segmentIndex: si, partIndex: null, segment, part: null});
|
65 | }
|
66 | return acc;
|
67 | }, []);
|
68 |
|
69 | export const getLastParts = (media) => {
|
70 | const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1];
|
71 |
|
72 | return lastSegment && lastSegment.parts || [];
|
73 | };
|
74 |
|
75 | export const getKnownPartCount = ({preloadSegment}) => {
|
76 | if (!preloadSegment) {
|
77 | return;
|
78 | }
|
79 | const {parts, preloadHints} = preloadSegment;
|
80 | let partCount = (preloadHints || [])
|
81 | .reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0);
|
82 |
|
83 | partCount += (parts && parts.length) ? parts.length : 0;
|
84 |
|
85 | return partCount;
|
86 | };
|
87 | /**
|
88 | * Get the number of seconds to delay from the end of a
|
89 | * live playlist.
|
90 | *
|
91 | * @param {Playlist} master the master playlist
|
92 | * @param {Playlist} media the media playlist
|
93 | * @return {number} the hold back in seconds.
|
94 | */
|
95 | export const liveEdgeDelay = (master, media) => {
|
96 | if (media.endList) {
|
97 | return 0;
|
98 | }
|
99 |
|
100 | // dash suggestedPresentationDelay trumps everything
|
101 | if (master && master.suggestedPresentationDelay) {
|
102 | return master.suggestedPresentationDelay;
|
103 | }
|
104 |
|
105 | const hasParts = getLastParts(media).length > 0;
|
106 |
|
107 | // look for "part" delays from ll-hls first
|
108 | if (hasParts && media.serverControl && media.serverControl.partHoldBack) {
|
109 | return media.serverControl.partHoldBack;
|
110 | } else if (hasParts && media.partTargetDuration) {
|
111 | return media.partTargetDuration * 3;
|
112 |
|
113 | // finally look for full segment delays
|
114 | } else if (media.serverControl && media.serverControl.holdBack) {
|
115 | return media.serverControl.holdBack;
|
116 | } else if (media.targetDuration) {
|
117 | return media.targetDuration * 3;
|
118 | }
|
119 |
|
120 | return 0;
|
121 | };
|
122 |
|
123 | /**
|
124 | * walk backward until we find a duration we can use
|
125 | * or return a failure
|
126 | *
|
127 | * @param {Playlist} playlist the playlist to walk through
|
128 | * @param {Number} endSequence the mediaSequence to stop walking on
|
129 | */
|
130 |
|
131 | const backwardDuration = function(playlist, endSequence) {
|
132 | let result = 0;
|
133 | let i = endSequence - playlist.mediaSequence;
|
134 | // if a start time is available for segment immediately following
|
135 | // the interval, use it
|
136 | let segment = playlist.segments[i];
|
137 |
|
138 | // Walk backward until we find the latest segment with timeline
|
139 | // information that is earlier than endSequence
|
140 | if (segment) {
|
141 | if (typeof segment.start !== 'undefined') {
|
142 | return { result: segment.start, precise: true };
|
143 | }
|
144 | if (typeof segment.end !== 'undefined') {
|
145 | return {
|
146 | result: segment.end - segment.duration,
|
147 | precise: true
|
148 | };
|
149 | }
|
150 | }
|
151 | while (i--) {
|
152 | segment = playlist.segments[i];
|
153 | if (typeof segment.end !== 'undefined') {
|
154 | return { result: result + segment.end, precise: true };
|
155 | }
|
156 |
|
157 | result += segmentDurationWithParts(playlist, segment);
|
158 |
|
159 | if (typeof segment.start !== 'undefined') {
|
160 | return { result: result + segment.start, precise: true };
|
161 | }
|
162 | }
|
163 | return { result, precise: false };
|
164 | };
|
165 |
|
166 | /**
|
167 | * walk forward until we find a duration we can use
|
168 | * or return a failure
|
169 | *
|
170 | * @param {Playlist} playlist the playlist to walk through
|
171 | * @param {number} endSequence the mediaSequence to stop walking on
|
172 | */
|
173 | const forwardDuration = function(playlist, endSequence) {
|
174 | let result = 0;
|
175 | let segment;
|
176 | let i = endSequence - playlist.mediaSequence;
|
177 | // Walk forward until we find the earliest segment with timeline
|
178 | // information
|
179 |
|
180 | for (; i < playlist.segments.length; i++) {
|
181 | segment = playlist.segments[i];
|
182 | if (typeof segment.start !== 'undefined') {
|
183 | return {
|
184 | result: segment.start - result,
|
185 | precise: true
|
186 | };
|
187 | }
|
188 |
|
189 | result += segmentDurationWithParts(playlist, segment);
|
190 |
|
191 | if (typeof segment.end !== 'undefined') {
|
192 | return {
|
193 | result: segment.end - result,
|
194 | precise: true
|
195 | };
|
196 | }
|
197 |
|
198 | }
|
199 | // indicate we didn't find a useful duration estimate
|
200 | return { result: -1, precise: false };
|
201 | };
|
202 |
|
203 | /**
|
204 | * Calculate the media duration from the segments associated with a
|
205 | * playlist. The duration of a subinterval of the available segments
|
206 | * may be calculated by specifying an end index.
|
207 | *
|
208 | * @param {Object} playlist a media playlist object
|
209 | * @param {number=} endSequence an exclusive upper boundary
|
210 | * for the playlist. Defaults to playlist length.
|
211 | * @param {number} expired the amount of time that has dropped
|
212 | * off the front of the playlist in a live scenario
|
213 | * @return {number} the duration between the first available segment
|
214 | * and end index.
|
215 | */
|
216 | const intervalDuration = function(playlist, endSequence, expired) {
|
217 | if (typeof endSequence === 'undefined') {
|
218 | endSequence = playlist.mediaSequence + playlist.segments.length;
|
219 | }
|
220 |
|
221 | if (endSequence < playlist.mediaSequence) {
|
222 | return 0;
|
223 | }
|
224 |
|
225 | // do a backward walk to estimate the duration
|
226 | const backward = backwardDuration(playlist, endSequence);
|
227 |
|
228 | if (backward.precise) {
|
229 | // if we were able to base our duration estimate on timing
|
230 | // information provided directly from the Media Source, return
|
231 | // it
|
232 | return backward.result;
|
233 | }
|
234 |
|
235 | // walk forward to see if a precise duration estimate can be made
|
236 | // that way
|
237 | const forward = forwardDuration(playlist, endSequence);
|
238 |
|
239 | if (forward.precise) {
|
240 | // we found a segment that has been buffered and so it's
|
241 | // position is known precisely
|
242 | return forward.result;
|
243 | }
|
244 |
|
245 | // return the less-precise, playlist-based duration estimate
|
246 | return backward.result + expired;
|
247 | };
|
248 |
|
249 | /**
|
250 | * Calculates the duration of a playlist. If a start and end index
|
251 | * are specified, the duration will be for the subset of the media
|
252 | * timeline between those two indices. The total duration for live
|
253 | * playlists is always Infinity.
|
254 | *
|
255 | * @param {Object} playlist a media playlist object
|
256 | * @param {number=} endSequence an exclusive upper
|
257 | * boundary for the playlist. Defaults to the playlist media
|
258 | * sequence number plus its length.
|
259 | * @param {number=} expired the amount of time that has
|
260 | * dropped off the front of the playlist in a live scenario
|
261 | * @return {number} the duration between the start index and end
|
262 | * index.
|
263 | */
|
264 | export const duration = function(playlist, endSequence, expired) {
|
265 | if (!playlist) {
|
266 | return 0;
|
267 | }
|
268 |
|
269 | if (typeof expired !== 'number') {
|
270 | expired = 0;
|
271 | }
|
272 |
|
273 | // if a slice of the total duration is not requested, use
|
274 | // playlist-level duration indicators when they're present
|
275 | if (typeof endSequence === 'undefined') {
|
276 | // if present, use the duration specified in the playlist
|
277 | if (playlist.totalDuration) {
|
278 | return playlist.totalDuration;
|
279 | }
|
280 |
|
281 | // duration should be Infinity for live playlists
|
282 | if (!playlist.endList) {
|
283 | return window.Infinity;
|
284 | }
|
285 | }
|
286 |
|
287 | // calculate the total duration based on the segment durations
|
288 | return intervalDuration(
|
289 | playlist,
|
290 | endSequence,
|
291 | expired
|
292 | );
|
293 | };
|
294 |
|
295 | /**
|
296 | * Calculate the time between two indexes in the current playlist
|
297 | * neight the start- nor the end-index need to be within the current
|
298 | * playlist in which case, the targetDuration of the playlist is used
|
299 | * to approximate the durations of the segments
|
300 | *
|
301 | * @param {Array} options.durationList list to iterate over for durations.
|
302 | * @param {number} options.defaultDuration duration to use for elements before or after the durationList
|
303 | * @param {number} options.startIndex partsAndSegments index to start
|
304 | * @param {number} options.endIndex partsAndSegments index to end.
|
305 | * @return {number} the number of seconds between startIndex and endIndex
|
306 | */
|
307 | export const sumDurations = function({defaultDuration, durationList, startIndex, endIndex}) {
|
308 | let durations = 0;
|
309 |
|
310 | if (startIndex > endIndex) {
|
311 | [startIndex, endIndex] = [endIndex, startIndex];
|
312 | }
|
313 |
|
314 | if (startIndex < 0) {
|
315 | for (let i = startIndex; i < Math.min(0, endIndex); i++) {
|
316 | durations += defaultDuration;
|
317 | }
|
318 | startIndex = 0;
|
319 | }
|
320 |
|
321 | for (let i = startIndex; i < endIndex; i++) {
|
322 | durations += durationList[i].duration;
|
323 | }
|
324 |
|
325 | return durations;
|
326 | };
|
327 |
|
328 | /**
|
329 | * Calculates the playlist end time
|
330 | *
|
331 | * @param {Object} playlist a media playlist object
|
332 | * @param {number=} expired the amount of time that has
|
333 | * dropped off the front of the playlist in a live scenario
|
334 | * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
|
335 | * playlist end calculation should consider the safe live end
|
336 | * (truncate the playlist end by three segments). This is normally
|
337 | * used for calculating the end of the playlist's seekable range.
|
338 | * This takes into account the value of liveEdgePadding.
|
339 | * Setting liveEdgePadding to 0 is equivalent to setting this to false.
|
340 | * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds.
|
341 | * If this is provided, it is used in the safe live end calculation.
|
342 | * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent.
|
343 | * Corresponds to suggestedPresentationDelay in DASH manifests.
|
344 | * @return {number} the end time of playlist
|
345 | * @function playlistEnd
|
346 | */
|
347 | export const playlistEnd = function(playlist, expired, useSafeLiveEnd, liveEdgePadding) {
|
348 | if (!playlist || !playlist.segments) {
|
349 | return null;
|
350 | }
|
351 | if (playlist.endList) {
|
352 | return duration(playlist);
|
353 | }
|
354 |
|
355 | if (expired === null) {
|
356 | return null;
|
357 | }
|
358 |
|
359 | expired = expired || 0;
|
360 |
|
361 | let lastSegmentEndTime = intervalDuration(
|
362 | playlist,
|
363 | playlist.mediaSequence + playlist.segments.length,
|
364 | expired
|
365 | );
|
366 |
|
367 | if (useSafeLiveEnd) {
|
368 | liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist);
|
369 | lastSegmentEndTime -= liveEdgePadding;
|
370 | }
|
371 |
|
372 | // don't return a time less than zero
|
373 | return Math.max(0, lastSegmentEndTime);
|
374 | };
|
375 |
|
376 | /**
|
377 | * Calculates the interval of time that is currently seekable in a
|
378 | * playlist. The returned time ranges are relative to the earliest
|
379 | * moment in the specified playlist that is still available. A full
|
380 | * seekable implementation for live streams would need to offset
|
381 | * these values by the duration of content that has expired from the
|
382 | * stream.
|
383 | *
|
384 | * @param {Object} playlist a media playlist object
|
385 | * dropped off the front of the playlist in a live scenario
|
386 | * @param {number=} expired the amount of time that has
|
387 | * dropped off the front of the playlist in a live scenario
|
388 | * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds.
|
389 | * Corresponds to suggestedPresentationDelay in DASH manifests.
|
390 | * @return {TimeRanges} the periods of time that are valid targets
|
391 | * for seeking
|
392 | */
|
393 | export const seekable = function(playlist, expired, liveEdgePadding) {
|
394 | const useSafeLiveEnd = true;
|
395 | const seekableStart = expired || 0;
|
396 | const seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding);
|
397 |
|
398 | if (seekableEnd === null) {
|
399 | return createTimeRange();
|
400 | }
|
401 | return createTimeRange(seekableStart, seekableEnd);
|
402 | };
|
403 |
|
404 | /**
|
405 | * Determine the index and estimated starting time of the segment that
|
406 | * contains a specified playback position in a media playlist.
|
407 | *
|
408 | * @param {Object} options.playlist the media playlist to query
|
409 | * @param {number} options.currentTime The number of seconds since the earliest
|
410 | * possible position to determine the containing segment for
|
411 | * @param {number} options.startTime the time when the segment/part starts
|
412 | * @param {number} options.startingSegmentIndex the segment index to start looking at.
|
413 | * @param {number?} [options.startingPartIndex] the part index to look at within the segment.
|
414 | *
|
415 | * @return {Object} an object with partIndex, segmentIndex, and startTime.
|
416 | */
|
417 | export const getMediaInfoForTime = function({
|
418 | playlist,
|
419 | currentTime,
|
420 | startingSegmentIndex,
|
421 | startingPartIndex,
|
422 | startTime,
|
423 | experimentalExactManifestTimings
|
424 | }) {
|
425 |
|
426 | let time = currentTime - startTime;
|
427 | const partsAndSegments = getPartsAndSegments(playlist);
|
428 |
|
429 | let startIndex = 0;
|
430 |
|
431 | for (let i = 0; i < partsAndSegments.length; i++) {
|
432 | const partAndSegment = partsAndSegments[i];
|
433 |
|
434 | if (startingSegmentIndex !== partAndSegment.segmentIndex) {
|
435 | continue;
|
436 | }
|
437 |
|
438 | // skip this if part index does not match.
|
439 | if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) {
|
440 | continue;
|
441 | }
|
442 |
|
443 | startIndex = i;
|
444 | break;
|
445 | }
|
446 |
|
447 | if (time < 0) {
|
448 | // Walk backward from startIndex in the playlist, adding durations
|
449 | // until we find a segment that contains `time` and return it
|
450 | if (startIndex > 0) {
|
451 | for (let i = startIndex - 1; i >= 0; i--) {
|
452 | const partAndSegment = partsAndSegments[i];
|
453 |
|
454 | time += partAndSegment.duration;
|
455 |
|
456 | if (experimentalExactManifestTimings) {
|
457 | if (time < 0) {
|
458 | continue;
|
459 | }
|
460 | } else if ((time + TIME_FUDGE_FACTOR) <= 0) {
|
461 | continue;
|
462 | }
|
463 | return {
|
464 | partIndex: partAndSegment.partIndex,
|
465 | segmentIndex: partAndSegment.segmentIndex,
|
466 | startTime: startTime - sumDurations({
|
467 | defaultDuration: playlist.targetDuration,
|
468 | durationList: partsAndSegments,
|
469 | startIndex,
|
470 | endIndex: i
|
471 | })
|
472 | };
|
473 | }
|
474 | }
|
475 |
|
476 | // We were unable to find a good segment within the playlist
|
477 | // so select the first segment
|
478 | return {
|
479 | partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
|
480 | segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
|
481 | startTime: currentTime
|
482 | };
|
483 | }
|
484 |
|
485 | // When startIndex is negative, we first walk forward to first segment
|
486 | // adding target durations. If we "run out of time" before getting to
|
487 | // the first segment, return the first segment
|
488 | if (startIndex < 0) {
|
489 | for (let i = startIndex; i < 0; i++) {
|
490 | time -= playlist.targetDuration;
|
491 |
|
492 | if (time < 0) {
|
493 | return {
|
494 | partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
|
495 | segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
|
496 | startTime: currentTime
|
497 | };
|
498 | }
|
499 | }
|
500 | startIndex = 0;
|
501 | }
|
502 |
|
503 | // Walk forward from startIndex in the playlist, subtracting durations
|
504 | // until we find a segment that contains `time` and return it
|
505 | for (let i = startIndex; i < partsAndSegments.length; i++) {
|
506 | const partAndSegment = partsAndSegments[i];
|
507 |
|
508 | time -= partAndSegment.duration;
|
509 |
|
510 | if (experimentalExactManifestTimings) {
|
511 | if (time > 0) {
|
512 | continue;
|
513 | }
|
514 | } else if ((time - TIME_FUDGE_FACTOR) >= 0) {
|
515 | continue;
|
516 | }
|
517 |
|
518 | return {
|
519 | partIndex: partAndSegment.partIndex,
|
520 | segmentIndex: partAndSegment.segmentIndex,
|
521 | startTime: startTime + sumDurations({
|
522 | defaultDuration: playlist.targetDuration,
|
523 | durationList: partsAndSegments,
|
524 | startIndex,
|
525 | endIndex: i
|
526 | })
|
527 | };
|
528 | }
|
529 |
|
530 | // We are out of possible candidates so load the last one...
|
531 | return {
|
532 | segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex,
|
533 | partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex,
|
534 | startTime: currentTime
|
535 | };
|
536 | };
|
537 |
|
538 | /**
|
539 | * Check whether the playlist is blacklisted or not.
|
540 | *
|
541 | * @param {Object} playlist the media playlist object
|
542 | * @return {boolean} whether the playlist is blacklisted or not
|
543 | * @function isBlacklisted
|
544 | */
|
545 | export const isBlacklisted = function(playlist) {
|
546 | return playlist.excludeUntil && playlist.excludeUntil > Date.now();
|
547 | };
|
548 |
|
549 | /**
|
550 | * Check whether the playlist is compatible with current playback configuration or has
|
551 | * been blacklisted permanently for being incompatible.
|
552 | *
|
553 | * @param {Object} playlist the media playlist object
|
554 | * @return {boolean} whether the playlist is incompatible or not
|
555 | * @function isIncompatible
|
556 | */
|
557 | export const isIncompatible = function(playlist) {
|
558 | return playlist.excludeUntil && playlist.excludeUntil === Infinity;
|
559 | };
|
560 |
|
561 | /**
|
562 | * Check whether the playlist is enabled or not.
|
563 | *
|
564 | * @param {Object} playlist the media playlist object
|
565 | * @return {boolean} whether the playlist is enabled or not
|
566 | * @function isEnabled
|
567 | */
|
568 | export const isEnabled = function(playlist) {
|
569 | const blacklisted = isBlacklisted(playlist);
|
570 |
|
571 | return (!playlist.disabled && !blacklisted);
|
572 | };
|
573 |
|
574 | /**
|
575 | * Check whether the playlist has been manually disabled through the representations api.
|
576 | *
|
577 | * @param {Object} playlist the media playlist object
|
578 | * @return {boolean} whether the playlist is disabled manually or not
|
579 | * @function isDisabled
|
580 | */
|
581 | export const isDisabled = function(playlist) {
|
582 | return playlist.disabled;
|
583 | };
|
584 |
|
585 | /**
|
586 | * Returns whether the current playlist is an AES encrypted HLS stream
|
587 | *
|
588 | * @return {boolean} true if it's an AES encrypted HLS stream
|
589 | */
|
590 | export const isAes = function(media) {
|
591 | for (let i = 0; i < media.segments.length; i++) {
|
592 | if (media.segments[i].key) {
|
593 | return true;
|
594 | }
|
595 | }
|
596 | return false;
|
597 | };
|
598 |
|
599 | /**
|
600 | * Checks if the playlist has a value for the specified attribute
|
601 | *
|
602 | * @param {string} attr
|
603 | * Attribute to check for
|
604 | * @param {Object} playlist
|
605 | * The media playlist object
|
606 | * @return {boolean}
|
607 | * Whether the playlist contains a value for the attribute or not
|
608 | * @function hasAttribute
|
609 | */
|
610 | export const hasAttribute = function(attr, playlist) {
|
611 | return playlist.attributes && playlist.attributes[attr];
|
612 | };
|
613 |
|
614 | /**
|
615 | * Estimates the time required to complete a segment download from the specified playlist
|
616 | *
|
617 | * @param {number} segmentDuration
|
618 | * Duration of requested segment
|
619 | * @param {number} bandwidth
|
620 | * Current measured bandwidth of the player
|
621 | * @param {Object} playlist
|
622 | * The media playlist object
|
623 | * @param {number=} bytesReceived
|
624 | * Number of bytes already received for the request. Defaults to 0
|
625 | * @return {number|NaN}
|
626 | * The estimated time to request the segment. NaN if bandwidth information for
|
627 | * the given playlist is unavailable
|
628 | * @function estimateSegmentRequestTime
|
629 | */
|
630 | export const estimateSegmentRequestTime = function(
|
631 | segmentDuration,
|
632 | bandwidth,
|
633 | playlist,
|
634 | bytesReceived = 0
|
635 | ) {
|
636 | if (!hasAttribute('BANDWIDTH', playlist)) {
|
637 | return NaN;
|
638 | }
|
639 |
|
640 | const size = segmentDuration * playlist.attributes.BANDWIDTH;
|
641 |
|
642 | return (size - (bytesReceived * 8)) / bandwidth;
|
643 | };
|
644 |
|
645 | /*
|
646 | * Returns whether the current playlist is the lowest rendition
|
647 | *
|
648 | * @return {Boolean} true if on lowest rendition
|
649 | */
|
650 | export const isLowestEnabledRendition = (master, media) => {
|
651 | if (master.playlists.length === 1) {
|
652 | return true;
|
653 | }
|
654 |
|
655 | const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE;
|
656 |
|
657 | return (master.playlists.filter((playlist) => {
|
658 | if (!isEnabled(playlist)) {
|
659 | return false;
|
660 | }
|
661 |
|
662 | return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth;
|
663 |
|
664 | }).length === 0);
|
665 | };
|
666 |
|
667 | export const playlistMatch = (a, b) => {
|
668 | // both playlits are null
|
669 | // or only one playlist is non-null
|
670 | // no match
|
671 | if (!a && !b || (!a && b) || (a && !b)) {
|
672 | return false;
|
673 | }
|
674 |
|
675 | // playlist objects are the same, match
|
676 | if (a === b) {
|
677 | return true;
|
678 | }
|
679 |
|
680 | // first try to use id as it should be the most
|
681 | // accurate
|
682 | if (a.id && b.id && a.id === b.id) {
|
683 | return true;
|
684 | }
|
685 |
|
686 | // next try to use reslovedUri as it should be the
|
687 | // second most accurate.
|
688 | if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) {
|
689 | return true;
|
690 | }
|
691 |
|
692 | // finally try to use uri as it should be accurate
|
693 | // but might miss a few cases for relative uris
|
694 | if (a.uri && b.uri && a.uri === b.uri) {
|
695 | return true;
|
696 | }
|
697 |
|
698 | return false;
|
699 | };
|
700 |
|
701 | const someAudioVariant = function(master, callback) {
|
702 | const AUDIO = master && master.mediaGroups && master.mediaGroups.AUDIO || {};
|
703 | let found = false;
|
704 |
|
705 | for (const groupName in AUDIO) {
|
706 | for (const label in AUDIO[groupName]) {
|
707 | found = callback(AUDIO[groupName][label]);
|
708 |
|
709 | if (found) {
|
710 | break;
|
711 | }
|
712 | }
|
713 |
|
714 | if (found) {
|
715 | break;
|
716 | }
|
717 | }
|
718 |
|
719 | return !!found;
|
720 | };
|
721 |
|
722 | export const isAudioOnly = (master) => {
|
723 | // we are audio only if we have no main playlists but do
|
724 | // have media group playlists.
|
725 | if (!master || !master.playlists || !master.playlists.length) {
|
726 | // without audio variants or playlists this
|
727 | // is not an audio only master.
|
728 | const found = someAudioVariant(master, (variant) =>
|
729 | (variant.playlists && variant.playlists.length) || variant.uri);
|
730 |
|
731 | return found;
|
732 | }
|
733 |
|
734 | // if every playlist has only an audio codec it is audio only
|
735 | for (let i = 0; i < master.playlists.length; i++) {
|
736 | const playlist = master.playlists[i];
|
737 | const CODECS = playlist.attributes && playlist.attributes.CODECS;
|
738 |
|
739 | // all codecs are audio, this is an audio playlist.
|
740 | if (CODECS && CODECS.split(',').every((c) => isAudioCodec(c))) {
|
741 | continue;
|
742 | }
|
743 |
|
744 | // playlist is in an audio group it is audio only
|
745 | const found = someAudioVariant(master, (variant) => playlistMatch(playlist, variant));
|
746 |
|
747 | if (found) {
|
748 | continue;
|
749 | }
|
750 |
|
751 | // if we make it here this playlist isn't audio and we
|
752 | // are not audio only
|
753 | return false;
|
754 | }
|
755 |
|
756 | // if we make it past every playlist without returning, then
|
757 | // this is an audio only playlist.
|
758 | return true;
|
759 | };
|
760 |
|
761 | // exports
|
762 | export default {
|
763 | liveEdgeDelay,
|
764 | duration,
|
765 | seekable,
|
766 | getMediaInfoForTime,
|
767 | isEnabled,
|
768 | isDisabled,
|
769 | isBlacklisted,
|
770 | isIncompatible,
|
771 | playlistEnd,
|
772 | isAes,
|
773 | hasAttribute,
|
774 | estimateSegmentRequestTime,
|
775 | isLowestEnabledRendition,
|
776 | isAudioOnly,
|
777 | playlistMatch,
|
778 | segmentDurationWithParts
|
779 | };
|