@transmitlive/m3u8-parser 4.7.2-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CONTRIBUTING.md +30 -0
  2. package/LICENSE +13 -0
  3. package/README.md +388 -0
  4. package/dist/m3u8-parser.cjs.js +1563 -0
  5. package/dist/m3u8-parser.es.js +1549 -0
  6. package/dist/m3u8-parser.js +1748 -0
  7. package/dist/m3u8-parser.min.js +2 -0
  8. package/index.html +15 -0
  9. package/package.json +100 -0
  10. package/scripts/karma.conf.js +12 -0
  11. package/scripts/rollup.config.js +47 -0
  12. package/src/index.js +19 -0
  13. package/src/line-stream.js +35 -0
  14. package/src/parse-stream.js +619 -0
  15. package/src/parser.js +748 -0
  16. package/test/fixtures/integration/absoluteUris.js +31 -0
  17. package/test/fixtures/integration/absoluteUris.m3u8 +12 -0
  18. package/test/fixtures/integration/allowCache.js +165 -0
  19. package/test/fixtures/integration/allowCache.m3u8 +58 -0
  20. package/test/fixtures/integration/allowCacheInvalid.js +21 -0
  21. package/test/fixtures/integration/allowCacheInvalid.m3u8 +10 -0
  22. package/test/fixtures/integration/alternateAudio.js +56 -0
  23. package/test/fixtures/integration/alternateAudio.m3u8 +9 -0
  24. package/test/fixtures/integration/alternateVideo.js +48 -0
  25. package/test/fixtures/integration/alternateVideo.m3u8 +8 -0
  26. package/test/fixtures/integration/brightcove.js +57 -0
  27. package/test/fixtures/integration/brightcove.m3u8 +9 -0
  28. package/test/fixtures/integration/byteRange.js +161 -0
  29. package/test/fixtures/integration/byteRange.m3u8 +56 -0
  30. package/test/fixtures/integration/dateTime.js +27 -0
  31. package/test/fixtures/integration/dateTime.m3u8 +12 -0
  32. package/test/fixtures/integration/diff-init-key.js +164 -0
  33. package/test/fixtures/integration/diff-init-key.m3u8 +57 -0
  34. package/test/fixtures/integration/disallowCache.js +21 -0
  35. package/test/fixtures/integration/disallowCache.m3u8 +10 -0
  36. package/test/fixtures/integration/disc-sequence.js +32 -0
  37. package/test/fixtures/integration/disc-sequence.m3u8 +15 -0
  38. package/test/fixtures/integration/discontinuity.js +59 -0
  39. package/test/fixtures/integration/discontinuity.m3u8 +26 -0
  40. package/test/fixtures/integration/domainUris.js +31 -0
  41. package/test/fixtures/integration/domainUris.m3u8 +12 -0
  42. package/test/fixtures/integration/empty.js +5 -0
  43. package/test/fixtures/integration/empty.m3u8 +0 -0
  44. package/test/fixtures/integration/emptyAllowCache.js +21 -0
  45. package/test/fixtures/integration/emptyAllowCache.m3u8 +10 -0
  46. package/test/fixtures/integration/emptyMediaSequence.js +31 -0
  47. package/test/fixtures/integration/emptyMediaSequence.m3u8 +14 -0
  48. package/test/fixtures/integration/emptyPlaylistType.js +40 -0
  49. package/test/fixtures/integration/emptyPlaylistType.m3u8 +16 -0
  50. package/test/fixtures/integration/emptyTargetDuration.js +57 -0
  51. package/test/fixtures/integration/emptyTargetDuration.m3u8 +10 -0
  52. package/test/fixtures/integration/encrypted.js +61 -0
  53. package/test/fixtures/integration/encrypted.m3u8 +28 -0
  54. package/test/fixtures/integration/event.js +41 -0
  55. package/test/fixtures/integration/event.m3u8 +16 -0
  56. package/test/fixtures/integration/extXPlaylistTypeInvalidPlaylist.js +15 -0
  57. package/test/fixtures/integration/extXPlaylistTypeInvalidPlaylist.m3u8 +8 -0
  58. package/test/fixtures/integration/extinf.js +165 -0
  59. package/test/fixtures/integration/extinf.m3u8 +57 -0
  60. package/test/fixtures/integration/fmp4.js +44 -0
  61. package/test/fixtures/integration/fmp4.m3u8 +14 -0
  62. package/test/fixtures/integration/headerOnly.js +5 -0
  63. package/test/fixtures/integration/headerOnly.m3u8 +1 -0
  64. package/test/fixtures/integration/invalidAllowCache.js +21 -0
  65. package/test/fixtures/integration/invalidAllowCache.m3u8 +10 -0
  66. package/test/fixtures/integration/invalidMediaSequence.js +31 -0
  67. package/test/fixtures/integration/invalidMediaSequence.m3u8 +14 -0
  68. package/test/fixtures/integration/invalidPlaylistType.js +40 -0
  69. package/test/fixtures/integration/invalidPlaylistType.m3u8 +16 -0
  70. package/test/fixtures/integration/invalidTargetDuration.js +164 -0
  71. package/test/fixtures/integration/invalidTargetDuration.m3u8 +57 -0
  72. package/test/fixtures/integration/liveMissingSegmentDuration.js +25 -0
  73. package/test/fixtures/integration/liveMissingSegmentDuration.m3u8 +9 -0
  74. package/test/fixtures/integration/liveStart30sBefore.js +54 -0
  75. package/test/fixtures/integration/liveStart30sBefore.m3u8 +22 -0
  76. package/test/fixtures/integration/llhls-byte-range.js +253 -0
  77. package/test/fixtures/integration/llhls-byte-range.m3u8 +66 -0
  78. package/test/fixtures/integration/llhls-delta-byte-range.js +149 -0
  79. package/test/fixtures/integration/llhls-delta-byte-range.m3u8 +30 -0
  80. package/test/fixtures/integration/llhls.js +214 -0
  81. package/test/fixtures/integration/llhls.m3u8 +56 -0
  82. package/test/fixtures/integration/llhlsDelta.js +186 -0
  83. package/test/fixtures/integration/llhlsDelta.m3u8 +50 -0
  84. package/test/fixtures/integration/manifestExtTTargetdurationNegative.js +14 -0
  85. package/test/fixtures/integration/manifestExtTTargetdurationNegative.m3u8 +5 -0
  86. package/test/fixtures/integration/manifestExtXEndlistEarly.js +35 -0
  87. package/test/fixtures/integration/manifestExtXEndlistEarly.m3u8 +14 -0
  88. package/test/fixtures/integration/manifestNoExtM3u.js +15 -0
  89. package/test/fixtures/integration/manifestNoExtM3u.m3u8 +4 -0
  90. package/test/fixtures/integration/master-fmp4.js +465 -0
  91. package/test/fixtures/integration/master-fmp4.m3u8 +76 -0
  92. package/test/fixtures/integration/master.js +57 -0
  93. package/test/fixtures/integration/master.m3u8 +10 -0
  94. package/test/fixtures/integration/media.js +31 -0
  95. package/test/fixtures/integration/media.m3u8 +12 -0
  96. package/test/fixtures/integration/mediaSequence.js +31 -0
  97. package/test/fixtures/integration/mediaSequence.m3u8 +14 -0
  98. package/test/fixtures/integration/missingEndlist.js +19 -0
  99. package/test/fixtures/integration/missingEndlist.m3u8 +6 -0
  100. package/test/fixtures/integration/missingExtinf.js +27 -0
  101. package/test/fixtures/integration/missingExtinf.m3u8 +11 -0
  102. package/test/fixtures/integration/missingMediaSequence.js +31 -0
  103. package/test/fixtures/integration/missingMediaSequence.m3u8 +13 -0
  104. package/test/fixtures/integration/missingSegmentDuration.js +31 -0
  105. package/test/fixtures/integration/missingSegmentDuration.m3u8 +11 -0
  106. package/test/fixtures/integration/multipleAudioGroups.js +89 -0
  107. package/test/fixtures/integration/multipleAudioGroups.m3u8 +17 -0
  108. package/test/fixtures/integration/multipleAudioGroupsCombinedMain.js +88 -0
  109. package/test/fixtures/integration/multipleAudioGroupsCombinedMain.m3u8 +17 -0
  110. package/test/fixtures/integration/multipleTargetDurations.js +28 -0
  111. package/test/fixtures/integration/multipleTargetDurations.m3u8 +8 -0
  112. package/test/fixtures/integration/multipleVideo.js +74 -0
  113. package/test/fixtures/integration/multipleVideo.m3u8 +16 -0
  114. package/test/fixtures/integration/negativeMediaSequence.js +31 -0
  115. package/test/fixtures/integration/negativeMediaSequence.m3u8 +14 -0
  116. package/test/fixtures/integration/playlist.js +165 -0
  117. package/test/fixtures/integration/playlist.m3u8 +57 -0
  118. package/test/fixtures/integration/playlistMediaSequenceHigher.js +16 -0
  119. package/test/fixtures/integration/playlistMediaSequenceHigher.m3u8 +8 -0
  120. package/test/fixtures/integration/start.js +36 -0
  121. package/test/fixtures/integration/start.m3u8 +13 -0
  122. package/test/fixtures/integration/streamInfInvalid.js +24 -0
  123. package/test/fixtures/integration/streamInfInvalid.m3u8 +6 -0
  124. package/test/fixtures/integration/twoMediaSequences.js +31 -0
  125. package/test/fixtures/integration/twoMediaSequences.m3u8 +15 -0
  126. package/test/fixtures/integration/versionInvalid.js +16 -0
  127. package/test/fixtures/integration/versionInvalid.m3u8 +8 -0
  128. package/test/fixtures/integration/whiteSpace.js +31 -0
  129. package/test/fixtures/integration/whiteSpace.m3u8 +13 -0
  130. package/test/fixtures/integration/zeroDuration.js +16 -0
  131. package/test/fixtures/integration/zeroDuration.m3u8 +7 -0
  132. package/test/line-stream.test.js +80 -0
  133. package/test/parse-stream.test.js +903 -0
  134. package/test/parser.test.js +884 -0
