m3u8-parser.cjs.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515
  1. /*! @name m3u8-parser @version 6.0.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. } // unknown tag type
  548. this.trigger('data', {
  549. type: 'tag',
  550. data: newLine.slice(4)
  551. });
  552. });
  553. }
  554. /**
  555. * Add a parser for custom headers
  556. *
  557. * @param {Object} options a map of options for the added parser
  558. * @param {RegExp} options.expression a regular expression to match the custom header
  559. * @param {string} options.customType the custom type to register to the output
  560. * @param {Function} [options.dataParser] function to parse the line into an object
  561. * @param {boolean} [options.segment] should tag data be attached to the segment object
  562. */
  563. addParser({
  564. expression,
  565. customType,
  566. dataParser,
  567. segment
  568. }) {
  569. if (typeof dataParser !== 'function') {
  570. dataParser = line => line;
  571. }
  572. this.customParsers.push(line => {
  573. const match = expression.exec(line);
  574. if (match) {
  575. this.trigger('data', {
  576. type: 'custom',
  577. data: dataParser(line),
  578. customType,
  579. segment
  580. });
  581. return true;
  582. }
  583. });
  584. }
  585. /**
  586. * Add a custom header mapper
  587. *
  588. * @param {Object} options
  589. * @param {RegExp} options.expression a regular expression to match the custom header
  590. * @param {Function} options.map function to translate tag into a different tag
  591. */
  592. addTagMapper({
  593. expression,
  594. map
  595. }) {
  596. const mapFn = line => {
  597. if (expression.test(line)) {
  598. return map(line);
  599. }
  600. return line;
  601. };
  602. this.tagMappers.push(mapFn);
  603. }
  604. }
  605. const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase());
  606. const camelCaseKeys = function (attributes) {
  607. const result = {};
  608. Object.keys(attributes).forEach(function (key) {
  609. result[camelCase(key)] = attributes[key];
  610. });
  611. return result;
  612. }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration
  613. // we need this helper because defaults are based upon targetDuration and
  614. // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before
  615. // target durations are set.
  616. const setHoldBack = function (manifest) {
  617. const {
  618. serverControl,
  619. targetDuration,
  620. partTargetDuration
  621. } = manifest;
  622. if (!serverControl) {
  623. return;
  624. }
  625. const tag = '#EXT-X-SERVER-CONTROL';
  626. const hb = 'holdBack';
  627. const phb = 'partHoldBack';
  628. const minTargetDuration = targetDuration && targetDuration * 3;
  629. const minPartDuration = partTargetDuration && partTargetDuration * 2;
  630. if (targetDuration && !serverControl.hasOwnProperty(hb)) {
  631. serverControl[hb] = minTargetDuration;
  632. this.trigger('info', {
  633. message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).`
  634. });
  635. }
  636. if (minTargetDuration && serverControl[hb] < minTargetDuration) {
  637. this.trigger('warn', {
  638. message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})`
  639. });
  640. serverControl[hb] = minTargetDuration;
  641. } // default no part hold back to part target duration * 3
  642. if (partTargetDuration && !serverControl.hasOwnProperty(phb)) {
  643. serverControl[phb] = partTargetDuration * 3;
  644. this.trigger('info', {
  645. message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).`
  646. });
  647. } // if part hold back is too small default it to part target duration * 2
  648. if (partTargetDuration && serverControl[phb] < minPartDuration) {
  649. this.trigger('warn', {
  650. message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).`
  651. });
  652. serverControl[phb] = minPartDuration;
  653. }
  654. };
  655. /**
  656. * A parser for M3U8 files. The current interpretation of the input is
  657. * exposed as a property `manifest` on parser objects. It's just two lines to
  658. * create and parse a manifest once you have the contents available as a string:
  659. *
  660. * ```js
  661. * var parser = new m3u8.Parser();
  662. * parser.push(xhr.responseText);
  663. * ```
  664. *
  665. * New input can later be applied to update the manifest object by calling
  666. * `push` again.
  667. *
  668. * The parser attempts to create a usable manifest object even if the
  669. * underlying input is somewhat nonsensical. It emits `info` and `warning`
  670. * events during the parse if it encounters input that seems invalid or
  671. * requires some property of the manifest object to be defaulted.
  672. *
  673. * @class Parser
  674. * @extends Stream
  675. */
  676. class Parser extends Stream__default["default"] {
  677. constructor() {
  678. super();
  679. this.lineStream = new LineStream();
  680. this.parseStream = new ParseStream();
  681. this.lineStream.pipe(this.parseStream);
  682. /* eslint-disable consistent-this */
  683. const self = this;
  684. /* eslint-enable consistent-this */
  685. const uris = [];
  686. let currentUri = {}; // if specified, the active EXT-X-MAP definition
  687. let currentMap; // if specified, the active decryption key
  688. let key;
  689. let hasParts = false;
  690. const noop = function () {};
  691. const defaultMediaGroups = {
  692. 'AUDIO': {},
  693. 'VIDEO': {},
  694. 'CLOSED-CAPTIONS': {},
  695. 'SUBTITLES': {}
  696. }; // This is the Widevine UUID from DASH IF IOP. The same exact string is
  697. // used in MPDs with Widevine encrypted streams.
  698. const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities
  699. let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data
  700. this.manifest = {
  701. allowCache: true,
  702. discontinuityStarts: [],
  703. segments: []
  704. }; // keep track of the last seen segment's byte range end, as segments are not required
  705. // to provide the offset, in which case it defaults to the next byte after the
  706. // previous segment
  707. let lastByterangeEnd = 0; // keep track of the last seen part's byte range end.
  708. let lastPartByterangeEnd = 0;
  709. this.on('end', () => {
  710. // only add preloadSegment if we don't yet have a uri for it.
  711. // and we actually have parts/preloadHints
  712. if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) {
  713. return;
  714. }
  715. if (!currentUri.map && currentMap) {
  716. currentUri.map = currentMap;
  717. }
  718. if (!currentUri.key && key) {
  719. currentUri.key = key;
  720. }
  721. if (!currentUri.timeline && typeof currentTimeline === 'number') {
  722. currentUri.timeline = currentTimeline;
  723. }
  724. this.manifest.preloadSegment = currentUri;
  725. }); // update the manifest with the m3u8 entry from the parse stream
  726. this.parseStream.on('data', function (entry) {
  727. let mediaGroup;
  728. let rendition;
  729. ({
  730. tag() {
  731. // switch based on the tag type
  732. (({
  733. version() {
  734. if (entry.version) {
  735. this.manifest.version = entry.version;
  736. }
  737. },
  738. 'allow-cache'() {
  739. this.manifest.allowCache = entry.allowed;
  740. if (!('allowed' in entry)) {
  741. this.trigger('info', {
  742. message: 'defaulting allowCache to YES'
  743. });
  744. this.manifest.allowCache = true;
  745. }
  746. },
  747. byterange() {
  748. const byterange = {};
  749. if ('length' in entry) {
  750. currentUri.byterange = byterange;
  751. byterange.length = entry.length;
  752. if (!('offset' in entry)) {
  753. /*
  754. * From the latest spec (as of this writing):
  755. * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2
  756. *
  757. * Same text since EXT-X-BYTERANGE's introduction in draft 7:
  758. * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1)
  759. *
  760. * "If o [offset] is not present, the sub-range begins at the next byte
  761. * following the sub-range of the previous media segment."
  762. */
  763. entry.offset = lastByterangeEnd;
  764. }
  765. }
  766. if ('offset' in entry) {
  767. currentUri.byterange = byterange;
  768. byterange.offset = entry.offset;
  769. }
  770. lastByterangeEnd = byterange.offset + byterange.length;
  771. },
  772. endlist() {
  773. this.manifest.endList = true;
  774. },
  775. inf() {
  776. if (!('mediaSequence' in this.manifest)) {
  777. this.manifest.mediaSequence = 0;
  778. this.trigger('info', {
  779. message: 'defaulting media sequence to zero'
  780. });
  781. }
  782. if (!('discontinuitySequence' in this.manifest)) {
  783. this.manifest.discontinuitySequence = 0;
  784. this.trigger('info', {
  785. message: 'defaulting discontinuity sequence to zero'
  786. });
  787. }
  788. if (entry.duration > 0) {
  789. currentUri.duration = entry.duration;
  790. }
  791. if (entry.duration === 0) {
  792. currentUri.duration = 0.01;
  793. this.trigger('info', {
  794. message: 'updating zero segment duration to a small value'
  795. });
  796. }
  797. this.manifest.segments = uris;
  798. },
  799. key() {
  800. if (!entry.attributes) {
  801. this.trigger('warn', {
  802. message: 'ignoring key declaration without attribute list'
  803. });
  804. return;
  805. } // clear the active encryption key
  806. if (entry.attributes.METHOD === 'NONE') {
  807. key = null;
  808. return;
  809. }
  810. if (!entry.attributes.URI) {
  811. this.trigger('warn', {
  812. message: 'ignoring key declaration without URI'
  813. });
  814. return;
  815. }
  816. if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') {
  817. this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
  818. this.manifest.contentProtection['com.apple.fps.1_0'] = {
  819. attributes: entry.attributes
  820. };
  821. return;
  822. }
  823. if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
  824. this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
  825. this.manifest.contentProtection['com.microsoft.playready'] = {
  826. uri: entry.attributes.URI
  827. };
  828. return;
  829. } // check if the content is encrypted for Widevine
  830. // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
  831. if (entry.attributes.KEYFORMAT === widevineUuid) {
  832. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
  833. if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
  834. this.trigger('warn', {
  835. message: 'invalid key method provided for Widevine'
  836. });
  837. return;
  838. }
  839. if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
  840. this.trigger('warn', {
  841. message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
  842. });
  843. }
  844. if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
  845. this.trigger('warn', {
  846. message: 'invalid key URI provided for Widevine'
  847. });
  848. return;
  849. }
  850. if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
  851. this.trigger('warn', {
  852. message: 'invalid key ID provided for Widevine'
  853. });
  854. return;
  855. } // if Widevine key attributes are valid, store them as `contentProtection`
  856. // on the manifest to emulate Widevine tag structure in a DASH mpd
  857. this.manifest.contentProtection = this.manifest.contentProtection || {};
  858. this.manifest.contentProtection['com.widevine.alpha'] = {
  859. attributes: {
  860. schemeIdUri: entry.attributes.KEYFORMAT,
  861. // remove '0x' from the key id string
  862. keyId: entry.attributes.KEYID.substring(2)
  863. },
  864. // decode the base64-encoded PSSH box
  865. pssh: decodeB64ToUint8Array__default["default"](entry.attributes.URI.split(',')[1])
  866. };
  867. return;
  868. }
  869. if (!entry.attributes.METHOD) {
  870. this.trigger('warn', {
  871. message: 'defaulting key method to AES-128'
  872. });
  873. } // setup an encryption key for upcoming segments
  874. key = {
  875. method: entry.attributes.METHOD || 'AES-128',
  876. uri: entry.attributes.URI
  877. };
  878. if (typeof entry.attributes.IV !== 'undefined') {
  879. key.iv = entry.attributes.IV;
  880. }
  881. },
  882. 'media-sequence'() {
  883. if (!isFinite(entry.number)) {
  884. this.trigger('warn', {
  885. message: 'ignoring invalid media sequence: ' + entry.number
  886. });
  887. return;
  888. }
  889. this.manifest.mediaSequence = entry.number;
  890. },
  891. 'discontinuity-sequence'() {
  892. if (!isFinite(entry.number)) {
  893. this.trigger('warn', {
  894. message: 'ignoring invalid discontinuity sequence: ' + entry.number
  895. });
  896. return;
  897. }
  898. this.manifest.discontinuitySequence = entry.number;
  899. currentTimeline = entry.number;
  900. },
  901. 'playlist-type'() {
  902. if (!/VOD|EVENT/.test(entry.playlistType)) {
  903. this.trigger('warn', {
  904. message: 'ignoring unknown playlist type: ' + entry.playlist
  905. });
  906. return;
  907. }
  908. this.manifest.playlistType = entry.playlistType;
  909. },
  910. map() {
  911. currentMap = {};
  912. if (entry.uri) {
  913. currentMap.uri = entry.uri;
  914. }
  915. if (entry.byterange) {
  916. currentMap.byterange = entry.byterange;
  917. }
  918. if (key) {
  919. currentMap.key = key;
  920. }
  921. },
  922. 'stream-inf'() {
  923. this.manifest.playlists = uris;
  924. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  925. if (!entry.attributes) {
  926. this.trigger('warn', {
  927. message: 'ignoring empty stream-inf attributes'
  928. });
  929. return;
  930. }
  931. if (!currentUri.attributes) {
  932. currentUri.attributes = {};
  933. }
  934. _extends__default["default"](currentUri.attributes, entry.attributes);
  935. },
  936. media() {
  937. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  938. if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
  939. this.trigger('warn', {
  940. message: 'ignoring incomplete or missing media group'
  941. });
  942. return;
  943. } // find the media group, creating defaults as necessary
  944. const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
  945. mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
  946. mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata
  947. rendition = {
  948. default: /yes/i.test(entry.attributes.DEFAULT)
  949. };
  950. if (rendition.default) {
  951. rendition.autoselect = true;
  952. } else {
  953. rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
  954. }
  955. if (entry.attributes.LANGUAGE) {
  956. rendition.language = entry.attributes.LANGUAGE;
  957. }
  958. if (entry.attributes.URI) {
  959. rendition.uri = entry.attributes.URI;
  960. }
  961. if (entry.attributes['INSTREAM-ID']) {
  962. rendition.instreamId = entry.attributes['INSTREAM-ID'];
  963. }
  964. if (entry.attributes.CHARACTERISTICS) {
  965. rendition.characteristics = entry.attributes.CHARACTERISTICS;
  966. }
  967. if (entry.attributes.FORCED) {
  968. rendition.forced = /yes/i.test(entry.attributes.FORCED);
  969. } // insert the new rendition
  970. mediaGroup[entry.attributes.NAME] = rendition;
  971. },
  972. discontinuity() {
  973. currentTimeline += 1;
  974. currentUri.discontinuity = true;
  975. this.manifest.discontinuityStarts.push(uris.length);
  976. },
  977. 'program-date-time'() {
  978. if (typeof this.manifest.dateTimeString === 'undefined') {
  979. // PROGRAM-DATE-TIME is a media-segment tag, but for backwards
  980. // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
  981. // to the manifest object
  982. // TODO: Consider removing this in future major version
  983. this.manifest.dateTimeString = entry.dateTimeString;
  984. this.manifest.dateTimeObject = entry.dateTimeObject;
  985. }
  986. currentUri.dateTimeString = entry.dateTimeString;
  987. currentUri.dateTimeObject = entry.dateTimeObject;
  988. },
  989. targetduration() {
  990. if (!isFinite(entry.duration) || entry.duration < 0) {
  991. this.trigger('warn', {
  992. message: 'ignoring invalid target duration: ' + entry.duration
  993. });
  994. return;
  995. }
  996. this.manifest.targetDuration = entry.duration;
  997. setHoldBack.call(this, this.manifest);
  998. },
  999. start() {
  1000. if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
  1001. this.trigger('warn', {
  1002. message: 'ignoring start declaration without appropriate attribute list'
  1003. });
  1004. return;
  1005. }
  1006. this.manifest.start = {
  1007. timeOffset: entry.attributes['TIME-OFFSET'],
  1008. precise: entry.attributes.PRECISE
  1009. };
  1010. },
  1011. 'cue-out'() {
  1012. currentUri.cueOut = entry.data;
  1013. },
  1014. 'cue-out-cont'() {
  1015. currentUri.cueOutCont = entry.data;
  1016. },
  1017. 'cue-in'() {
  1018. currentUri.cueIn = entry.data;
  1019. },
  1020. 'skip'() {
  1021. this.manifest.skip = camelCaseKeys(entry.attributes);
  1022. this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']);
  1023. },
  1024. 'part'() {
  1025. hasParts = true; // parts are always specifed before a segment
  1026. const segmentIndex = this.manifest.segments.length;
  1027. const part = camelCaseKeys(entry.attributes);
  1028. currentUri.parts = currentUri.parts || [];
  1029. currentUri.parts.push(part);
  1030. if (part.byterange) {
  1031. if (!part.byterange.hasOwnProperty('offset')) {
  1032. part.byterange.offset = lastPartByterangeEnd;
  1033. }
  1034. lastPartByterangeEnd = part.byterange.offset + part.byterange.length;
  1035. }
  1036. const partIndex = currentUri.parts.length - 1;
  1037. this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']);
  1038. if (this.manifest.renditionReports) {
  1039. this.manifest.renditionReports.forEach((r, i) => {
  1040. if (!r.hasOwnProperty('lastPart')) {
  1041. this.trigger('warn', {
  1042. message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART`
  1043. });
  1044. }
  1045. });
  1046. }
  1047. },
  1048. 'server-control'() {
  1049. const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes);
  1050. if (!attrs.hasOwnProperty('canBlockReload')) {
  1051. attrs.canBlockReload = false;
  1052. this.trigger('info', {
  1053. message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false'
  1054. });
  1055. }
  1056. setHoldBack.call(this, this.manifest);
  1057. if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) {
  1058. this.trigger('warn', {
  1059. message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
  1060. });
  1061. }
  1062. },
  1063. 'preload-hint'() {
  1064. // parts are always specifed before a segment
  1065. const segmentIndex = this.manifest.segments.length;
  1066. const hint = camelCaseKeys(entry.attributes);
  1067. const isPart = hint.type && hint.type === 'PART';
  1068. currentUri.preloadHints = currentUri.preloadHints || [];
  1069. currentUri.preloadHints.push(hint);
  1070. if (hint.byterange) {
  1071. if (!hint.byterange.hasOwnProperty('offset')) {
  1072. // use last part byterange end or zero if not a part.
  1073. hint.byterange.offset = isPart ? lastPartByterangeEnd : 0;
  1074. if (isPart) {
  1075. lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length;
  1076. }
  1077. }
  1078. }
  1079. const index = currentUri.preloadHints.length - 1;
  1080. this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']);
  1081. if (!hint.type) {
  1082. return;
  1083. } // search through all preload hints except for the current one for
  1084. // a duplicate type.
  1085. for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
  1086. const otherHint = currentUri.preloadHints[i];
  1087. if (!otherHint.type) {
  1088. continue;
  1089. }
  1090. if (otherHint.type === hint.type) {
  1091. this.trigger('warn', {
  1092. message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}`
  1093. });
  1094. }
  1095. }
  1096. },
  1097. 'rendition-report'() {
  1098. const report = camelCaseKeys(entry.attributes);
  1099. this.manifest.renditionReports = this.manifest.renditionReports || [];
  1100. this.manifest.renditionReports.push(report);
  1101. const index = this.manifest.renditionReports.length - 1;
  1102. const required = ['LAST-MSN', 'URI'];
  1103. if (hasParts) {
  1104. required.push('LAST-PART');
  1105. }
  1106. this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required);
  1107. },
  1108. 'part-inf'() {
  1109. this.manifest.partInf = camelCaseKeys(entry.attributes);
  1110. this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']);
  1111. if (this.manifest.partInf.partTarget) {
  1112. this.manifest.partTargetDuration = this.manifest.partInf.partTarget;
  1113. }
  1114. setHoldBack.call(this, this.manifest);
  1115. }
  1116. })[entry.tagType] || noop).call(self);
  1117. },
  1118. uri() {
  1119. currentUri.uri = entry.uri;
  1120. uris.push(currentUri); // if no explicit duration was declared, use the target duration
  1121. if (this.manifest.targetDuration && !('duration' in currentUri)) {
  1122. this.trigger('warn', {
  1123. message: 'defaulting segment duration to the target duration'
  1124. });
  1125. currentUri.duration = this.manifest.targetDuration;
  1126. } // annotate with encryption information, if necessary
  1127. if (key) {
  1128. currentUri.key = key;
  1129. }
  1130. currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary
  1131. if (currentMap) {
  1132. currentUri.map = currentMap;
  1133. } // reset the last byterange end as it needs to be 0 between parts
  1134. lastPartByterangeEnd = 0; // prepare for the next URI
  1135. currentUri = {};
  1136. },
  1137. comment() {// comments are not important for playback
  1138. },
  1139. custom() {
  1140. // if this is segment-level data attach the output to the segment
  1141. if (entry.segment) {
  1142. currentUri.custom = currentUri.custom || {};
  1143. currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object
  1144. } else {
  1145. this.manifest.custom = this.manifest.custom || {};
  1146. this.manifest.custom[entry.customType] = entry.data;
  1147. }
  1148. }
  1149. })[entry.type].call(self);
  1150. });
  1151. }
  1152. warnOnMissingAttributes_(identifier, attributes, required) {
  1153. const missing = [];
  1154. required.forEach(function (key) {
  1155. if (!attributes.hasOwnProperty(key)) {
  1156. missing.push(key);
  1157. }
  1158. });
  1159. if (missing.length) {
  1160. this.trigger('warn', {
  1161. message: `${identifier} lacks required attribute(s): ${missing.join(', ')}`
  1162. });
  1163. }
  1164. }
  1165. /**
  1166. * Parse the input string and update the manifest object.
  1167. *
  1168. * @param {string} chunk a potentially incomplete portion of the manifest
  1169. */
  1170. push(chunk) {
  1171. this.lineStream.push(chunk);
  1172. }
  1173. /**
  1174. * Flush any remaining input. This can be handy if the last line of an M3U8
  1175. * manifest did not contain a trailing newline but the file has been
  1176. * completely received.
  1177. */
  1178. end() {
  1179. // flush any buffered input
  1180. this.lineStream.push('\n');
  1181. this.trigger('end');
  1182. }
  1183. /**
  1184. * Add an additional parser for non-standard tags
  1185. *
  1186. * @param {Object} options a map of options for the added parser
  1187. * @param {RegExp} options.expression a regular expression to match the custom header
  1188. * @param {string} options.type the type to register to the output
  1189. * @param {Function} [options.dataParser] function to parse the line into an object
  1190. * @param {boolean} [options.segment] should tag data be attached to the segment object
  1191. */
  1192. addParser(options) {
  1193. this.parseStream.addParser(options);
  1194. }
  1195. /**
  1196. * Add a custom header mapper
  1197. *
  1198. * @param {Object} options
  1199. * @param {RegExp} options.expression a regular expression to match the custom header
  1200. * @param {Function} options.map function to translate tag into a different tag
  1201. */
  1202. addTagMapper(options) {
  1203. this.parseStream.addTagMapper(options);
  1204. }
  1205. }
  1206. exports.LineStream = LineStream;
  1207. exports.ParseStream = ParseStream;
  1208. exports.Parser = Parser;