Home Reference Source

src/loader/m3u8-parser.ts

  1. import * as URLToolkit from 'url-toolkit';
  2.  
  3. import { DateRange } from './date-range';
  4. import { Fragment, Part } from './fragment';
  5. import { LevelDetails } from './level-details';
  6. import { LevelKey } from './level-key';
  7.  
  8. import { AttrList } from '../utils/attr-list';
  9. import { logger } from '../utils/logger';
  10. import type { CodecType } from '../utils/codecs';
  11. import { isCodecType } from '../utils/codecs';
  12. import type {
  13. MediaPlaylist,
  14. AudioGroup,
  15. MediaPlaylistType,
  16. } from '../types/media-playlist';
  17. import type { PlaylistLevelType } from '../types/loader';
  18. import type { LevelAttributes, LevelParsed } from '../types/level';
  19.  
  20. type M3U8ParserFragments = Array<Fragment | null>;
  21.  
  22. // https://regex101.com is your friend
  23. const MASTER_PLAYLIST_REGEX =
  24. /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+/g;
  25. const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
  26.  
  27. const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
  28. [
  29. /#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
  30. /(?!#) *(\S[\S ]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
  31. /#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
  32. /#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
  33. /#.*/.source, // All other non-segment oriented tags will match with all groups empty
  34. ].join('|'),
  35. 'g'
  36. );
  37.  
  38. const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
  39. [
  40. /#(EXTM3U)/.source,
  41. /#EXT-X-(DATERANGE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/
  42. .source,
  43. /#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/
  44. .source,
  45. /#EXT-X-(DISCONTINUITY|ENDLIST|GAP)/.source,
  46. /(#)([^:]*):(.*)/.source,
  47. /(#)(.*)(?:.*)\r?\n?/.source,
  48. ].join('|')
  49. );
  50.  
  51. const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i;
  52.  
  53. function isMP4Url(url: string): boolean {
  54. return MP4_REGEX_SUFFIX.test(URLToolkit.parseURL(url)?.path ?? '');
  55. }
  56.  
  57. export default class M3U8Parser {
  58. static findGroup(
  59. groups: Array<AudioGroup>,
  60. mediaGroupId: string
  61. ): AudioGroup | undefined {
  62. for (let i = 0; i < groups.length; i++) {
  63. const group = groups[i];
  64. if (group.id === mediaGroupId) {
  65. return group;
  66. }
  67. }
  68. }
  69.  
  70. static convertAVC1ToAVCOTI(codec) {
  71. // Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported
  72. const avcdata = codec.split('.');
  73. if (avcdata.length > 2) {
  74. let result = avcdata.shift() + '.';
  75. result += parseInt(avcdata.shift()).toString(16);
  76. result += ('000' + parseInt(avcdata.shift()).toString(16)).slice(-4);
  77. return result;
  78. }
  79. return codec;
  80. }
  81.  
  82. static resolve(url, baseUrl) {
  83. return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
  84. }
  85.  
  86. static parseMasterPlaylist(string: string, baseurl: string) {
  87. const levels: Array<LevelParsed> = [];
  88. const levelsWithKnownCodecs: Array<LevelParsed> = [];
  89. const sessionData: Record<string, AttrList> = {};
  90. let hasSessionData = false;
  91. MASTER_PLAYLIST_REGEX.lastIndex = 0;
  92.  
  93. let result: RegExpExecArray | null;
  94. while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
  95. if (result[1]) {
  96. // '#EXT-X-STREAM-INF' is found, parse level tag in group 1
  97. const attrs = new AttrList(result[1]);
  98. const level: LevelParsed = {
  99. attrs,
  100. bitrate:
  101. attrs.decimalInteger('AVERAGE-BANDWIDTH') ||
  102. attrs.decimalInteger('BANDWIDTH'),
  103. name: attrs.NAME,
  104. url: M3U8Parser.resolve(result[2], baseurl),
  105. };
  106.  
  107. const resolution = attrs.decimalResolution('RESOLUTION');
  108. if (resolution) {
  109. level.width = resolution.width;
  110. level.height = resolution.height;
  111. }
  112.  
  113. setCodecs(
  114. (attrs.CODECS || '').split(/[ ,]+/).filter((c) => c),
  115. level
  116. );
  117.  
  118. if (level.videoCodec && level.videoCodec.indexOf('avc1') !== -1) {
  119. level.videoCodec = M3U8Parser.convertAVC1ToAVCOTI(level.videoCodec);
  120. }
  121.  
  122. if (!level.unknownCodecs?.length) {
  123. levelsWithKnownCodecs.push(level);
  124. }
  125.  
  126. levels.push(level);
  127. } else if (result[3]) {
  128. // '#EXT-X-SESSION-DATA' is found, parse session data in group 3
  129. const sessionAttrs = new AttrList(result[3]);
  130. if (sessionAttrs['DATA-ID']) {
  131. hasSessionData = true;
  132. sessionData[sessionAttrs['DATA-ID']] = sessionAttrs;
  133. }
  134. }
  135. }
  136. // Filter out levels with unknown codecs if it does not remove all levels
  137. const stripUnknownCodecLevels =
  138. levelsWithKnownCodecs.length > 0 &&
  139. levelsWithKnownCodecs.length < levels.length;
  140.  
  141. return {
  142. levels: stripUnknownCodecLevels ? levelsWithKnownCodecs : levels,
  143. sessionData: hasSessionData ? sessionData : null,
  144. };
  145. }
  146.  
  147. static parseMasterPlaylistMedia(
  148. string: string,
  149. baseurl: string,
  150. type: MediaPlaylistType,
  151. groups: Array<AudioGroup> = []
  152. ): Array<MediaPlaylist> {
  153. let result: RegExpExecArray | null;
  154. const medias: Array<MediaPlaylist> = [];
  155. let id = 0;
  156. MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
  157. while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
  158. const attrs = new AttrList(result[1]) as LevelAttributes;
  159. if (attrs.TYPE === type) {
  160. const media: MediaPlaylist = {
  161. attrs,
  162. bitrate: 0,
  163. id: id++,
  164. groupId: attrs['GROUP-ID'],
  165. instreamId: attrs['INSTREAM-ID'],
  166. name: attrs.NAME || attrs.LANGUAGE || '',
  167. type,
  168. default: attrs.bool('DEFAULT'),
  169. autoselect: attrs.bool('AUTOSELECT'),
  170. forced: attrs.bool('FORCED'),
  171. lang: attrs.LANGUAGE,
  172. url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
  173. };
  174.  
  175. if (groups.length) {
  176. // If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track
  177. // If we don't find the track signalled, lets use the first audio groups codec we have
  178. // Acting as a best guess
  179. const groupCodec =
  180. M3U8Parser.findGroup(groups, media.groupId as string) || groups[0];
  181. assignCodec(media, groupCodec, 'audioCodec');
  182. assignCodec(media, groupCodec, 'textCodec');
  183. }
  184.  
  185. medias.push(media);
  186. }
  187. }
  188. return medias;
  189. }
  190.  
  191. static parseLevelPlaylist(
  192. string: string,
  193. baseurl: string,
  194. id: number,
  195. type: PlaylistLevelType,
  196. levelUrlId: number
  197. ): LevelDetails {
  198. const level = new LevelDetails(baseurl);
  199. const fragments: M3U8ParserFragments = level.fragments;
  200. // The most recent init segment seen (applies to all subsequent segments)
  201. let currentInitSegment: Fragment | null = null;
  202. let currentSN = 0;
  203. let currentPart = 0;
  204. let totalduration = 0;
  205. let discontinuityCounter = 0;
  206. let prevFrag: Fragment | null = null;
  207. let frag: Fragment = new Fragment(type, baseurl);
  208. let result: RegExpExecArray | RegExpMatchArray | null;
  209. let i: number;
  210. let levelkey: LevelKey | undefined;
  211. let firstPdtIndex = -1;
  212. let createNextFrag = false;
  213.  
  214. LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
  215. level.m3u8 = string;
  216.  
  217. while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
  218. if (createNextFrag) {
  219. createNextFrag = false;
  220. frag = new Fragment(type, baseurl);
  221. // setup the next fragment for part loading
  222. frag.start = totalduration;
  223. frag.sn = currentSN;
  224. frag.cc = discontinuityCounter;
  225. frag.level = id;
  226. if (currentInitSegment) {
  227. frag.initSegment = currentInitSegment;
  228. frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
  229. currentInitSegment.rawProgramDateTime = null;
  230. }
  231. }
  232.  
  233. const duration = result[1];
  234. if (duration) {
  235. // INF
  236. frag.duration = parseFloat(duration);
  237. // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
  238. const title = (' ' + result[2]).slice(1);
  239. frag.title = title || null;
  240. frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
  241. } else if (result[3]) {
  242. // url
  243. if (Number.isFinite(frag.duration)) {
  244. frag.start = totalduration;
  245. if (levelkey) {
  246. frag.levelkey = levelkey;
  247. }
  248. frag.sn = currentSN;
  249. frag.level = id;
  250. frag.cc = discontinuityCounter;
  251. frag.urlId = levelUrlId;
  252. fragments.push(frag);
  253. // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
  254. frag.relurl = (' ' + result[3]).slice(1);
  255. assignProgramDateTime(frag, prevFrag);
  256. prevFrag = frag;
  257. totalduration += frag.duration;
  258. currentSN++;
  259. currentPart = 0;
  260. createNextFrag = true;
  261. }
  262. } else if (result[4]) {
  263. // X-BYTERANGE
  264. const data = (' ' + result[4]).slice(1);
  265. if (prevFrag) {
  266. frag.setByteRange(data, prevFrag);
  267. } else {
  268. frag.setByteRange(data);
  269. }
  270. } else if (result[5]) {
  271. // PROGRAM-DATE-TIME
  272. // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
  273. frag.rawProgramDateTime = (' ' + result[5]).slice(1);
  274. frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]);
  275. if (firstPdtIndex === -1) {
  276. firstPdtIndex = fragments.length;
  277. }
  278. } else {
  279. result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
  280. if (!result) {
  281. logger.warn('No matches on slow regex match for level playlist!');
  282. continue;
  283. }
  284. for (i = 1; i < result.length; i++) {
  285. if (typeof result[i] !== 'undefined') {
  286. break;
  287. }
  288. }
  289.  
  290. // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
  291. const tag = (' ' + result[i]).slice(1);
  292. const value1 = (' ' + result[i + 1]).slice(1);
  293. const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : '';
  294.  
  295. switch (tag) {
  296. case 'PLAYLIST-TYPE':
  297. level.type = value1.toUpperCase();
  298. break;
  299. case 'MEDIA-SEQUENCE':
  300. currentSN = level.startSN = parseInt(value1);
  301. break;
  302. case 'SKIP': {
  303. const skipAttrs = new AttrList(value1);
  304. const skippedSegments =
  305. skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
  306. if (Number.isFinite(skippedSegments)) {
  307. level.skippedSegments = skippedSegments;
  308. // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
  309. for (let i = skippedSegments; i--; ) {
  310. fragments.unshift(null);
  311. }
  312. currentSN += skippedSegments;
  313. }
  314. const recentlyRemovedDateranges = skipAttrs.enumeratedString(
  315. 'RECENTLY-REMOVED-DATERANGES'
  316. );
  317. if (recentlyRemovedDateranges) {
  318. level.recentlyRemovedDateranges =
  319. recentlyRemovedDateranges.split('\t');
  320. }
  321. break;
  322. }
  323. case 'TARGETDURATION':
  324. level.targetduration = parseFloat(value1);
  325. break;
  326. case 'VERSION':
  327. level.version = parseInt(value1);
  328. break;
  329. case 'EXTM3U':
  330. break;
  331. case 'ENDLIST':
  332. level.live = false;
  333. break;
  334. case '#':
  335. if (value1 || value2) {
  336. frag.tagList.push(value2 ? [value1, value2] : [value1]);
  337. }
  338. break;
  339. case 'DISCONTINUITY':
  340. discontinuityCounter++;
  341. frag.tagList.push(['DIS']);
  342. break;
  343. case 'GAP':
  344. frag.tagList.push([tag]);
  345. break;
  346. case 'BITRATE':
  347. frag.tagList.push([tag, value1]);
  348. break;
  349. case 'DATERANGE': {
  350. const dateRangeAttr = new AttrList(value1);
  351. const dateRange = new DateRange(
  352. dateRangeAttr,
  353. level.dateRanges[dateRangeAttr.ID]
  354. );
  355. if (dateRange.isValid || level.skippedSegments) {
  356. level.dateRanges[dateRange.id] = dateRange;
  357. } else {
  358. logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`);
  359. }
  360. // Add to fragment tag list for backwards compatibility (< v1.2.0)
  361. frag.tagList.push(['EXT-X-DATERANGE', value1]);
  362. break;
  363. }
  364. case 'DISCONTINUITY-SEQUENCE':
  365. discontinuityCounter = parseInt(value1);
  366. break;
  367. case 'KEY': {
  368. // https://tools.ietf.org/html/rfc8216#section-4.3.2.4
  369. const keyAttrs = new AttrList(value1);
  370. const decryptmethod = keyAttrs.enumeratedString('METHOD');
  371. const decrypturi = keyAttrs.URI;
  372. const decryptiv = keyAttrs.hexadecimalInteger('IV');
  373. const decryptkeyformatversions =
  374. keyAttrs.enumeratedString('KEYFORMATVERSIONS');
  375. const decryptkeyid = keyAttrs.enumeratedString('KEYID');
  376. // From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
  377. const decryptkeyformat =
  378. keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
  379.  
  380. const unsupportedKnownKeyformatsInManifest = [
  381. 'com.apple.streamingkeydelivery',
  382. 'com.microsoft.playready',
  383. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', // widevine (v2)
  384. 'com.widevine', // earlier widevine (v1)
  385. ];
  386.  
  387. if (
  388. unsupportedKnownKeyformatsInManifest.indexOf(decryptkeyformat) >
  389. -1
  390. ) {
  391. logger.warn(
  392. `Keyformat ${decryptkeyformat} is not supported from the manifest`
  393. );
  394. continue;
  395. } else if (decryptkeyformat !== 'identity') {
  396. // We are supposed to skip keys we don't understand.
  397. // As we currently only officially support identity keys
  398. // from the manifest we shouldn't save any other key.
  399. continue;
  400. }
  401.  
  402. // TODO: multiple keys can be defined on a fragment, and we need to support this
  403. // for clients that support both playready and widevine
  404. if (decryptmethod) {
  405. // TODO: need to determine if the level key is actually a relative URL
  406. // if it isn't, then we should instead construct the LevelKey using fromURI.
  407. levelkey = LevelKey.fromURL(baseurl, decrypturi);
  408. if (
  409. decrypturi &&
  410. ['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(
  411. decryptmethod
  412. ) >= 0
  413. ) {
  414. levelkey.method = decryptmethod;
  415. levelkey.keyFormat = decryptkeyformat;
  416.  
  417. if (decryptkeyid) {
  418. levelkey.keyID = decryptkeyid;
  419. }
  420.  
  421. if (decryptkeyformatversions) {
  422. levelkey.keyFormatVersions = decryptkeyformatversions;
  423. }
  424.  
  425. // Initialization Vector (IV)
  426. levelkey.iv = decryptiv;
  427. }
  428. }
  429. break;
  430. }
  431. case 'START': {
  432. const startAttrs = new AttrList(value1);
  433. const startTimeOffset =
  434. startAttrs.decimalFloatingPoint('TIME-OFFSET');
  435. // TIME-OFFSET can be 0
  436. if (Number.isFinite(startTimeOffset)) {
  437. level.startTimeOffset = startTimeOffset;
  438. }
  439. break;
  440. }
  441. case 'MAP': {
  442. const mapAttrs = new AttrList(value1);
  443. if (frag.duration) {
  444. // Initial segment tag is after segment duration tag.
  445. // #EXTINF: 6.0
  446. // #EXT-X-MAP:URI="init.mp4
  447. const init = new Fragment(type, baseurl);
  448. setInitSegment(init, mapAttrs, id, levelkey);
  449. currentInitSegment = init;
  450. frag.initSegment = currentInitSegment;
  451. if (
  452. currentInitSegment.rawProgramDateTime &&
  453. !frag.rawProgramDateTime
  454. ) {
  455. frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
  456. }
  457. } else {
  458. // Initial segment tag is before segment duration tag
  459. setInitSegment(frag, mapAttrs, id, levelkey);
  460. currentInitSegment = frag;
  461. createNextFrag = true;
  462. }
  463. break;
  464. }
  465. case 'SERVER-CONTROL': {
  466. const serverControlAttrs = new AttrList(value1);
  467. level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
  468. level.canSkipUntil = serverControlAttrs.optionalFloat(
  469. 'CAN-SKIP-UNTIL',
  470. 0
  471. );
  472. level.canSkipDateRanges =
  473. level.canSkipUntil > 0 &&
  474. serverControlAttrs.bool('CAN-SKIP-DATERANGES');
  475. level.partHoldBack = serverControlAttrs.optionalFloat(
  476. 'PART-HOLD-BACK',
  477. 0
  478. );
  479. level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
  480. break;
  481. }
  482. case 'PART-INF': {
  483. const partInfAttrs = new AttrList(value1);
  484. level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
  485. break;
  486. }
  487. case 'PART': {
  488. let partList = level.partList;
  489. if (!partList) {
  490. partList = level.partList = [];
  491. }
  492. const previousFragmentPart =
  493. currentPart > 0 ? partList[partList.length - 1] : undefined;
  494. const index = currentPart++;
  495. const part = new Part(
  496. new AttrList(value1),
  497. frag,
  498. baseurl,
  499. index,
  500. previousFragmentPart
  501. );
  502. partList.push(part);
  503. frag.duration += part.duration;
  504. break;
  505. }
  506. case 'PRELOAD-HINT': {
  507. const preloadHintAttrs = new AttrList(value1);
  508. level.preloadHint = preloadHintAttrs;
  509. break;
  510. }
  511. case 'RENDITION-REPORT': {
  512. const renditionReportAttrs = new AttrList(value1);
  513. level.renditionReports = level.renditionReports || [];
  514. level.renditionReports.push(renditionReportAttrs);
  515. break;
  516. }
  517. default:
  518. logger.warn(`line parsed but not handled: ${result}`);
  519. break;
  520. }
  521. }
  522. }
  523. if (prevFrag && !prevFrag.relurl) {
  524. fragments.pop();
  525. totalduration -= prevFrag.duration;
  526. if (level.partList) {
  527. level.fragmentHint = prevFrag;
  528. }
  529. } else if (level.partList) {
  530. assignProgramDateTime(frag, prevFrag);
  531. frag.cc = discontinuityCounter;
  532. level.fragmentHint = frag;
  533. }
  534. const fragmentLength = fragments.length;
  535. const firstFragment = fragments[0];
  536. const lastFragment = fragments[fragmentLength - 1];
  537. totalduration += level.skippedSegments * level.targetduration;
  538. if (totalduration > 0 && fragmentLength && lastFragment) {
  539. level.averagetargetduration = totalduration / fragmentLength;
  540. const lastSn = lastFragment.sn;
  541. level.endSN = lastSn !== 'initSegment' ? lastSn : 0;
  542. if (firstFragment) {
  543. level.startCC = firstFragment.cc;
  544. if (!firstFragment.initSegment) {
  545. // this is a bit lurky but HLS really has no other way to tell us
  546. // if the fragments are TS or MP4, except if we download them :/
  547. // but this is to be able to handle SIDX.
  548. if (
  549. level.fragments.every(
  550. (frag) => frag.relurl && isMP4Url(frag.relurl)
  551. )
  552. ) {
  553. logger.warn(
  554. 'MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX'
  555. );
  556. frag = new Fragment(type, baseurl);
  557. frag.relurl = lastFragment.relurl;
  558. frag.level = id;
  559. frag.sn = 'initSegment';
  560. firstFragment.initSegment = frag;
  561. level.needSidxRanges = true;
  562. }
  563. }
  564. }
  565. } else {
  566. level.endSN = 0;
  567. level.startCC = 0;
  568. }
  569. if (level.fragmentHint) {
  570. totalduration += level.fragmentHint.duration;
  571. }
  572. level.totalduration = totalduration;
  573. level.endCC = discontinuityCounter;
  574.  
  575. /**
  576. * Backfill any missing PDT values
  577. * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
  578. * one or more Media Segment URIs, the client SHOULD extrapolate
  579. * backward from that tag (using EXTINF durations and/or media
  580. * timestamps) to associate dates with those segments."
  581. * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
  582. * computed.
  583. */
  584. if (firstPdtIndex > 0) {
  585. backfillProgramDateTimes(fragments, firstPdtIndex);
  586. }
  587.  
  588. return level;
  589. }
  590. }
  591.  
  592. function setCodecs(codecs: Array<string>, level: LevelParsed) {
  593. ['video', 'audio', 'text'].forEach((type: CodecType) => {
  594. const filtered = codecs.filter((codec) => isCodecType(codec, type));
  595. if (filtered.length) {
  596. const preferred = filtered.filter((codec) => {
  597. return (
  598. codec.lastIndexOf('avc1', 0) === 0 ||
  599. codec.lastIndexOf('mp4a', 0) === 0
  600. );
  601. });
  602. level[`${type}Codec`] = preferred.length > 0 ? preferred[0] : filtered[0];
  603.  
  604. // remove from list
  605. codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
  606. }
  607. });
  608.  
  609. level.unknownCodecs = codecs;
  610. }
  611.  
  612. function assignCodec(media, groupItem, codecProperty) {
  613. const codecValue = groupItem[codecProperty];
  614. if (codecValue) {
  615. media[codecProperty] = codecValue;
  616. }
  617. }
  618.  
  619. function backfillProgramDateTimes(
  620. fragments: M3U8ParserFragments,
  621. firstPdtIndex: number
  622. ) {
  623. let fragPrev = fragments[firstPdtIndex] as Fragment;
  624. for (let i = firstPdtIndex; i--; ) {
  625. const frag = fragments[i];
  626. // Exit on delta-playlist skipped segments
  627. if (!frag) {
  628. return;
  629. }
  630. frag.programDateTime =
  631. (fragPrev.programDateTime as number) - frag.duration * 1000;
  632. fragPrev = frag;
  633. }
  634. }
  635.  
  636. function assignProgramDateTime(frag, prevFrag) {
  637. if (frag.rawProgramDateTime) {
  638. frag.programDateTime = Date.parse(frag.rawProgramDateTime);
  639. } else if (prevFrag?.programDateTime) {
  640. frag.programDateTime = prevFrag.endProgramDateTime;
  641. }
  642.  
  643. if (!Number.isFinite(frag.programDateTime)) {
  644. frag.programDateTime = null;
  645. frag.rawProgramDateTime = null;
  646. }
  647. }
  648.  
  649. function setInitSegment(
  650. frag: Fragment,
  651. mapAttrs: AttrList,
  652. id: number,
  653. levelkey: LevelKey | undefined
  654. ) {
  655. frag.relurl = mapAttrs.URI;
  656. if (mapAttrs.BYTERANGE) {
  657. frag.setByteRange(mapAttrs.BYTERANGE);
  658. }
  659. frag.level = id;
  660. frag.sn = 'initSegment';
  661. if (levelkey) {
  662. frag.levelkey = levelkey;
  663. }
  664. frag.initSegment = null;
  665. }