1 |
|
2 |
|
3 |
|
4 | import SegmentLoader from './segment-loader';
|
5 | import videojs from 'video.js';
|
6 | import window from 'global/window';
|
7 | import { removeCuesFromTrack, removeDuplicateCuesFromTrack } from './util/text-tracks';
|
8 | import { initSegmentId } from './bin-utils';
|
9 | import { uint8ToUtf8 } from './util/string';
|
10 | import { REQUEST_ERRORS } from './media-segment-request';
|
11 | import { ONE_SECOND_IN_TS } from 'mux.js/lib/utils/clock';
|
12 |
|
13 | const VTT_LINE_TERMINATORS =
|
14 | new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0)));
|
15 |
|
16 | class NoVttJsError extends Error {
|
17 | constructor() {
|
18 | super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.');
|
19 | }
|
20 | }
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export default class VTTSegmentLoader extends SegmentLoader {
|
30 | constructor(settings, options = {}) {
|
31 | super(settings, options);
|
32 |
|
33 |
|
34 |
|
35 | this.mediaSource_ = null;
|
36 |
|
37 | this.subtitlesTrack_ = null;
|
38 |
|
39 | this.loaderType_ = 'subtitle';
|
40 |
|
41 | this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks;
|
42 |
|
43 | this.loadVttJs = settings.loadVttJs;
|
44 |
|
45 |
|
46 |
|
47 | this.shouldSaveSegmentTimingInfo_ = false;
|
48 | }
|
49 |
|
50 | createTransmuxer_() {
|
51 |
|
52 | return null;
|
53 | }
|
54 |
|
55 | |
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | buffered_() {
|
62 | if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) {
|
63 | return videojs.createTimeRanges();
|
64 | }
|
65 |
|
66 | const cues = this.subtitlesTrack_.cues;
|
67 | const start = cues[0].startTime;
|
68 | const end = cues[cues.length - 1].startTime;
|
69 |
|
70 | return videojs.createTimeRanges([[start, end]]);
|
71 | }
|
72 |
|
73 | |
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | initSegmentForMap(map, set = false) {
|
84 | if (!map) {
|
85 | return null;
|
86 | }
|
87 |
|
88 | const id = initSegmentId(map);
|
89 | let storedMap = this.initSegments_[id];
|
90 |
|
91 | if (set && !storedMap && map.bytes) {
|
92 |
|
93 |
|
94 |
|
95 |
|
96 | const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
|
97 | const combinedSegment = new Uint8Array(combinedByteLength);
|
98 |
|
99 | combinedSegment.set(map.bytes);
|
100 | combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
|
101 |
|
102 | this.initSegments_[id] = storedMap = {
|
103 | resolvedUri: map.resolvedUri,
|
104 | byterange: map.byterange,
|
105 | bytes: combinedSegment
|
106 | };
|
107 | }
|
108 |
|
109 | return storedMap || map;
|
110 | }
|
111 |
|
112 | |
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | couldBeginLoading_() {
|
119 | return this.playlist_ &&
|
120 | this.subtitlesTrack_ &&
|
121 | !this.paused();
|
122 | }
|
123 |
|
124 | |
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | init_() {
|
132 | this.state = 'READY';
|
133 | this.resetEverything();
|
134 | return this.monitorBuffer_();
|
135 | }
|
136 |
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | track(track) {
|
146 | if (typeof track === 'undefined') {
|
147 | return this.subtitlesTrack_;
|
148 | }
|
149 |
|
150 | this.subtitlesTrack_ = track;
|
151 |
|
152 |
|
153 |
|
154 | if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
155 | this.init_();
|
156 | }
|
157 |
|
158 | return this.subtitlesTrack_;
|
159 | }
|
160 |
|
161 | |
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | remove(start, end) {
|
168 | removeCuesFromTrack(start, end, this.subtitlesTrack_);
|
169 | }
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 | fillBuffer_() {
|
181 |
|
182 | const segmentInfo = this.chooseNextRequest_();
|
183 |
|
184 | if (!segmentInfo) {
|
185 | return;
|
186 | }
|
187 |
|
188 | if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
|
189 |
|
190 |
|
191 | const checkTimestampOffset = () => {
|
192 | this.state = 'READY';
|
193 | if (!this.paused()) {
|
194 |
|
195 | this.monitorBuffer_();
|
196 | }
|
197 | };
|
198 |
|
199 | this.syncController_.one('timestampoffset', checkTimestampOffset);
|
200 | this.state = 'WAITING_ON_TIMELINE';
|
201 | return;
|
202 | }
|
203 |
|
204 | this.loadSegment_(segmentInfo);
|
205 | }
|
206 |
|
207 |
|
208 | timestampOffsetForSegment_() {
|
209 | return null;
|
210 | }
|
211 |
|
212 | chooseNextRequest_() {
|
213 | return this.skipEmptySegments_(super.chooseNextRequest_());
|
214 | }
|
215 |
|
216 | |
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | skipEmptySegments_(segmentInfo) {
|
227 | while (segmentInfo && segmentInfo.segment.empty) {
|
228 |
|
229 | if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) {
|
230 | segmentInfo = null;
|
231 | break;
|
232 | }
|
233 | segmentInfo = this.generateSegmentInfo_({
|
234 | playlist: segmentInfo.playlist,
|
235 | mediaIndex: segmentInfo.mediaIndex + 1,
|
236 | startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration,
|
237 | isSyncRequest: segmentInfo.isSyncRequest
|
238 | });
|
239 | }
|
240 | return segmentInfo;
|
241 | }
|
242 |
|
243 | stopForError(error) {
|
244 | this.error(error);
|
245 | this.state = 'READY';
|
246 | this.pause();
|
247 | this.trigger('error');
|
248 | }
|
249 |
|
250 | |
251 |
|
252 |
|
253 |
|
254 |
|
255 | segmentRequestFinished_(error, simpleSegment, result) {
|
256 | if (!this.subtitlesTrack_) {
|
257 | this.state = 'READY';
|
258 | return;
|
259 | }
|
260 |
|
261 | this.saveTransferStats_(simpleSegment.stats);
|
262 |
|
263 |
|
264 | if (!this.pendingSegment_) {
|
265 | this.state = 'READY';
|
266 | this.mediaRequestsAborted += 1;
|
267 | return;
|
268 | }
|
269 |
|
270 | if (error) {
|
271 | if (error.code === REQUEST_ERRORS.TIMEOUT) {
|
272 | this.handleTimeout_();
|
273 | }
|
274 |
|
275 | if (error.code === REQUEST_ERRORS.ABORTED) {
|
276 | this.mediaRequestsAborted += 1;
|
277 | } else {
|
278 | this.mediaRequestsErrored += 1;
|
279 | }
|
280 |
|
281 | this.stopForError(error);
|
282 | return;
|
283 | }
|
284 |
|
285 | const segmentInfo = this.pendingSegment_;
|
286 |
|
287 |
|
288 |
|
289 | this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);
|
290 |
|
291 |
|
292 | if (simpleSegment.key) {
|
293 | this.segmentKey(simpleSegment.key, true);
|
294 | }
|
295 |
|
296 | this.state = 'APPENDING';
|
297 |
|
298 |
|
299 | this.trigger('appending');
|
300 |
|
301 | const segment = segmentInfo.segment;
|
302 |
|
303 | if (segment.map) {
|
304 | segment.map.bytes = simpleSegment.map.bytes;
|
305 | }
|
306 | segmentInfo.bytes = simpleSegment.bytes;
|
307 |
|
308 |
|
309 | if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') {
|
310 | this.state = 'WAITING_ON_VTTJS';
|
311 |
|
312 |
|
313 | this.loadVttJs()
|
314 | .then(
|
315 | () => this.segmentRequestFinished_(error, simpleSegment, result),
|
316 | () => this.stopForError({ message: 'Error loading vtt.js' })
|
317 | );
|
318 | return;
|
319 | }
|
320 |
|
321 | segment.requested = true;
|
322 |
|
323 | try {
|
324 | this.parseVTTCues_(segmentInfo);
|
325 | } catch (e) {
|
326 | this.stopForError({
|
327 | message: e.message
|
328 | });
|
329 | return;
|
330 | }
|
331 |
|
332 | this.updateTimeMapping_(
|
333 | segmentInfo,
|
334 | this.syncController_.timelines[segmentInfo.timeline],
|
335 | this.playlist_
|
336 | );
|
337 |
|
338 | if (segmentInfo.cues.length) {
|
339 | segmentInfo.timingInfo = {
|
340 | start: segmentInfo.cues[0].startTime,
|
341 | end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime
|
342 | };
|
343 | } else {
|
344 | segmentInfo.timingInfo = {
|
345 | start: segmentInfo.startOfSegment,
|
346 | end: segmentInfo.startOfSegment + segmentInfo.duration
|
347 | };
|
348 | }
|
349 |
|
350 | if (segmentInfo.isSyncRequest) {
|
351 | this.trigger('syncinfoupdate');
|
352 | this.pendingSegment_ = null;
|
353 | this.state = 'READY';
|
354 | return;
|
355 | }
|
356 |
|
357 | segmentInfo.byteLength = segmentInfo.bytes.byteLength;
|
358 |
|
359 | this.mediaSecondsLoaded += segment.duration;
|
360 |
|
361 |
|
362 |
|
363 | segmentInfo.cues.forEach((cue) => {
|
364 | this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ?
|
365 | new window.VTTCue(cue.startTime, cue.endTime, cue.text) :
|
366 | cue);
|
367 | });
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 | removeDuplicateCuesFromTrack(this.subtitlesTrack_);
|
374 |
|
375 | this.handleAppendsDone_();
|
376 | }
|
377 |
|
378 | handleData_() {
|
379 |
|
380 |
|
381 | }
|
382 | updateTimingInfoEnd_() {
|
383 |
|
384 | }
|
385 |
|
386 | |
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 | parseVTTCues_(segmentInfo) {
|
396 | let decoder;
|
397 | let decodeBytesToString = false;
|
398 |
|
399 | if (typeof window.WebVTT !== 'function') {
|
400 |
|
401 | throw new NoVttJsError();
|
402 | }
|
403 |
|
404 | if (typeof window.TextDecoder === 'function') {
|
405 | decoder = new window.TextDecoder('utf8');
|
406 | } else {
|
407 | decoder = window.WebVTT.StringDecoder();
|
408 | decodeBytesToString = true;
|
409 | }
|
410 |
|
411 | const parser = new window.WebVTT.Parser(
|
412 | window,
|
413 | window.vttjs,
|
414 | decoder
|
415 | );
|
416 |
|
417 | segmentInfo.cues = [];
|
418 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
419 |
|
420 | parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
|
421 | parser.ontimestampmap = (map) => {
|
422 | segmentInfo.timestampmap = map;
|
423 | };
|
424 | parser.onparsingerror = (error) => {
|
425 | videojs.log.warn('Error encountered when parsing cues: ' + error.message);
|
426 | };
|
427 |
|
428 | if (segmentInfo.segment.map) {
|
429 | let mapData = segmentInfo.segment.map.bytes;
|
430 |
|
431 | if (decodeBytesToString) {
|
432 | mapData = uint8ToUtf8(mapData);
|
433 | }
|
434 |
|
435 | parser.parse(mapData);
|
436 | }
|
437 |
|
438 | let segmentData = segmentInfo.bytes;
|
439 |
|
440 | if (decodeBytesToString) {
|
441 | segmentData = uint8ToUtf8(segmentData);
|
442 | }
|
443 |
|
444 | parser.parse(segmentData);
|
445 | parser.flush();
|
446 | }
|
447 |
|
448 | |
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 | updateTimeMapping_(segmentInfo, mappingObj, playlist) {
|
462 | const segment = segmentInfo.segment;
|
463 |
|
464 | if (!mappingObj) {
|
465 |
|
466 |
|
467 |
|
468 | return;
|
469 | }
|
470 |
|
471 | if (!segmentInfo.cues.length) {
|
472 |
|
473 |
|
474 |
|
475 | segment.empty = true;
|
476 | return;
|
477 | }
|
478 |
|
479 | const timestampmap = segmentInfo.timestampmap;
|
480 | const diff = (timestampmap.MPEGTS / ONE_SECOND_IN_TS) - timestampmap.LOCAL + mappingObj.mapping;
|
481 |
|
482 | segmentInfo.cues.forEach((cue) => {
|
483 |
|
484 | cue.startTime += diff;
|
485 | cue.endTime += diff;
|
486 | });
|
487 |
|
488 | if (!playlist.syncInfo) {
|
489 | const firstStart = segmentInfo.cues[0].startTime;
|
490 | const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
|
491 |
|
492 | playlist.syncInfo = {
|
493 | mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
|
494 | time: Math.min(firstStart, lastStart - segment.duration)
|
495 | };
|
496 | }
|
497 | }
|
498 | }
|