playlist.js 24 KB

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