@@ -0,0 +1,619 @@
1
+ /**
2
+ * @file m3u8/parse-stream.js
3
+ */
4
+ import Stream from '@videojs/vhs-utils/es/stream.js';
5
+
6
+ const TAB = String.fromCharCode(0x09);
7
+
8
+ const parseByterange = function(byterangeString) {
9
+ // optionally match and capture 0+ digits before `@`
10
+ // optionally match and capture 0+ digits after `@`
11
+ const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
12
+ const result = {};
13
+
14
+ if (match[1]) {
15
+ result.length = parseInt(match[1], 10);
16
+ }
17
+
18
+ if (match[2]) {
19
+ result.offset = parseInt(match[2], 10);
20
+ }
21
+
22
+ return result;
23
+ };
24
+
25
+ /**
26
+ * "forgiving" attribute list psuedo-grammar:
27
+ * attributes -> keyvalue (',' keyvalue)*
28
+ * keyvalue -> key '=' value
29
+ * key -> [^=]*
30
+ * value -> '"' [^"]* '"' | [^,]*
31
+ */
32
+ const attributeSeparator = function() {
33
+ const key = '[^=]*';
34
+ const value = '"[^"]*"|[^,]*';
35
+ const keyvalue = '(?:' + key + ')=(?:' + value + ')';
36
+
37
+ return new RegExp('(?:^|,)(' + keyvalue + ')');
38
+ };
39
+
40
+ /**
41
+ * Parse attributes from a line given the separator
42
+ *
43
+ * @param {string} attributes the attribute line to parse
44
+ */
45
+ const parseAttributes = function(attributes) {
46
+ // split the string using attributes as the separator
47
+ const attrs = attributes.split(attributeSeparator());
48
+ const result = {};
49
+ let i = attrs.length;
50
+ let attr;
51
+
52
+ while (i--) {
53
+ // filter out unmatched portions of the string
54
+ if (attrs[i] === '') {
55
+ continue;
56
+ }
57
+
58
+ // split the key and value
59
+ attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1);
60
+ // trim whitespace and remove optional quotes around the value
61
+ attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
62
+ attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
63
+ attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
64
+ result[attr[0]] = attr[1];
65
+ }
66
+ return result;
67
+ };
68
+
69
+ /**
70
+ * A line-level M3U8 parser event stream. It expects to receive input one
71
+ * line at a time and performs a context-free parse of its contents. A stream
72
+ * interpretation of a manifest can be useful if the manifest is expected to
73
+ * be too large to fit comfortably into memory or the entirety of the input
74
+ * is not immediately available. Otherwise, it's probably much easier to work
75
+ * with a regular `Parser` object.
76
+ *
77
+ * Produces `data` events with an object that captures the parser's
78
+ * interpretation of the input. That object has a property `tag` that is one
79
+ * of `uri`, `comment`, or `tag`. URIs only have a single additional
80
+ * property, `line`, which captures the entirety of the input without
81
+ * interpretation. Comments similarly have a single additional property
82
+ * `text` which is the input without the leading `#`.
83
+ *
84
+ * Tags always have a property `tagType` which is the lower-cased version of
85
+ * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
86
+ * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
87
+ * tags are given the tag type `unknown` and a single additional property
88
+ * `data` with the remainder of the input.
89
+ *
90
+ * @class ParseStream
91
+ * @extends Stream
92
+ */
93
+ export default class ParseStream extends Stream {
94
+ constructor() {
95
+ super();
96
+ this.customParsers = [];
97
+ this.tagMappers = [];
98
+ this.lineNumber = 0;
99
+ }
100
+
101
+ /**
102
+ * Parses an additional line of input.
103
+ *
104
+ * @param {string} line a single line of an M3U8 file to parse
105
+ */
106
+ push(line) {
107
+ let match;
108
+ let event;
109
+
110
+ this.lineNumber = this.lineNumber + 1;
111
+ // strip whitespace
112
+ line = line.trim();
113
+
114
+ if (line.length === 0) {
115
+ // ignore empty lines
116
+ return;
117
+ }
118
+
119
+ // URIs
120
+ if (line[0] !== '#') {
121
+ this.trigger('data', {
122
+ type: 'uri',
123
+ uri: line
124
+ });
125
+ return;
126
+ }
127
+
128
+ // map tags
129
+ const newLines = this.tagMappers.reduce((acc, mapper) => {
130
+ const mappedLine = mapper(line);
131
+
132
+ // skip if unchanged
133
+ if (mappedLine === line) {
134
+ return acc;
135
+ }
136
+
137
+ return acc.concat([mappedLine]);
138
+ }, [line]);
139
+
140
+ newLines.forEach(newLine => {
141
+ for (let i = 0; i < this.customParsers.length; i++) {
142
+ if (this.customParsers[i].call(this, newLine)) {
143
+ return;
144
+ }
145
+ }
146
+
147
+ // Comments
148
+ if (newLine.indexOf('#EXT') !== 0) {
149
+ this.trigger('data', {
150
+ type: 'comment',
151
+ text: newLine.slice(1)
152
+ });
153
+ return;
154
+ }
155
+
156
+ // strip off any carriage returns here so the regex matching
157
+ // doesn't have to account for them.
158
+ newLine = newLine.replace('\r', '');
159
+
160
+ // Tags
161
+ match = (/^#EXTM3U/).exec(newLine);
162
+ if (match) {
163
+ this.trigger('data', {
164
+ type: 'tag',
165
+ tagType: 'm3u'
166
+ });
167
+ return;
168
+ }
169
+ match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(newLine);
170
+ if (match) {
171
+ event = {
172
+ type: 'tag',
173
+ tagType: 'inf'
174
+ };
175
+ if (match[1]) {
176
+ event.duration = parseFloat(match[1]);
177
+ }
178
+ if (match[2]) {
179
+ event.title = match[2];
180
+ }
181
+ this.trigger('data', event);
182
+ return;
183
+ }
184
+ match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(newLine);
185
+ if (match) {
186
+ event = {
187
+ type: 'tag',
188
+ tagType: 'targetduration'
189
+ };
190
+ if (match[1]) {
191
+ event.duration = parseInt(match[1], 10);
192
+ }
193
+ this.trigger('data', event);
194
+ return;
195
+ }
196
+ match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(newLine);
197
+ if (match) {
198
+ event = {
199
+ type: 'tag',
200
+ tagType: 'version'
201
+ };
202
+ if (match[1]) {
203
+ event.version = parseInt(match[1], 10);
204
+ }
205
+ this.trigger('data', event);
206
+ return;
207
+ }
208
+ match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(newLine);
209
+ if (match) {
210
+ event = {
211
+ type: 'tag',
212
+ tagType: 'media-sequence'
213
+ };
214
+ if (match[1]) {
215
+ event.number = parseInt(match[1], 10);
216
+ }
217
+ this.trigger('data', event);
218
+ return;
219
+ }
220
+ match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(newLine);
221
+ if (match) {
222
+ event = {
223
+ type: 'tag',
224
+ tagType: 'discontinuity-sequence'
225
+ };
226
+ if (match[1]) {
227
+ event.number = parseInt(match[1], 10);
228
+ }
229
+ this.trigger('data', event);
230
+ return;
231
+ }
232
+ match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(newLine);
233
+ if (match) {
234
+ event = {
235
+ type: 'tag',
236
+ tagType: 'playlist-type'
237
+ };
238
+ if (match[1]) {
239
+ event.playlistType = match[1];
240
+ }
241
+ this.trigger('data', event);
242
+ return;
243
+ }
244
+ match = (/^#EXT-X-BYTERANGE:?(.*)?$/).exec(newLine);
245
+ if (match) {
246
+ event = Object.assign(parseByterange(match[1]), {
247
+ type: 'tag',
248
+ tagType: 'byterange'
249
+ });
250
+ this.trigger('data', event);
251
+ return;
252
+ }
253
+ match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(newLine);
254
+ if (match) {
255
+ event = {
256
+ type: 'tag',
257
+ tagType: 'allow-cache'
258
+ };
259
+ if (match[1]) {
260
+ event.allowed = !(/NO/).test(match[1]);
261
+ }
262
+ this.trigger('data', event);
263
+ return;
264
+ }
265
+ match = (/^#EXT-X-MAP:?(.*)$/).exec(newLine);
266
+ if (match) {
267
+ event = {
268
+ type: 'tag',
269
+ tagType: 'map'
270
+ };
271
+
272
+ if (match[1]) {
273
+ const attributes = parseAttributes(match[1]);
274
+
275
+ if (attributes.URI) {
276
+ event.uri = attributes.URI;
277
+ }
278
+ if (attributes.BYTERANGE) {
279
+ event.byterange = parseByterange(attributes.BYTERANGE);
280
+ }
281
+ }
282
+
283
+ this.trigger('data', event);
284
+ return;
285
+ }
286
+ match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(newLine);
287
+ if (match) {
288
+ event = {
289
+ type: 'tag',
290
+ tagType: 'stream-inf'
291
+ };
292
+ if (match[1]) {
293
+ event.attributes = parseAttributes(match[1]);
294
+
295
+ if (event.attributes.RESOLUTION) {
296
+ const split = event.attributes.RESOLUTION.split('x');
297
+ const resolution = {};
298
+
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['PROGRAM-ID']) {
311
+ event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
312
+ }
313
+ }
314
+ this.trigger('data', event);
315
+ return;
316
+ }
317
+ match = (/^#EXT-X-MEDIA:?(.*)$/).exec(newLine);
318
+ if (match) {
319
+ event = {
320
+ type: 'tag',
321
+ tagType: 'media'
322
+ };
323
+ if (match[1]) {
324
+ event.attributes = parseAttributes(match[1]);
325
+ }
326
+ this.trigger('data', event);
327
+ return;
328
+ }
329
+ match = (/^#EXT-X-ENDLIST/).exec(newLine);
330
+ if (match) {
331
+ this.trigger('data', {
332
+ type: 'tag',
333
+ tagType: 'endlist'
334
+ });
335
+ return;
336
+ }
337
+ match = (/^#EXT-X-DISCONTINUITY/).exec(newLine);
338
+ if (match) {
339
+ this.trigger('data', {
340
+ type: 'tag',
341
+ tagType: 'discontinuity'
342
+ });
343
+ return;
344
+ }
345
+ match = (/^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/).exec(newLine);
346
+ if (match) {
347
+ event = {
348
+ type: 'tag',
349
+ tagType: 'program-date-time'
350
+ };
351
+ if (match[1]) {
352
+ event.dateTimeString = match[1];
353
+ event.dateTimeObject = new Date(match[1]);
354
+ }
355
+ this.trigger('data', event);
356
+ return;
357
+ }
358
+ match = (/^#EXT-X-KEY:?(.*)$/).exec(newLine);
359
+ if (match) {
360
+ event = {
361
+ type: 'tag',
362
+ tagType: 'key'
363
+ };
364
+ if (match[1]) {
365
+ event.attributes = parseAttributes(match[1]);
366
+ // parse the IV string into a Uint32Array
367
+ if (event.attributes.IV) {
368
+ if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
369
+ event.attributes.IV = event.attributes.IV.substring(2);
370
+ }
371
+
372
+ event.attributes.IV = event.attributes.IV.match(/.{8}/g);
373
+ event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
374
+ event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
375
+ event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
376
+ event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
377
+ event.attributes.IV = new Uint32Array(event.attributes.IV);
378
+ }
379
+ }
380
+ this.trigger('data', event);
381
+ return;
382
+ }
383
+ match = (/^#EXT-X-START:?(.*)$/).exec(newLine);
384
+ if (match) {
385
+ event = {
386
+ type: 'tag',
387
+ tagType: 'start'
388
+ };
389
+ if (match[1]) {
390
+ event.attributes = parseAttributes(match[1]);
391
+
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
+
448
+ if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) {
449
+ event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10);
450
+ }
451
+
452
+ if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) {
453
+ event.attributes['RECENTLY-REMOVED-DATERANGES'] =
454
+ event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB);
455
+ }
456
+
457
+ this.trigger('data', event);
458
+ return;
459
+ }
460
+ match = (/^#EXT-X-PART:(.*)$/).exec(newLine);
461
+ if (match && match[1]) {
462
+ event = {
463
+ type: 'tag',
464
+ tagType: 'part'
465
+ };
466
+ event.attributes = parseAttributes(match[1]);
467
+ ['DURATION'].forEach(function(key) {
468
+ if (event.attributes.hasOwnProperty(key)) {
469
+ event.attributes[key] = parseFloat(event.attributes[key]);
470
+ }
471
+ });
472
+
473
+ ['INDEPENDENT', 'GAP'].forEach(function(key) {
474
+ if (event.attributes.hasOwnProperty(key)) {
475
+ event.attributes[key] = (/YES/).test(event.attributes[key]);
476
+ }
477
+ });
478
+
479
+ if (event.attributes.hasOwnProperty('BYTERANGE')) {
480
+ event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
481
+ }
482
+
483
+ this.trigger('data', event);
484
+ return;
485
+ }
486
+ match = (/^#EXT-X-SERVER-CONTROL:(.*)$/).exec(newLine);
487
+ if (match && match[1]) {
488
+ event = {
489
+ type: 'tag',
490
+ tagType: 'server-control'
491
+ };
492
+ event.attributes = parseAttributes(match[1]);
493
+ ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function(key) {
494
+ if (event.attributes.hasOwnProperty(key)) {
495
+ event.attributes[key] = parseFloat(event.attributes[key]);
496
+ }
497
+ });
498
+
499
+ ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function(key) {
500
+ if (event.attributes.hasOwnProperty(key)) {
501
+ event.attributes[key] = (/YES/).test(event.attributes[key]);
502
+ }
503
+ });
504
+
505
+ this.trigger('data', event);
506
+ return;
507
+ }
508
+ match = (/^#EXT-X-PART-INF:(.*)$/).exec(newLine);
509
+ if (match && match[1]) {
510
+ event = {
511
+ type: 'tag',
512
+ tagType: 'part-inf'
513
+ };
514
+ event.attributes = parseAttributes(match[1]);
515
+ ['PART-TARGET'].forEach(function(key) {
516
+ if (event.attributes.hasOwnProperty(key)) {
517
+ event.attributes[key] = parseFloat(event.attributes[key]);
518
+ }
519
+ });
520
+
521
+ this.trigger('data', event);
522
+ return;
523
+ }
524
+
525
+ match = (/^#EXT-X-PRELOAD-HINT:(.*)$/).exec(newLine);
526
+ if (match && match[1]) {
527
+ event = {
528
+ type: 'tag',
529
+ tagType: 'preload-hint'
530
+ };
531
+ event.attributes = parseAttributes(match[1]);
532
+ ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function(key) {
533
+ if (event.attributes.hasOwnProperty(key)) {
534
+ event.attributes[key] = parseInt(event.attributes[key], 10);
535
+
536
+ const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
537
+
538
+ event.attributes.byterange = event.attributes.byterange || {};
539
+ event.attributes.byterange[subkey] = event.attributes[key];
540
+ // only keep the parsed byterange object.
541
+ delete event.attributes[key];
542
+ }
543
+ });
544
+
545
+ this.trigger('data', event);
546
+ return;
547
+ }
548
+ match = (/^#EXT-X-RENDITION-REPORT:(.*)$/).exec(newLine);
549
+ if (match && match[1]) {
550
+ event = {
551
+ type: 'tag',
552
+ tagType: 'rendition-report'
553
+ };
554
+ event.attributes = parseAttributes(match[1]);
555
+ ['LAST-MSN', 'LAST-PART'].forEach(function(key) {
556
+ if (event.attributes.hasOwnProperty(key)) {
557
+ event.attributes[key] = parseInt(event.attributes[key], 10);
558
+ }
559
+ });
560
+
561
+ this.trigger('data', event);
562
+ return;
563
+ }
564
+
565
+ // unknown tag type
566
+ this.trigger('data', {
567
+ type: 'tag',
568
+ data: newLine.slice(4)
569
+ });
570
+ });
571
+ }
572
+
573
+ /**
574
+ * Add a parser for custom headers
575
+ *
576
+ * @param {Object} options a map of options for the added parser
577
+ * @param {RegExp} options.expression a regular expression to match the custom header
578
+ * @param {string} options.customType the custom type to register to the output
579
+ * @param {Function} [options.dataParser] function to parse the line into an object
580
+ * @param {boolean} [options.segment] should tag data be attached to the segment object
581
+ */
582
+ addParser({expression, customType, dataParser, segment}) {
583
+ if (typeof dataParser !== 'function') {
584
+ dataParser = (line) => line;
585
+ }
586
+ this.customParsers.push(line => {
587
+ const match = expression.exec(line);
588
+
589
+ if (match) {
590
+ this.trigger('data', {
591
+ type: 'custom',
592
+ data: dataParser(line),
593
+ customType,
594
+ segment
595
+ });
596
+ return true;
597
+ }
598
+ });
599
+ }
600
+
601
+ /**
602
+ * Add a custom header mapper
603
+ *
604
+ * @param {Object} options
605
+ * @param {RegExp} options.expression a regular expression to match the custom header
606
+ * @param {Function} options.map function to translate tag into a different tag
607
+ */
608
+ addTagMapper({expression, map}) {
609
+ const mapFn = line => {
610
+ if (expression.test(line)) {
611
+ return map(line);
612
+ }
613
+
614
+ return line;
615
+ };
616
+
617
+ this.tagMappers.push(mapFn);
618
+ }
619
+ }