@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.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- 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
|
+
}
|