m3u8-parser.cjs.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696
  1. /*! @name m3u8-parser @version 7.1.0 @license Apache-2.0 */
  2. 'use strict';
  3. Object.defineProperty(exports, '__esModule', { value: true });
  4. var Stream = require('@videojs/vhs-utils/cjs/stream.js');
  5. var _extends = require('@babel/runtime/helpers/extends');
  6. var decodeB64ToUint8Array = require('@videojs/vhs-utils/cjs/decode-b64-to-uint8-array.js');
  7. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  8. var Stream__default = /*#__PURE__*/_interopDefaultLegacy(Stream);
  9. var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
  10. var decodeB64ToUint8Array__default = /*#__PURE__*/_interopDefaultLegacy(decodeB64ToUint8Array);
  11. /**
  12. * @file m3u8/line-stream.js
  13. */
  14. /**
  15. * A stream that buffers string input and generates a `data` event for each
  16. * line.
  17. *
  18. * @class LineStream
  19. * @extends Stream
  20. */
  21. class LineStream extends Stream__default["default"] {
  22. constructor() {
  23. super();
  24. this.buffer = '';
  25. }
  26. /**
  27. * Add new data to be parsed.
  28. *
  29. * @param {string} data the text to process
  30. */
  31. push(data) {
  32. let nextNewline;
  33. this.buffer += data;
  34. nextNewline = this.buffer.indexOf('\n');
  35. for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
  36. this.trigger('data', this.buffer.substring(0, nextNewline));
  37. this.buffer = this.buffer.substring(nextNewline + 1);
  38. }
  39. }
  40. }
  41. const TAB = String.fromCharCode(0x09);
  42. const parseByterange = function (byterangeString) {
  43. // optionally match and capture 0+ digits before `@`
  44. // optionally match and capture 0+ digits after `@`
  45. const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
  46. const result = {};
  47. if (match[1]) {
  48. result.length = parseInt(match[1], 10);
  49. }
  50. if (match[2]) {
  51. result.offset = parseInt(match[2], 10);
  52. }
  53. return result;
  54. };
  55. /**
  56. * "forgiving" attribute list psuedo-grammar:
  57. * attributes -> keyvalue (',' keyvalue)*
  58. * keyvalue -> key '=' value
  59. * key -> [^=]*
  60. * value -> '"' [^"]* '"' | [^,]*
  61. */
  62. const attributeSeparator = function () {
  63. const key = '[^=]*';
  64. const value = '"[^"]*"|[^,]*';
  65. const keyvalue = '(?:' + key + ')=(?:' + value + ')';
  66. return new RegExp('(?:^|,)(' + keyvalue + ')');
  67. };
  68. /**
  69. * Parse attributes from a line given the separator
  70. *
  71. * @param {string} attributes the attribute line to parse
  72. */
  73. const parseAttributes = function (attributes) {
  74. const result = {};
  75. if (!attributes) {
  76. return result;
  77. } // split the string using attributes as the separator
  78. const attrs = attributes.split(attributeSeparator());
  79. let i = attrs.length;
  80. let attr;
  81. while (i--) {
  82. // filter out unmatched portions of the string
  83. if (attrs[i] === '') {
  84. continue;
  85. } // split the key and value
  86. attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value
  87. attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
  88. attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
  89. attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
  90. result[attr[0]] = attr[1];
  91. }
  92. return result;
  93. };
  94. /**
  95. * A line-level M3U8 parser event stream. It expects to receive input one
  96. * line at a time and performs a context-free parse of its contents. A stream
  97. * interpretation of a manifest can be useful if the manifest is expected to
  98. * be too large to fit comfortably into memory or the entirety of the input
  99. * is not immediately available. Otherwise, it's probably much easier to work
  100. * with a regular `Parser` object.
  101. *
  102. * Produces `data` events with an object that captures the parser's
  103. * interpretation of the input. That object has a property `tag` that is one
  104. * of `uri`, `comment`, or `tag`. URIs only have a single additional
  105. * property, `line`, which captures the entirety of the input without
  106. * interpretation. Comments similarly have a single additional property
  107. * `text` which is the input without the leading `#`.
  108. *
  109. * Tags always have a property `tagType` which is the lower-cased version of
  110. * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
  111. * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
  112. * tags are given the tag type `unknown` and a single additional property
  113. * `data` with the remainder of the input.
  114. *
  115. * @class ParseStream
  116. * @extends Stream
  117. */
  118. class ParseStream extends Stream__default["default"] {
  119. constructor() {
  120. super();
  121. this.customParsers = [];
  122. this.tagMappers = [];
  123. }
  124. /**
  125. * Parses an additional line of input.
  126. *
  127. * @param {string} line a single line of an M3U8 file to parse
  128. */
  129. push(line) {
  130. let match;
  131. let event; // strip whitespace
  132. line = line.trim();
  133. if (line.length === 0) {
  134. // ignore empty lines
  135. return;
  136. } // URIs
  137. if (line[0] !== '#') {
  138. this.trigger('data', {
  139. type: 'uri',
  140. uri: line
  141. });
  142. return;
  143. } // map tags
  144. const newLines = this.tagMappers.reduce((acc, mapper) => {
  145. const mappedLine = mapper(line); // skip if unchanged
  146. if (mappedLine === line) {
  147. return acc;
  148. }
  149. return acc.concat([mappedLine]);
  150. }, [line]);
  151. newLines.forEach(newLine => {
  152. for (let i = 0; i < this.customParsers.length; i++) {
  153. if (this.customParsers[i].call(this, newLine)) {
  154. return;
  155. }
  156. } // Comments
  157. if (newLine.indexOf('#EXT') !== 0) {
  158. this.trigger('data', {
  159. type: 'comment',
  160. text: newLine.slice(1)
  161. });
  162. return;
  163. } // strip off any carriage returns here so the regex matching
  164. // doesn't have to account for them.
  165. newLine = newLine.replace('\r', ''); // Tags
  166. match = /^#EXTM3U/.exec(newLine);
  167. if (match) {
  168. this.trigger('data', {
  169. type: 'tag',
  170. tagType: 'm3u'
  171. });
  172. return;
  173. }
  174. match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine);
  175. if (match) {
  176. event = {
  177. type: 'tag',
  178. tagType: 'inf'
  179. };
  180. if (match[1]) {
  181. event.duration = parseFloat(match[1]);
  182. }
  183. if (match[2]) {
  184. event.title = match[2];
  185. }
  186. this.trigger('data', event);
  187. return;
  188. }
  189. match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine);
  190. if (match) {
  191. event = {
  192. type: 'tag',
  193. tagType: 'targetduration'
  194. };
  195. if (match[1]) {
  196. event.duration = parseInt(match[1], 10);
  197. }
  198. this.trigger('data', event);
  199. return;
  200. }
  201. match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine);
  202. if (match) {
  203. event = {
  204. type: 'tag',
  205. tagType: 'version'
  206. };
  207. if (match[1]) {
  208. event.version = parseInt(match[1], 10);
  209. }
  210. this.trigger('data', event);
  211. return;
  212. }
  213. match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
  214. if (match) {
  215. event = {
  216. type: 'tag',
  217. tagType: 'media-sequence'
  218. };
  219. if (match[1]) {
  220. event.number = parseInt(match[1], 10);
  221. }
  222. this.trigger('data', event);
  223. return;
  224. }
  225. match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
  226. if (match) {
  227. event = {
  228. type: 'tag',
  229. tagType: 'discontinuity-sequence'
  230. };
  231. if (match[1]) {
  232. event.number = parseInt(match[1], 10);
  233. }
  234. this.trigger('data', event);
  235. return;
  236. }
  237. match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine);
  238. if (match) {
  239. event = {
  240. type: 'tag',
  241. tagType: 'playlist-type'
  242. };
  243. if (match[1]) {
  244. event.playlistType = match[1];
  245. }
  246. this.trigger('data', event);
  247. return;
  248. }
  249. match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine);
  250. if (match) {
  251. event = _extends__default["default"](parseByterange(match[1]), {
  252. type: 'tag',
  253. tagType: 'byterange'
  254. });
  255. this.trigger('data', event);
  256. return;
  257. }
  258. match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine);
  259. if (match) {
  260. event = {
  261. type: 'tag',
  262. tagType: 'allow-cache'
  263. };
  264. if (match[1]) {
  265. event.allowed = !/NO/.test(match[1]);
  266. }
  267. this.trigger('data', event);
  268. return;
  269. }
  270. match = /^#EXT-X-MAP:(.*)$/.exec(newLine);
  271. if (match) {
  272. event = {
  273. type: 'tag',
  274. tagType: 'map'
  275. };
  276. if (match[1]) {
  277. const attributes = parseAttributes(match[1]);
  278. if (attributes.URI) {
  279. event.uri = attributes.URI;
  280. }
  281. if (attributes.BYTERANGE) {
  282. event.byterange = parseByterange(attributes.BYTERANGE);
  283. }
  284. }
  285. this.trigger('data', event);
  286. return;
  287. }
  288. match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine);
  289. if (match) {
  290. event = {
  291. type: 'tag',
  292. tagType: 'stream-inf'
  293. };
  294. if (match[1]) {
  295. event.attributes = parseAttributes(match[1]);
  296. if (event.attributes.RESOLUTION) {
  297. const split = event.attributes.RESOLUTION.split('x');
  298. const resolution = {};
  299. if (split[0]) {
  300. resolution.width = parseInt(split[0], 10);
  301. }
  302. if (split[1]) {
  303. resolution.height = parseInt(split[1], 10);
  304. }
  305. event.attributes.RESOLUTION = resolution;
  306. }
  307. if (event.attributes.BANDWIDTH) {
  308. event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
  309. }
  310. if (event.attributes['FRAME-RATE']) {
  311. event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
  312. }
  313. if (event.attributes['PROGRAM-ID']) {
  314. event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
  315. }
  316. }
  317. this.trigger('data', event);
  318. return;
  319. }
  320. match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine);
  321. if (match) {
  322. event = {
  323. type: 'tag',
  324. tagType: 'media'
  325. };
  326. if (match[1]) {
  327. event.attributes = parseAttributes(match[1]);
  328. }
  329. this.trigger('data', event);
  330. return;
  331. }
  332. match = /^#EXT-X-ENDLIST/.exec(newLine);
  333. if (match) {
  334. this.trigger('data', {
  335. type: 'tag',
  336. tagType: 'endlist'
  337. });
  338. return;
  339. }
  340. match = /^#EXT-X-DISCONTINUITY/.exec(newLine);
  341. if (match) {
  342. this.trigger('data', {
  343. type: 'tag',
  344. tagType: 'discontinuity'
  345. });
  346. return;
  347. }
  348. match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine);
  349. if (match) {
  350. event = {
  351. type: 'tag',
  352. tagType: 'program-date-time'
  353. };
  354. if (match[1]) {
  355. event.dateTimeString = match[1];
  356. event.dateTimeObject = new Date(match[1]);
  357. }
  358. this.trigger('data', event);
  359. return;
  360. }
  361. match = /^#EXT-X-KEY:(.*)$/.exec(newLine);
  362. if (match) {
  363. event = {
  364. type: 'tag',
  365. tagType: 'key'
  366. };
  367. if (match[1]) {
  368. event.attributes = parseAttributes(match[1]); // parse the IV string into a Uint32Array
  369. if (event.attributes.IV) {
  370. if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
  371. event.attributes.IV = event.attributes.IV.substring(2);
  372. }
  373. event.attributes.IV = event.attributes.IV.match(/.{8}/g);
  374. event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
  375. event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
  376. event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
  377. event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
  378. event.attributes.IV = new Uint32Array(event.attributes.IV);
  379. }
  380. }
  381. this.trigger('data', event);
  382. return;
  383. }
  384. match = /^#EXT-X-START:(.*)$/.exec(newLine);
  385. if (match) {
  386. event = {
  387. type: 'tag',
  388. tagType: 'start'
  389. };
  390. if (match[1]) {
  391. event.attributes = parseAttributes(match[1]);
  392. event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']);
  393. event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE);
  394. }
  395. this.trigger('data', event);
  396. return;
  397. }
  398. match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine);
  399. if (match) {
  400. event = {
  401. type: 'tag',
  402. tagType: 'cue-out-cont'
  403. };
  404. if (match[1]) {
  405. event.data = match[1];
  406. } else {
  407. event.data = '';
  408. }
  409. this.trigger('data', event);
  410. return;
  411. }
  412. match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine);
  413. if (match) {
  414. event = {
  415. type: 'tag',
  416. tagType: 'cue-out'
  417. };
  418. if (match[1]) {
  419. event.data = match[1];
  420. } else {
  421. event.data = '';
  422. }
  423. this.trigger('data', event);
  424. return;
  425. }
  426. match = /^#EXT-X-CUE-IN:(.*)?$/.exec(newLine);
  427. if (match) {
  428. event = {
  429. type: 'tag',
  430. tagType: 'cue-in'
  431. };
  432. if (match[1]) {
  433. event.data = match[1];
  434. } else {
  435. event.data = '';
  436. }
  437. this.trigger('data', event);
  438. return;
  439. }
  440. match = /^#EXT-X-SKIP:(.*)$/.exec(newLine);
  441. if (match && match[1]) {
  442. event = {
  443. type: 'tag',
  444. tagType: 'skip'
  445. };
  446. event.attributes = parseAttributes(match[1]);
  447. if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) {
  448. event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10);
  449. }
  450. if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) {
  451. event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB);
  452. }
  453. this.trigger('data', event);
  454. return;
  455. }
  456. match = /^#EXT-X-PART:(.*)$/.exec(newLine);
  457. if (match && match[1]) {
  458. event = {
  459. type: 'tag',
  460. tagType: 'part'
  461. };
  462. event.attributes = parseAttributes(match[1]);
  463. ['DURATION'].forEach(function (key) {
  464. if (event.attributes.hasOwnProperty(key)) {
  465. event.attributes[key] = parseFloat(event.attributes[key]);
  466. }
  467. });
  468. ['INDEPENDENT', 'GAP'].forEach(function (key) {
  469. if (event.attributes.hasOwnProperty(key)) {
  470. event.attributes[key] = /YES/.test(event.attributes[key]);
  471. }
  472. });
  473. if (event.attributes.hasOwnProperty('BYTERANGE')) {
  474. event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
  475. }
  476. this.trigger('data', event);
  477. return;
  478. }
  479. match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine);
  480. if (match && match[1]) {
  481. event = {
  482. type: 'tag',
  483. tagType: 'server-control'
  484. };
  485. event.attributes = parseAttributes(match[1]);
  486. ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) {
  487. if (event.attributes.hasOwnProperty(key)) {
  488. event.attributes[key] = parseFloat(event.attributes[key]);
  489. }
  490. });
  491. ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) {
  492. if (event.attributes.hasOwnProperty(key)) {
  493. event.attributes[key] = /YES/.test(event.attributes[key]);
  494. }
  495. });
  496. this.trigger('data', event);
  497. return;
  498. }
  499. match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine);
  500. if (match && match[1]) {
  501. event = {
  502. type: 'tag',
  503. tagType: 'part-inf'
  504. };
  505. event.attributes = parseAttributes(match[1]);
  506. ['PART-TARGET'].forEach(function (key) {
  507. if (event.attributes.hasOwnProperty(key)) {
  508. event.attributes[key] = parseFloat(event.attributes[key]);
  509. }
  510. });
  511. this.trigger('data', event);
  512. return;
  513. }
  514. match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine);
  515. if (match && match[1]) {
  516. event = {
  517. type: 'tag',
  518. tagType: 'preload-hint'
  519. };
  520. event.attributes = parseAttributes(match[1]);
  521. ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) {
  522. if (event.attributes.hasOwnProperty(key)) {
  523. event.attributes[key] = parseInt(event.attributes[key], 10);
  524. const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
  525. event.attributes.byterange = event.attributes.byterange || {};
  526. event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object.
  527. delete event.attributes[key];
  528. }
  529. });
  530. this.trigger('data', event);
  531. return;
  532. }
  533. match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine);
  534. if (match && match[1]) {
  535. event = {
  536. type: 'tag',
  537. tagType: 'rendition-report'
  538. };
  539. event.attributes = parseAttributes(match[1]);
  540. ['LAST-MSN', 'LAST-PART'].forEach(function (key) {
  541. if (event.attributes.hasOwnProperty(key)) {
  542. event.attributes[key] = parseInt(event.attributes[key], 10);
  543. }
  544. });
  545. this.trigger('data', event);
  546. return;
  547. }
  548. match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine);
  549. if (match && match[1]) {
  550. event = {
  551. type: 'tag',
  552. tagType: 'daterange'
  553. };
  554. event.attributes = parseAttributes(match[1]);
  555. ['ID', 'CLASS'].forEach(function (key) {
  556. if (event.attributes.hasOwnProperty(key)) {
  557. event.attributes[key] = String(event.attributes[key]);
  558. }
  559. });
  560. ['START-DATE', 'END-DATE'].forEach(function (key) {
  561. if (event.attributes.hasOwnProperty(key)) {
  562. event.attributes[key] = new Date(event.attributes[key]);
  563. }
  564. });
  565. ['DURATION', 'PLANNED-DURATION'].forEach(function (key) {
  566. if (event.attributes.hasOwnProperty(key)) {
  567. event.attributes[key] = parseFloat(event.attributes[key]);
  568. }
  569. });
  570. ['END-ON-NEXT'].forEach(function (key) {
  571. if (event.attributes.hasOwnProperty(key)) {
  572. event.attributes[key] = /YES/i.test(event.attributes[key]);
  573. }
  574. });
  575. ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) {
  576. if (event.attributes.hasOwnProperty(key)) {
  577. event.attributes[key] = event.attributes[key].toString(16);
  578. }
  579. });
  580. const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/;
  581. for (const key in event.attributes) {
  582. if (!clientAttributePattern.test(key)) {
  583. continue;
  584. }
  585. const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]);
  586. const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]);
  587. event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]);
  588. }
  589. this.trigger('data', event);
  590. return;
  591. }
  592. match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine);
  593. if (match) {
  594. this.trigger('data', {
  595. type: 'tag',
  596. tagType: 'independent-segments'
  597. });
  598. return;
  599. }
  600. match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine);
  601. if (match) {
  602. event = {
  603. type: 'tag',
  604. tagType: 'content-steering'
  605. };
  606. event.attributes = parseAttributes(match[1]);
  607. this.trigger('data', event);
  608. return;
  609. } // unknown tag type
  610. this.trigger('data', {
  611. type: 'tag',
  612. data: newLine.slice(4)
  613. });
  614. });
  615. }
  616. /**
  617. * Add a parser for custom headers
  618. *
  619. * @param {Object} options a map of options for the added parser
  620. * @param {RegExp} options.expression a regular expression to match the custom header
  621. * @param {string} options.customType the custom type to register to the output
  622. * @param {Function} [options.dataParser] function to parse the line into an object
  623. * @param {boolean} [options.segment] should tag data be attached to the segment object
  624. */
  625. addParser({
  626. expression,
  627. customType,
  628. dataParser,
  629. segment
  630. }) {
  631. if (typeof dataParser !== 'function') {
  632. dataParser = line => line;
  633. }
  634. this.customParsers.push(line => {
  635. const match = expression.exec(line);
  636. if (match) {
  637. this.trigger('data', {
  638. type: 'custom',
  639. data: dataParser(line),
  640. customType,
  641. segment
  642. });
  643. return true;
  644. }
  645. });
  646. }
  647. /**
  648. * Add a custom header mapper
  649. *
  650. * @param {Object} options
  651. * @param {RegExp} options.expression a regular expression to match the custom header
  652. * @param {Function} options.map function to translate tag into a different tag
  653. */
  654. addTagMapper({
  655. expression,
  656. map
  657. }) {
  658. const mapFn = line => {
  659. if (expression.test(line)) {
  660. return map(line);
  661. }
  662. return line;
  663. };
  664. this.tagMappers.push(mapFn);
  665. }
  666. }
  667. const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase());
  668. const camelCaseKeys = function (attributes) {
  669. const result = {};
  670. Object.keys(attributes).forEach(function (key) {
  671. result[camelCase(key)] = attributes[key];
  672. });
  673. return result;
  674. }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration
  675. // we need this helper because defaults are based upon targetDuration and
  676. // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before
  677. // target durations are set.
  678. const setHoldBack = function (manifest) {
  679. const {
  680. serverControl,
  681. targetDuration,
  682. partTargetDuration
  683. } = manifest;
  684. if (!serverControl) {
  685. return;
  686. }
  687. const tag = '#EXT-X-SERVER-CONTROL';
  688. const hb = 'holdBack';
  689. const phb = 'partHoldBack';
  690. const minTargetDuration = targetDuration && targetDuration * 3;
  691. const minPartDuration = partTargetDuration && partTargetDuration * 2;
  692. if (targetDuration && !serverControl.hasOwnProperty(hb)) {
  693. serverControl[hb] = minTargetDuration;
  694. this.trigger('info', {
  695. message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).`
  696. });
  697. }
  698. if (minTargetDuration && serverControl[hb] < minTargetDuration) {
  699. this.trigger('warn', {
  700. message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})`
  701. });
  702. serverControl[hb] = minTargetDuration;
  703. } // default no part hold back to part target duration * 3
  704. if (partTargetDuration && !serverControl.hasOwnProperty(phb)) {
  705. serverControl[phb] = partTargetDuration * 3;
  706. this.trigger('info', {
  707. message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).`
  708. });
  709. } // if part hold back is too small default it to part target duration * 2
  710. if (partTargetDuration && serverControl[phb] < minPartDuration) {
  711. this.trigger('warn', {
  712. message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).`
  713. });
  714. serverControl[phb] = minPartDuration;
  715. }
  716. };
  717. /**
  718. * A parser for M3U8 files. The current interpretation of the input is
  719. * exposed as a property `manifest` on parser objects. It's just two lines to
  720. * create and parse a manifest once you have the contents available as a string:
  721. *
  722. * ```js
  723. * var parser = new m3u8.Parser();
  724. * parser.push(xhr.responseText);
  725. * ```
  726. *
  727. * New input can later be applied to update the manifest object by calling
  728. * `push` again.
  729. *
  730. * The parser attempts to create a usable manifest object even if the
  731. * underlying input is somewhat nonsensical. It emits `info` and `warning`
  732. * events during the parse if it encounters input that seems invalid or
  733. * requires some property of the manifest object to be defaulted.
  734. *
  735. * @class Parser
  736. * @extends Stream
  737. */
  738. class Parser extends Stream__default["default"] {
  739. constructor() {
  740. super();
  741. this.lineStream = new LineStream();
  742. this.parseStream = new ParseStream();
  743. this.lineStream.pipe(this.parseStream);
  744. this.lastProgramDateTime = null;
  745. /* eslint-disable consistent-this */
  746. const self = this;
  747. /* eslint-enable consistent-this */
  748. const uris = [];
  749. let currentUri = {}; // if specified, the active EXT-X-MAP definition
  750. let currentMap; // if specified, the active decryption key
  751. let key;
  752. let hasParts = false;
  753. const noop = function () {};
  754. const defaultMediaGroups = {
  755. 'AUDIO': {},
  756. 'VIDEO': {},
  757. 'CLOSED-CAPTIONS': {},
  758. 'SUBTITLES': {}
  759. }; // This is the Widevine UUID from DASH IF IOP. The same exact string is
  760. // used in MPDs with Widevine encrypted streams.
  761. const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities
  762. let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data
  763. this.manifest = {
  764. allowCache: true,
  765. discontinuityStarts: [],
  766. dateRanges: [],
  767. segments: []
  768. }; // keep track of the last seen segment's byte range end, as segments are not required
  769. // to provide the offset, in which case it defaults to the next byte after the
  770. // previous segment
  771. let lastByterangeEnd = 0; // keep track of the last seen part's byte range end.
  772. let lastPartByterangeEnd = 0;
  773. const dateRangeTags = {};
  774. this.on('end', () => {
  775. // only add preloadSegment if we don't yet have a uri for it.
  776. // and we actually have parts/preloadHints
  777. if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) {
  778. return;
  779. }
  780. if (!currentUri.map && currentMap) {
  781. currentUri.map = currentMap;
  782. }
  783. if (!currentUri.key && key) {
  784. currentUri.key = key;
  785. }
  786. if (!currentUri.timeline && typeof currentTimeline === 'number') {
  787. currentUri.timeline = currentTimeline;
  788. }
  789. this.manifest.preloadSegment = currentUri;
  790. }); // update the manifest with the m3u8 entry from the parse stream
  791. this.parseStream.on('data', function (entry) {
  792. let mediaGroup;
  793. let rendition;
  794. ({
  795. tag() {
  796. // switch based on the tag type
  797. (({
  798. version() {
  799. if (entry.version) {
  800. this.manifest.version = entry.version;
  801. }
  802. },
  803. 'allow-cache'() {
  804. this.manifest.allowCache = entry.allowed;
  805. if (!('allowed' in entry)) {
  806. this.trigger('info', {
  807. message: 'defaulting allowCache to YES'
  808. });
  809. this.manifest.allowCache = true;
  810. }
  811. },
  812. byterange() {
  813. const byterange = {};
  814. if ('length' in entry) {
  815. currentUri.byterange = byterange;
  816. byterange.length = entry.length;
  817. if (!('offset' in entry)) {
  818. /*
  819. * From the latest spec (as of this writing):
  820. * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2
  821. *
  822. * Same text since EXT-X-BYTERANGE's introduction in draft 7:
  823. * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1)
  824. *
  825. * "If o [offset] is not present, the sub-range begins at the next byte
  826. * following the sub-range of the previous media segment."
  827. */
  828. entry.offset = lastByterangeEnd;
  829. }
  830. }
  831. if ('offset' in entry) {
  832. currentUri.byterange = byterange;
  833. byterange.offset = entry.offset;
  834. }
  835. lastByterangeEnd = byterange.offset + byterange.length;
  836. },
  837. endlist() {
  838. this.manifest.endList = true;
  839. },
  840. inf() {
  841. if (!('mediaSequence' in this.manifest)) {
  842. this.manifest.mediaSequence = 0;
  843. this.trigger('info', {
  844. message: 'defaulting media sequence to zero'
  845. });
  846. }
  847. if (!('discontinuitySequence' in this.manifest)) {
  848. this.manifest.discontinuitySequence = 0;
  849. this.trigger('info', {
  850. message: 'defaulting discontinuity sequence to zero'
  851. });
  852. }
  853. if (entry.title) {
  854. currentUri.title = entry.title;
  855. }
  856. if (entry.duration > 0) {
  857. currentUri.duration = entry.duration;
  858. }
  859. if (entry.duration === 0) {
  860. currentUri.duration = 0.01;
  861. this.trigger('info', {
  862. message: 'updating zero segment duration to a small value'
  863. });
  864. }
  865. this.manifest.segments = uris;
  866. },
  867. key() {
  868. if (!entry.attributes) {
  869. this.trigger('warn', {
  870. message: 'ignoring key declaration without attribute list'
  871. });
  872. return;
  873. } // clear the active encryption key
  874. if (entry.attributes.METHOD === 'NONE') {
  875. key = null;
  876. return;
  877. }
  878. if (!entry.attributes.URI) {
  879. this.trigger('warn', {
  880. message: 'ignoring key declaration without URI'
  881. });
  882. return;
  883. }
  884. if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') {
  885. this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
  886. this.manifest.contentProtection['com.apple.fps.1_0'] = {
  887. attributes: entry.attributes
  888. };
  889. return;
  890. }
  891. if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
  892. this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
  893. this.manifest.contentProtection['com.microsoft.playready'] = {
  894. uri: entry.attributes.URI
  895. };
  896. return;
  897. } // check if the content is encrypted for Widevine
  898. // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
  899. if (entry.attributes.KEYFORMAT === widevineUuid) {
  900. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
  901. if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
  902. this.trigger('warn', {
  903. message: 'invalid key method provided for Widevine'
  904. });
  905. return;
  906. }
  907. if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
  908. this.trigger('warn', {
  909. message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
  910. });
  911. }
  912. if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
  913. this.trigger('warn', {
  914. message: 'invalid key URI provided for Widevine'
  915. });
  916. return;
  917. }
  918. if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
  919. this.trigger('warn', {
  920. message: 'invalid key ID provided for Widevine'
  921. });
  922. return;
  923. } // if Widevine key attributes are valid, store them as `contentProtection`
  924. // on the manifest to emulate Widevine tag structure in a DASH mpd
  925. this.manifest.contentProtection = this.manifest.contentProtection || {};
  926. this.manifest.contentProtection['com.widevine.alpha'] = {
  927. attributes: {
  928. schemeIdUri: entry.attributes.KEYFORMAT,
  929. // remove '0x' from the key id string
  930. keyId: entry.attributes.KEYID.substring(2)
  931. },
  932. // decode the base64-encoded PSSH box
  933. pssh: decodeB64ToUint8Array__default["default"](entry.attributes.URI.split(',')[1])
  934. };
  935. return;
  936. }
  937. if (!entry.attributes.METHOD) {
  938. this.trigger('warn', {
  939. message: 'defaulting key method to AES-128'
  940. });
  941. } // setup an encryption key for upcoming segments
  942. key = {
  943. method: entry.attributes.METHOD || 'AES-128',
  944. uri: entry.attributes.URI
  945. };
  946. if (typeof entry.attributes.IV !== 'undefined') {
  947. key.iv = entry.attributes.IV;
  948. }
  949. },
  950. 'media-sequence'() {
  951. if (!isFinite(entry.number)) {
  952. this.trigger('warn', {
  953. message: 'ignoring invalid media sequence: ' + entry.number
  954. });
  955. return;
  956. }
  957. this.manifest.mediaSequence = entry.number;
  958. },
  959. 'discontinuity-sequence'() {
  960. if (!isFinite(entry.number)) {
  961. this.trigger('warn', {
  962. message: 'ignoring invalid discontinuity sequence: ' + entry.number
  963. });
  964. return;
  965. }
  966. this.manifest.discontinuitySequence = entry.number;
  967. currentTimeline = entry.number;
  968. },
  969. 'playlist-type'() {
  970. if (!/VOD|EVENT/.test(entry.playlistType)) {
  971. this.trigger('warn', {
  972. message: 'ignoring unknown playlist type: ' + entry.playlist
  973. });
  974. return;
  975. }
  976. this.manifest.playlistType = entry.playlistType;
  977. },
  978. map() {
  979. currentMap = {};
  980. if (entry.uri) {
  981. currentMap.uri = entry.uri;
  982. }
  983. if (entry.byterange) {
  984. currentMap.byterange = entry.byterange;
  985. }
  986. if (key) {
  987. currentMap.key = key;
  988. }
  989. },
  990. 'stream-inf'() {
  991. this.manifest.playlists = uris;
  992. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  993. if (!entry.attributes) {
  994. this.trigger('warn', {
  995. message: 'ignoring empty stream-inf attributes'
  996. });
  997. return;
  998. }
  999. if (!currentUri.attributes) {
  1000. currentUri.attributes = {};
  1001. }
  1002. _extends__default["default"](currentUri.attributes, entry.attributes);
  1003. },
  1004. media() {
  1005. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  1006. if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
  1007. this.trigger('warn', {
  1008. message: 'ignoring incomplete or missing media group'
  1009. });
  1010. return;
  1011. } // find the media group, creating defaults as necessary
  1012. const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
  1013. mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
  1014. mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata
  1015. rendition = {
  1016. default: /yes/i.test(entry.attributes.DEFAULT)
  1017. };
  1018. if (rendition.default) {
  1019. rendition.autoselect = true;
  1020. } else {
  1021. rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
  1022. }
  1023. if (entry.attributes.LANGUAGE) {
  1024. rendition.language = entry.attributes.LANGUAGE;
  1025. }
  1026. if (entry.attributes.URI) {
  1027. rendition.uri = entry.attributes.URI;
  1028. }
  1029. if (entry.attributes['INSTREAM-ID']) {
  1030. rendition.instreamId = entry.attributes['INSTREAM-ID'];
  1031. }
  1032. if (entry.attributes.CHARACTERISTICS) {
  1033. rendition.characteristics = entry.attributes.CHARACTERISTICS;
  1034. }
  1035. if (entry.attributes.FORCED) {
  1036. rendition.forced = /yes/i.test(entry.attributes.FORCED);
  1037. } // insert the new rendition
  1038. mediaGroup[entry.attributes.NAME] = rendition;
  1039. },
  1040. discontinuity() {
  1041. currentTimeline += 1;
  1042. currentUri.discontinuity = true;
  1043. this.manifest.discontinuityStarts.push(uris.length);
  1044. },
  1045. 'program-date-time'() {
  1046. if (typeof this.manifest.dateTimeString === 'undefined') {
  1047. // PROGRAM-DATE-TIME is a media-segment tag, but for backwards
  1048. // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
  1049. // to the manifest object
  1050. // TODO: Consider removing this in future major version
  1051. this.manifest.dateTimeString = entry.dateTimeString;
  1052. this.manifest.dateTimeObject = entry.dateTimeObject;
  1053. }
  1054. currentUri.dateTimeString = entry.dateTimeString;
  1055. currentUri.dateTimeObject = entry.dateTimeObject;
  1056. const {
  1057. lastProgramDateTime
  1058. } = this;
  1059. this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence.
  1060. // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
  1061. if (lastProgramDateTime === null) {
  1062. // Extrapolate Program Date Time backward
  1063. // Since it is first program date time occurrence we're assuming that
  1064. // all this.manifest.segments have no program date time info
  1065. this.manifest.segments.reduceRight((programDateTime, segment) => {
  1066. segment.programDateTime = programDateTime - segment.duration * 1000;
  1067. return segment.programDateTime;
  1068. }, this.lastProgramDateTime);
  1069. }
  1070. },
  1071. targetduration() {
  1072. if (!isFinite(entry.duration) || entry.duration < 0) {
  1073. this.trigger('warn', {
  1074. message: 'ignoring invalid target duration: ' + entry.duration
  1075. });
  1076. return;
  1077. }
  1078. this.manifest.targetDuration = entry.duration;
  1079. setHoldBack.call(this, this.manifest);
  1080. },
  1081. start() {
  1082. if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
  1083. this.trigger('warn', {
  1084. message: 'ignoring start declaration without appropriate attribute list'
  1085. });
  1086. return;
  1087. }
  1088. this.manifest.start = {
  1089. timeOffset: entry.attributes['TIME-OFFSET'],
  1090. precise: entry.attributes.PRECISE
  1091. };
  1092. },
  1093. 'cue-out'() {
  1094. currentUri.cueOut = entry.data;
  1095. },
  1096. 'cue-out-cont'() {
  1097. currentUri.cueOutCont = entry.data;
  1098. },
  1099. 'cue-in'() {
  1100. currentUri.cueIn = entry.data;
  1101. },
  1102. 'skip'() {
  1103. this.manifest.skip = camelCaseKeys(entry.attributes);
  1104. this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']);
  1105. },
  1106. 'part'() {
  1107. hasParts = true; // parts are always specifed before a segment
  1108. const segmentIndex = this.manifest.segments.length;
  1109. const part = camelCaseKeys(entry.attributes);
  1110. currentUri.parts = currentUri.parts || [];
  1111. currentUri.parts.push(part);
  1112. if (part.byterange) {
  1113. if (!part.byterange.hasOwnProperty('offset')) {
  1114. part.byterange.offset = lastPartByterangeEnd;
  1115. }
  1116. lastPartByterangeEnd = part.byterange.offset + part.byterange.length;
  1117. }
  1118. const partIndex = currentUri.parts.length - 1;
  1119. this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']);
  1120. if (this.manifest.renditionReports) {
  1121. this.manifest.renditionReports.forEach((r, i) => {
  1122. if (!r.hasOwnProperty('lastPart')) {
  1123. this.trigger('warn', {
  1124. message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART`
  1125. });
  1126. }
  1127. });
  1128. }
  1129. },
  1130. 'server-control'() {
  1131. const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes);
  1132. if (!attrs.hasOwnProperty('canBlockReload')) {
  1133. attrs.canBlockReload = false;
  1134. this.trigger('info', {
  1135. message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false'
  1136. });
  1137. }
  1138. setHoldBack.call(this, this.manifest);
  1139. if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) {
  1140. this.trigger('warn', {
  1141. message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
  1142. });
  1143. }
  1144. },
  1145. 'preload-hint'() {
  1146. // parts are always specifed before a segment
  1147. const segmentIndex = this.manifest.segments.length;
  1148. const hint = camelCaseKeys(entry.attributes);
  1149. const isPart = hint.type && hint.type === 'PART';
  1150. currentUri.preloadHints = currentUri.preloadHints || [];
  1151. currentUri.preloadHints.push(hint);
  1152. if (hint.byterange) {
  1153. if (!hint.byterange.hasOwnProperty('offset')) {
  1154. // use last part byterange end or zero if not a part.
  1155. hint.byterange.offset = isPart ? lastPartByterangeEnd : 0;
  1156. if (isPart) {
  1157. lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length;
  1158. }
  1159. }
  1160. }
  1161. const index = currentUri.preloadHints.length - 1;
  1162. this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']);
  1163. if (!hint.type) {
  1164. return;
  1165. } // search through all preload hints except for the current one for
  1166. // a duplicate type.
  1167. for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
  1168. const otherHint = currentUri.preloadHints[i];
  1169. if (!otherHint.type) {
  1170. continue;
  1171. }
  1172. if (otherHint.type === hint.type) {
  1173. this.trigger('warn', {
  1174. message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}`
  1175. });
  1176. }
  1177. }
  1178. },
  1179. 'rendition-report'() {
  1180. const report = camelCaseKeys(entry.attributes);
  1181. this.manifest.renditionReports = this.manifest.renditionReports || [];
  1182. this.manifest.renditionReports.push(report);
  1183. const index = this.manifest.renditionReports.length - 1;
  1184. const required = ['LAST-MSN', 'URI'];
  1185. if (hasParts) {
  1186. required.push('LAST-PART');
  1187. }
  1188. this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required);
  1189. },
  1190. 'part-inf'() {
  1191. this.manifest.partInf = camelCaseKeys(entry.attributes);
  1192. this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']);
  1193. if (this.manifest.partInf.partTarget) {
  1194. this.manifest.partTargetDuration = this.manifest.partInf.partTarget;
  1195. }
  1196. setHoldBack.call(this, this.manifest);
  1197. },
  1198. 'daterange'() {
  1199. this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
  1200. const index = this.manifest.dateRanges.length - 1;
  1201. this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']);
  1202. const dateRange = this.manifest.dateRanges[index];
  1203. if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) {
  1204. this.trigger('warn', {
  1205. message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
  1206. });
  1207. }
  1208. if (dateRange.duration && dateRange.duration < 0) {
  1209. this.trigger('warn', {
  1210. message: 'EXT-X-DATERANGE DURATION must not be negative'
  1211. });
  1212. }
  1213. if (dateRange.plannedDuration && dateRange.plannedDuration < 0) {
  1214. this.trigger('warn', {
  1215. message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
  1216. });
  1217. }
  1218. const endOnNextYes = !!dateRange.endOnNext;
  1219. if (endOnNextYes && !dateRange.class) {
  1220. this.trigger('warn', {
  1221. message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
  1222. });
  1223. }
  1224. if (endOnNextYes && (dateRange.duration || dateRange.endDate)) {
  1225. this.trigger('warn', {
  1226. message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
  1227. });
  1228. }
  1229. if (dateRange.duration && dateRange.endDate) {
  1230. const startDate = dateRange.startDate;
  1231. const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000;
  1232. this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
  1233. }
  1234. if (!dateRangeTags[dateRange.id]) {
  1235. dateRangeTags[dateRange.id] = dateRange;
  1236. } else {
  1237. for (const attribute in dateRangeTags[dateRange.id]) {
  1238. if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) {
  1239. this.trigger('warn', {
  1240. message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
  1241. });
  1242. break;
  1243. }
  1244. } // if tags with the same ID do not have conflicting attributes, merge them
  1245. const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id);
  1246. this.manifest.dateRanges[dateRangeWithSameId] = _extends__default["default"](this.manifest.dateRanges[dateRangeWithSameId], dateRange);
  1247. dateRangeTags[dateRange.id] = _extends__default["default"](dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last
  1248. this.manifest.dateRanges.pop();
  1249. }
  1250. },
  1251. 'independent-segments'() {
  1252. this.manifest.independentSegments = true;
  1253. },
  1254. 'content-steering'() {
  1255. this.manifest.contentSteering = camelCaseKeys(entry.attributes);
  1256. this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']);
  1257. }
  1258. })[entry.tagType] || noop).call(self);
  1259. },
  1260. uri() {
  1261. currentUri.uri = entry.uri;
  1262. uris.push(currentUri); // if no explicit duration was declared, use the target duration
  1263. if (this.manifest.targetDuration && !('duration' in currentUri)) {
  1264. this.trigger('warn', {
  1265. message: 'defaulting segment duration to the target duration'
  1266. });
  1267. currentUri.duration = this.manifest.targetDuration;
  1268. } // annotate with encryption information, if necessary
  1269. if (key) {
  1270. currentUri.key = key;
  1271. }
  1272. currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary
  1273. if (currentMap) {
  1274. currentUri.map = currentMap;
  1275. } // reset the last byterange end as it needs to be 0 between parts
  1276. lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward
  1277. if (this.lastProgramDateTime !== null) {
  1278. currentUri.programDateTime = this.lastProgramDateTime;
  1279. this.lastProgramDateTime += currentUri.duration * 1000;
  1280. } // prepare for the next URI
  1281. currentUri = {};
  1282. },
  1283. comment() {// comments are not important for playback
  1284. },
  1285. custom() {
  1286. // if this is segment-level data attach the output to the segment
  1287. if (entry.segment) {
  1288. currentUri.custom = currentUri.custom || {};
  1289. currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object
  1290. } else {
  1291. this.manifest.custom = this.manifest.custom || {};
  1292. this.manifest.custom[entry.customType] = entry.data;
  1293. }
  1294. }
  1295. })[entry.type].call(self);
  1296. });
  1297. }
  1298. warnOnMissingAttributes_(identifier, attributes, required) {
  1299. const missing = [];
  1300. required.forEach(function (key) {
  1301. if (!attributes.hasOwnProperty(key)) {
  1302. missing.push(key);
  1303. }
  1304. });
  1305. if (missing.length) {
  1306. this.trigger('warn', {
  1307. message: `${identifier} lacks required attribute(s): ${missing.join(', ')}`
  1308. });
  1309. }
  1310. }
  1311. /**
  1312. * Parse the input string and update the manifest object.
  1313. *
  1314. * @param {string} chunk a potentially incomplete portion of the manifest
  1315. */
  1316. push(chunk) {
  1317. this.lineStream.push(chunk);
  1318. }
  1319. /**
  1320. * Flush any remaining input. This can be handy if the last line of an M3U8
  1321. * manifest did not contain a trailing newline but the file has been
  1322. * completely received.
  1323. */
  1324. end() {
  1325. // flush any buffered input
  1326. this.lineStream.push('\n');
  1327. if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
  1328. this.trigger('warn', {
  1329. message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
  1330. });
  1331. }
  1332. this.lastProgramDateTime = null;
  1333. this.trigger('end');
  1334. }
  1335. /**
  1336. * Add an additional parser for non-standard tags
  1337. *
  1338. * @param {Object} options a map of options for the added parser
  1339. * @param {RegExp} options.expression a regular expression to match the custom header
  1340. * @param {string} options.customType the custom type to register to the output
  1341. * @param {Function} [options.dataParser] function to parse the line into an object
  1342. * @param {boolean} [options.segment] should tag data be attached to the segment object
  1343. */
  1344. addParser(options) {
  1345. this.parseStream.addParser(options);
  1346. }
  1347. /**
  1348. * Add a custom header mapper
  1349. *
  1350. * @param {Object} options
  1351. * @param {RegExp} options.expression a regular expression to match the custom header
  1352. * @param {Function} options.map function to translate tag into a different tag
  1353. */
  1354. addTagMapper(options) {
  1355. this.parseStream.addTagMapper(options);
  1356. }
  1357. }
  1358. exports.LineStream = LineStream;
  1359. exports.ParseStream = ParseStream;
  1360. exports.Parser = Parser;