forked from oscar-davids/hls.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webvtt-parser.js
174 lines (149 loc) · 5.62 KB
/
webvtt-parser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import VTTParser from './vttparser';
import { utf8ArrayToStr } from '../demux/id3';
// String.prototype.startsWith is not supported in IE11
const startsWith = function (inputString, searchString, position) {
return inputString.substr(position || 0, searchString.length) === searchString;
};
const cueString2millis = function (timeString) {
let ts = parseInt(timeString.substr(-3));
let secs = parseInt(timeString.substr(-6, 2));
let mins = parseInt(timeString.substr(-9, 2));
let hours = timeString.length > 9 ? parseInt(timeString.substr(0, timeString.indexOf(':'))) : 0;
if (!Number.isFinite(ts) || !Number.isFinite(secs) || !Number.isFinite(mins) || !Number.isFinite(hours)) {
throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`);
}
ts += 1000 * secs;
ts += 60 * 1000 * mins;
ts += 60 * 60 * 1000 * hours;
return ts;
};
// From https://github.com/darkskyapp/string-hash
const hash = function (text) {
let hash = 5381;
let i = text.length;
while (i) {
hash = (hash * 33) ^ text.charCodeAt(--i);
}
return (hash >>> 0).toString();
};
const calculateOffset = function (vttCCs, cc, presentationTime) {
let currCC = vttCCs[cc];
let prevCC = vttCCs[currCC.prevCC];
// This is the first discontinuity or cues have been processed since the last discontinuity
// Offset = current discontinuity time
if (!prevCC || (!prevCC.new && currCC.new)) {
vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start;
currCC.new = false;
return;
}
// There have been discontinuities since cues were last parsed.
// Offset = time elapsed
while (prevCC && prevCC.new) {
vttCCs.ccOffset += currCC.start - prevCC.start;
currCC.new = false;
currCC = prevCC;
prevCC = vttCCs[currCC.prevCC];
}
vttCCs.presentationOffset = presentationTime;
};
const WebVTTParser = {
parse: function (vttByteArray, syncPTS, vttCCs, cc, callBack, errorCallBack) {
// Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
let re = /\r\n|\n\r|\n|\r/g;
// Uint8Array.prototype.reduce is not implemented in IE11
let vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray)).trim().replace(re, '\n').split('\n');
let cueTime = '00:00.000';
let mpegTs = 0;
let localTime = 0;
let presentationTime = 0;
let cues = [];
let parsingError;
let inHeader = true;
let timestampMap = false;
// let VTTCue = VTTCue || window.TextTrackCue;
// Create parser object using VTTCue with TextTrackCue fallback on certain browsers.
let parser = new VTTParser();
parser.oncue = function (cue) {
// Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline.
let currCC = vttCCs[cc];
let cueOffset = vttCCs.ccOffset;
// Update offsets for new discontinuities
if (currCC && currCC.new) {
if (localTime !== undefined) {
// When local time is provided, offset = discontinuity start time - local time
cueOffset = vttCCs.ccOffset = currCC.start;
} else {
calculateOffset(vttCCs, cc, presentationTime);
}
}
if (presentationTime) {
// If we have MPEGTS, offset = presentation time + discontinuity offset
cueOffset = presentationTime - vttCCs.presentationOffset;
}
if (timestampMap) {
cue.startTime += cueOffset - localTime;
cue.endTime += cueOffset - localTime;
}
// Create a unique hash id for a cue based on start/end times and text.
// This helps timeline-controller to avoid showing repeated captions.
cue.id = hash(cue.startTime.toString()) + hash(cue.endTime.toString()) + hash(cue.text);
// Fix encoding of special characters. TODO: Test with all sorts of weird characters.
cue.text = decodeURIComponent(encodeURIComponent(cue.text));
if (cue.endTime > 0) {
cues.push(cue);
}
};
parser.onparsingerror = function (e) {
parsingError = e;
};
parser.onflush = function () {
if (parsingError && errorCallBack) {
errorCallBack(parsingError);
return;
}
callBack(cues);
};
// Go through contents line by line.
vttLines.forEach(line => {
if (inHeader) {
// Look for X-TIMESTAMP-MAP in header.
if (startsWith(line, 'X-TIMESTAMP-MAP=')) {
// Once found, no more are allowed anyway, so stop searching.
inHeader = false;
timestampMap = true;
// Extract LOCAL and MPEGTS.
line.substr(16).split(',').forEach(timestamp => {
if (startsWith(timestamp, 'LOCAL:')) {
cueTime = timestamp.substr(6);
} else if (startsWith(timestamp, 'MPEGTS:')) {
mpegTs = parseInt(timestamp.substr(7));
}
});
try {
// Calculate subtitle offset in milliseconds.
if (syncPTS + ((vttCCs[cc].start * 90000) || 0) < 0) {
syncPTS += 8589934592;
}
// Adjust MPEGTS by sync PTS.
mpegTs -= syncPTS;
// Convert cue time to seconds
localTime = cueString2millis(cueTime) / 1000;
// Convert MPEGTS to seconds from 90kHz.
presentationTime = mpegTs / 90000;
} catch (e) {
timestampMap = false;
parsingError = e;
}
// Return without parsing X-TIMESTAMP-MAP line.
return;
} else if (line === '') {
inHeader = false;
}
}
// Parse line by default.
parser.parse(line + '\n');
});
parser.flush();
}
};
export default WebVTTParser;