@zenvor/hls.js 1.0.0

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 (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,497 @@
1
+ /*
2
+ * Source: https://github.com/mozilla/vtt.js/blob/master/dist/vtt.js
3
+ */
4
+
5
+ import VTTCue from './vttcue';
6
+
7
+ class StringDecoder {
8
+ decode(data: string | any, options?: Object): string | never {
9
+ if (!data) {
10
+ return '';
11
+ }
12
+
13
+ if (typeof data !== 'string') {
14
+ throw new Error('Error - expected string data.');
15
+ }
16
+
17
+ return decodeURIComponent(encodeURIComponent(data));
18
+ }
19
+ }
20
+
21
+ // Try to parse input as a time stamp.
22
+ export function parseTimeStamp(input: string) {
23
+ function computeSeconds(h, m, s, f) {
24
+ return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + parseFloat(f || 0);
25
+ }
26
+
27
+ const m = input.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);
28
+ if (!m) {
29
+ return null;
30
+ }
31
+
32
+ if (parseFloat(m[2]) > 59) {
33
+ // Timestamp takes the form of [hours]:[minutes].[milliseconds]
34
+ // First position is hours as it's over 59.
35
+ return computeSeconds(m[2], m[3], 0, m[4]);
36
+ }
37
+ // Timestamp takes the form of [hours (optional)]:[minutes]:[seconds].[milliseconds]
38
+ return computeSeconds(m[1], m[2], m[3], m[4]);
39
+ }
40
+
41
+ // A settings object holds key/value pairs and will ignore anything but the first
42
+ // assignment to a specific key.
43
+ class Settings {
44
+ private readonly values: { [key: string]: any } = Object.create(null);
45
+
46
+ // Only accept the first assignment to any key.
47
+ set(k: string, v: any) {
48
+ if (!this.get(k) && v !== '') {
49
+ this.values[k] = v;
50
+ }
51
+ }
52
+ // Return the value for a key, or a default value.
53
+ // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
54
+ // a number of possible default values as properties where 'defaultKey' is
55
+ // the key of the property that will be chosen; otherwise it's assumed to be
56
+ // a single value.
57
+ get(k: string, dflt?: any, defaultKey?: string): any {
58
+ if (defaultKey) {
59
+ return this.has(k) ? this.values[k] : dflt[defaultKey];
60
+ }
61
+
62
+ return this.has(k) ? this.values[k] : dflt;
63
+ }
64
+ // Check whether we have a value for a key.
65
+ has(k: string): boolean {
66
+ return k in this.values;
67
+ }
68
+ // Accept a setting if its one of the given alternatives.
69
+ alt(k: string, v: any, a: any[]) {
70
+ for (let n = 0; n < a.length; ++n) {
71
+ if (v === a[n]) {
72
+ this.set(k, v);
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ // Accept a setting if its a valid (signed) integer.
78
+ integer(k: string, v: any) {
79
+ if (/^-?\d+$/.test(v)) {
80
+ // integer
81
+ this.set(k, parseInt(v, 10));
82
+ }
83
+ }
84
+ // Accept a setting if its a valid percentage.
85
+ percent(k: string, v: any): boolean {
86
+ if (/^([\d]{1,3})(\.[\d]*)?%$/.test(v)) {
87
+ const percent = parseFloat(v);
88
+ if (percent >= 0 && percent <= 100) {
89
+ this.set(k, percent);
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+
97
+ // Helper function to parse input into groups separated by 'groupDelim', and
98
+ // interpret each group as a key/value pair separated by 'keyValueDelim'.
99
+ function parseOptions(
100
+ input: string,
101
+ callback: (k: string, v: any) => void,
102
+ keyValueDelim: RegExp,
103
+ groupDelim?: RegExp,
104
+ ) {
105
+ const groups = groupDelim ? input.split(groupDelim) : [input];
106
+ for (const i in groups) {
107
+ if (typeof groups[i] !== 'string') {
108
+ continue;
109
+ }
110
+
111
+ const kv = groups[i].split(keyValueDelim);
112
+ if (kv.length !== 2) {
113
+ continue;
114
+ }
115
+
116
+ const k = kv[0];
117
+ const v = kv[1];
118
+ callback(k, v);
119
+ }
120
+ }
121
+
122
+ const defaults = new VTTCue(0, 0, '');
123
+ // 'middle' was changed to 'center' in the spec: https://github.com/w3c/webvtt/pull/244
124
+ // Safari doesn't yet support this change, but FF and Chrome do.
125
+ const center = (defaults.align as string) === 'middle' ? 'middle' : 'center';
126
+
127
+ function parseCue(input: string, cue: VTTCue, regionList: Region[]) {
128
+ // Remember the original input if we need to throw an error.
129
+ const oInput = input;
130
+ // 4.1 WebVTT timestamp
131
+ function consumeTimeStamp(): number | never {
132
+ const ts = parseTimeStamp(input);
133
+ if (ts === null) {
134
+ throw new Error('Malformed timestamp: ' + oInput);
135
+ }
136
+
137
+ // Remove time stamp from input.
138
+ input = input.replace(/^[^\sa-zA-Z-]+/, '');
139
+ return ts;
140
+ }
141
+
142
+ // 4.4.2 WebVTT cue settings
143
+ function consumeCueSettings(input: string, cue: VTTCue) {
144
+ const settings = new Settings();
145
+
146
+ parseOptions(
147
+ input,
148
+ function (k, v) {
149
+ let vals;
150
+ switch (k) {
151
+ case 'region':
152
+ // Find the last region we parsed with the same region id.
153
+ for (let i = regionList.length - 1; i >= 0; i--) {
154
+ if (regionList[i].id === v) {
155
+ settings.set(k, regionList[i].region);
156
+ break;
157
+ }
158
+ }
159
+ break;
160
+ case 'vertical':
161
+ settings.alt(k, v, ['rl', 'lr']);
162
+ break;
163
+ case 'line':
164
+ vals = v.split(',');
165
+ settings.integer(k, vals[0]);
166
+ if (settings.percent(k, vals[0])) {
167
+ settings.set('snapToLines', false);
168
+ }
169
+
170
+ settings.alt(k, vals[0], ['auto']);
171
+ if (vals.length === 2) {
172
+ settings.alt('lineAlign', vals[1], ['start', center, 'end']);
173
+ }
174
+
175
+ break;
176
+ case 'position':
177
+ vals = v.split(',');
178
+ settings.percent(k, vals[0]);
179
+ if (vals.length === 2) {
180
+ settings.alt('positionAlign', vals[1], [
181
+ 'start',
182
+ center,
183
+ 'end',
184
+ 'line-left',
185
+ 'line-right',
186
+ 'auto',
187
+ ]);
188
+ }
189
+
190
+ break;
191
+ case 'size':
192
+ settings.percent(k, v);
193
+ break;
194
+ case 'align':
195
+ settings.alt(k, v, ['start', center, 'end', 'left', 'right']);
196
+ break;
197
+ }
198
+ },
199
+ /:/,
200
+ /\s/,
201
+ );
202
+
203
+ // Apply default values for any missing fields.
204
+ cue.region = settings.get('region', null);
205
+ cue.vertical = settings.get('vertical', '');
206
+ let line = settings.get('line', 'auto');
207
+ if (line === 'auto' && defaults.line === -1) {
208
+ // set numeric line number for Safari
209
+ line = -1;
210
+ }
211
+ cue.line = line;
212
+ cue.lineAlign = settings.get('lineAlign', 'start');
213
+ cue.snapToLines = settings.get('snapToLines', true);
214
+ cue.size = settings.get('size', 100);
215
+ cue.align = settings.get('align', center);
216
+ let position = settings.get('position', 'auto');
217
+ if (position === 'auto' && defaults.position === 50) {
218
+ // set numeric position for Safari
219
+ position =
220
+ cue.align === 'start' || cue.align === 'left'
221
+ ? 0
222
+ : cue.align === 'end' || cue.align === 'right'
223
+ ? 100
224
+ : 50;
225
+ }
226
+ cue.position = position;
227
+ }
228
+
229
+ function skipWhitespace() {
230
+ input = input.replace(/^\s+/, '');
231
+ }
232
+
233
+ // 4.1 WebVTT cue timings.
234
+ skipWhitespace();
235
+ cue.startTime = consumeTimeStamp(); // (1) collect cue start time
236
+ skipWhitespace();
237
+ if (input.slice(0, 3) !== '-->') {
238
+ // (3) next characters must match '-->'
239
+ throw new Error(
240
+ "Malformed time stamp (time stamps must be separated by '-->'): " +
241
+ oInput,
242
+ );
243
+ }
244
+ input = input.slice(3);
245
+ skipWhitespace();
246
+ cue.endTime = consumeTimeStamp(); // (5) collect cue end time
247
+
248
+ // 4.1 WebVTT cue settings list.
249
+ skipWhitespace();
250
+ consumeCueSettings(input, cue);
251
+ }
252
+
253
+ export function fixLineBreaks(input: string): string {
254
+ return input.replace(/<br(?: \/)?>/gi, '\n');
255
+ }
256
+
257
+ type Region = {
258
+ id: string;
259
+ region: any;
260
+ };
261
+
262
+ export class VTTParser {
263
+ private state:
264
+ | 'INITIAL'
265
+ | 'HEADER'
266
+ | 'ID'
267
+ | 'CUE'
268
+ | 'CUETEXT'
269
+ | 'NOTE'
270
+ | 'BADWEBVTT'
271
+ | 'BADCUE' = 'INITIAL';
272
+ private buffer: string = '';
273
+ private decoder: StringDecoder = new StringDecoder();
274
+ private regionList: Region[] = [];
275
+ private cue: VTTCue | null = null;
276
+ public oncue?: (cue: VTTCue) => void;
277
+ public onparsingerror?: (error: Error) => void;
278
+ public onflush?: () => void;
279
+
280
+ parse(data?: string): VTTParser {
281
+ const _this = this;
282
+
283
+ // If there is no data then we won't decode it, but will just try to parse
284
+ // whatever is in buffer already. This may occur in circumstances, for
285
+ // example when flush() is called.
286
+ if (data) {
287
+ // Try to decode the data that we received.
288
+ _this.buffer += _this.decoder.decode(data, { stream: true });
289
+ }
290
+
291
+ function collectNextLine(): string {
292
+ let buffer: string = _this.buffer;
293
+ let pos = 0;
294
+
295
+ buffer = fixLineBreaks(buffer);
296
+
297
+ while (
298
+ pos < buffer.length &&
299
+ buffer[pos] !== '\r' &&
300
+ buffer[pos] !== '\n'
301
+ ) {
302
+ ++pos;
303
+ }
304
+
305
+ const line: string = buffer.slice(0, pos);
306
+ // Advance the buffer early in case we fail below.
307
+ if (buffer[pos] === '\r') {
308
+ ++pos;
309
+ }
310
+
311
+ if (buffer[pos] === '\n') {
312
+ ++pos;
313
+ }
314
+
315
+ _this.buffer = buffer.slice(pos);
316
+ return line;
317
+ }
318
+
319
+ // 3.2 WebVTT metadata header syntax
320
+ function parseHeader(input) {
321
+ parseOptions(
322
+ input,
323
+ function (k, v) {
324
+ // switch (k) {
325
+ // case 'region':
326
+ // 3.3 WebVTT region metadata header syntax
327
+ // console.log('parse region', v);
328
+ // parseRegion(v);
329
+ // break;
330
+ // }
331
+ },
332
+ /:/,
333
+ );
334
+ }
335
+
336
+ // 5.1 WebVTT file parsing.
337
+ try {
338
+ let line: string = '';
339
+ if (_this.state === 'INITIAL') {
340
+ // We can't start parsing until we have the first line.
341
+ if (!/\r\n|\n/.test(_this.buffer)) {
342
+ return this;
343
+ }
344
+
345
+ line = collectNextLine();
346
+ // strip of UTF-8 BOM if any
347
+ // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
348
+ const m = line.match(/^()?WEBVTT([ \t].*)?$/);
349
+ if (!m?.[0]) {
350
+ throw new Error('Malformed WebVTT signature.');
351
+ }
352
+
353
+ _this.state = 'HEADER';
354
+ }
355
+
356
+ let alreadyCollectedLine = false;
357
+ while (_this.buffer) {
358
+ // We can't parse a line until we have the full line.
359
+ if (!/\r\n|\n/.test(_this.buffer)) {
360
+ return this;
361
+ }
362
+
363
+ if (!alreadyCollectedLine) {
364
+ line = collectNextLine();
365
+ } else {
366
+ alreadyCollectedLine = false;
367
+ }
368
+
369
+ switch (_this.state) {
370
+ case 'HEADER':
371
+ // 13-18 - Allow a header (metadata) under the WEBVTT line.
372
+ if (/:/.test(line)) {
373
+ parseHeader(line);
374
+ } else if (!line) {
375
+ // An empty line terminates the header and starts the body (cues).
376
+ _this.state = 'ID';
377
+ }
378
+ continue;
379
+ case 'NOTE':
380
+ // Ignore NOTE blocks.
381
+ if (!line) {
382
+ _this.state = 'ID';
383
+ }
384
+
385
+ continue;
386
+ case 'ID':
387
+ // Check for the start of NOTE blocks.
388
+ if (/^NOTE($|[ \t])/.test(line)) {
389
+ _this.state = 'NOTE';
390
+ break;
391
+ }
392
+ // 19-29 - Allow any number of line terminators, then initialize new cue values.
393
+ if (!line) {
394
+ continue;
395
+ }
396
+
397
+ _this.cue = new VTTCue(0, 0, '');
398
+ _this.state = 'CUE';
399
+ // 30-39 - Check if self line contains an optional identifier or timing data.
400
+ if (line.indexOf('-->') === -1) {
401
+ _this.cue.id = line;
402
+ continue;
403
+ }
404
+ // Process line as start of a cue.
405
+ /* falls through */
406
+ case 'CUE':
407
+ // 40 - Collect cue timings and settings.
408
+ if (!_this.cue) {
409
+ _this.state = 'BADCUE';
410
+ continue;
411
+ }
412
+ try {
413
+ parseCue(line, _this.cue, _this.regionList);
414
+ } catch (e) {
415
+ // In case of an error ignore rest of the cue.
416
+ _this.cue = null;
417
+ _this.state = 'BADCUE';
418
+ continue;
419
+ }
420
+ _this.state = 'CUETEXT';
421
+ continue;
422
+ case 'CUETEXT':
423
+ {
424
+ const hasSubstring = line.indexOf('-->') !== -1;
425
+ // 34 - If we have an empty line then report the cue.
426
+ // 35 - If we have the special substring '-->' then report the cue,
427
+ // but do not collect the line as we need to process the current
428
+ // one as a new cue.
429
+ if (!line || (hasSubstring && (alreadyCollectedLine = true))) {
430
+ // We are done parsing self cue.
431
+ if (_this.oncue && _this.cue) {
432
+ _this.oncue(_this.cue);
433
+ }
434
+
435
+ _this.cue = null;
436
+ _this.state = 'ID';
437
+ continue;
438
+ }
439
+ if (_this.cue === null) {
440
+ continue;
441
+ }
442
+
443
+ if (_this.cue.text) {
444
+ _this.cue.text += '\n';
445
+ }
446
+ _this.cue.text += line;
447
+ }
448
+ continue;
449
+ case 'BADCUE':
450
+ // 54-62 - Collect and discard the remaining cue.
451
+ if (!line) {
452
+ _this.state = 'ID';
453
+ }
454
+ }
455
+ }
456
+ } catch (e) {
457
+ // If we are currently parsing a cue, report what we have.
458
+ if (_this.state === 'CUETEXT' && _this.cue && _this.oncue) {
459
+ _this.oncue(_this.cue);
460
+ }
461
+
462
+ _this.cue = null;
463
+ // Enter BADWEBVTT state if header was not parsed correctly otherwise
464
+ // another exception occurred so enter BADCUE state.
465
+ _this.state = _this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE';
466
+ }
467
+ return this;
468
+ }
469
+
470
+ flush(): VTTParser {
471
+ const _this = this;
472
+ try {
473
+ // Finish decoding the stream.
474
+ // _this.buffer += _this.decoder.decode();
475
+ // Synthesize the end of the current cue or region.
476
+ if (_this.cue || _this.state === 'HEADER') {
477
+ _this.buffer += '\n\n';
478
+ _this.parse();
479
+ }
480
+ // If we've flushed, parsed, and we're still on the INITIAL state then
481
+ // that means we don't have enough of the stream to parse the first
482
+ // line.
483
+ if (_this.state === 'INITIAL' || _this.state === 'BADWEBVTT') {
484
+ throw new Error('Malformed WebVTT signature.');
485
+ }
486
+ } catch (e) {
487
+ if (_this.onparsingerror) {
488
+ _this.onparsingerror(e);
489
+ }
490
+ }
491
+ if (_this.onflush) {
492
+ _this.onflush();
493
+ }
494
+
495
+ return this;
496
+ }
497
+ }
@@ -0,0 +1,166 @@
1
+ import { utf8ArrayToStr } from '@svta/common-media-library/utils/utf8ArrayToStr';
2
+ import { hash } from './hash';
3
+ import { toMpegTsClockFromTimescale } from './timescale-conversion';
4
+ import { VTTParser } from './vttparser';
5
+ import { normalizePts } from '../remux/mp4-remuxer';
6
+ import type { TimestampOffset } from './timescale-conversion';
7
+ import type { VTTCCs } from '../types/vtt';
8
+
9
+ const LINEBREAKS = /\r\n|\n\r|\n|\r/g;
10
+
11
+ const cueString2millis = function (timeString: string) {
12
+ let ts = parseInt(timeString.slice(-3));
13
+ const secs = parseInt(timeString.slice(-6, -4));
14
+ const mins = parseInt(timeString.slice(-9, -7));
15
+ const hours =
16
+ timeString.length > 9
17
+ ? parseInt(timeString.substring(0, timeString.indexOf(':')))
18
+ : 0;
19
+
20
+ if (
21
+ !Number.isFinite(ts) ||
22
+ !Number.isFinite(secs) ||
23
+ !Number.isFinite(mins) ||
24
+ !Number.isFinite(hours)
25
+ ) {
26
+ throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`);
27
+ }
28
+
29
+ ts += 1000 * secs;
30
+ ts += 60 * 1000 * mins;
31
+ ts += 60 * 60 * 1000 * hours;
32
+
33
+ return ts;
34
+ };
35
+
36
+ // Create a unique hash id for a cue based on start/end times and text.
37
+ // This helps timeline-controller to avoid showing repeated captions.
38
+ export function generateCueId(
39
+ startTime: number,
40
+ endTime: number,
41
+ text: string,
42
+ ) {
43
+ return hash(startTime.toString()) + hash(endTime.toString()) + hash(text);
44
+ }
45
+
46
+ export function parseWebVTT(
47
+ vttByteArray: ArrayBuffer,
48
+ initPTS: TimestampOffset | undefined,
49
+ vttCCs: VTTCCs,
50
+ cc: number,
51
+ timeOffset: number,
52
+ callBack: (cues: VTTCue[]) => void,
53
+ errorCallBack: (error: Error) => void,
54
+ ) {
55
+ const parser = new VTTParser();
56
+ // Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
57
+ // Uint8Array.prototype.reduce is not implemented in IE11
58
+ const vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray))
59
+ .trim()
60
+ .replace(LINEBREAKS, '\n')
61
+ .split('\n');
62
+ const cues: VTTCue[] = [];
63
+ const init90kHz = initPTS
64
+ ? toMpegTsClockFromTimescale(initPTS.baseTime, initPTS.timescale)
65
+ : 0;
66
+ let cueTime = '00:00.000';
67
+ let timestampMapMPEGTS = 0;
68
+ let timestampMapLOCAL = 0;
69
+ let parsingError: Error | undefined;
70
+ let inHeader = true;
71
+
72
+ parser.oncue = function (cue: VTTCue) {
73
+ // Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline.
74
+ const currCC = vttCCs[cc];
75
+ let cueOffset = vttCCs.ccOffset;
76
+
77
+ // Calculate subtitle PTS offset
78
+ const webVttMpegTsMapOffset = (timestampMapMPEGTS - init90kHz) / 90000;
79
+
80
+ // Update offsets for new discontinuities
81
+ if (currCC?.new) {
82
+ // When local time is provided, offset = discontinuity start time - local time
83
+ cueOffset = vttCCs.ccOffset = currCC.start;
84
+ }
85
+ if (webVttMpegTsMapOffset) {
86
+ if (!initPTS) {
87
+ parsingError = new Error('Missing initPTS for VTT MPEGTS');
88
+ return;
89
+ }
90
+ // If we have MPEGTS, offset = presentation time + discontinuity offset
91
+ cueOffset = webVttMpegTsMapOffset - vttCCs.presentationOffset;
92
+ }
93
+
94
+ const duration = cue.endTime - cue.startTime;
95
+ const startTime =
96
+ normalizePts(
97
+ (cue.startTime + cueOffset - timestampMapLOCAL) * 90000,
98
+ timeOffset * 90000,
99
+ ) / 90000;
100
+ cue.startTime = Math.max(startTime, 0);
101
+ cue.endTime = Math.max(startTime + duration, 0);
102
+
103
+ //trim trailing webvtt block whitespaces
104
+ const text = cue.text.trim();
105
+
106
+ // Fix encoding of special characters
107
+ cue.text = decodeURIComponent(encodeURIComponent(text));
108
+
109
+ // If the cue was not assigned an id from the VTT file (line above the content), create one.
110
+ if (!cue.id) {
111
+ cue.id = generateCueId(cue.startTime, cue.endTime, text);
112
+ }
113
+
114
+ if (cue.endTime > 0) {
115
+ cues.push(cue);
116
+ }
117
+ };
118
+
119
+ parser.onparsingerror = function (error: Error) {
120
+ parsingError = error;
121
+ };
122
+
123
+ parser.onflush = function () {
124
+ if (parsingError) {
125
+ errorCallBack(parsingError);
126
+ return;
127
+ }
128
+ callBack(cues);
129
+ };
130
+
131
+ // Go through contents line by line.
132
+ vttLines.forEach((line) => {
133
+ if (inHeader) {
134
+ // Look for X-TIMESTAMP-MAP in header.
135
+ if (line.startsWith('X-TIMESTAMP-MAP=')) {
136
+ // Once found, no more are allowed anyway, so stop searching.
137
+ inHeader = false;
138
+ // Extract LOCAL and MPEGTS.
139
+ line
140
+ .slice(16)
141
+ .split(',')
142
+ .forEach((timestamp) => {
143
+ if (timestamp.startsWith('LOCAL:')) {
144
+ cueTime = timestamp.slice(6);
145
+ } else if (timestamp.startsWith('MPEGTS:')) {
146
+ timestampMapMPEGTS = parseInt(timestamp.slice(7));
147
+ }
148
+ });
149
+ try {
150
+ // Convert cue time to seconds
151
+ timestampMapLOCAL = cueString2millis(cueTime) / 1000;
152
+ } catch (error) {
153
+ parsingError = error;
154
+ }
155
+ // Return without parsing X-TIMESTAMP-MAP line.
156
+ return;
157
+ } else if (line === '') {
158
+ inHeader = false;
159
+ }
160
+ }
161
+ // Parse line by default.
162
+ parser.parse(line + '\n');
163
+ });
164
+
165
+ parser.flush();
166
+ }