1 | import QUnit from 'qunit';
|
2 | import VTTSegmentLoader from '../src/vtt-segment-loader';
|
3 | import videojs from 'video.js';
|
4 | import {
|
5 | playlistWithDuration as oldPlaylistWithDuration,
|
6 | MockTextTrack
|
7 | } from './test-helpers.js';
|
8 | import {
|
9 | LoaderCommonHooks,
|
10 | LoaderCommonSettings,
|
11 | LoaderCommonFactory
|
12 | } from './loader-common.js';
|
13 |
|
14 | const oldVTT = window.WebVTT;
|
15 |
|
16 | const playlistWithDuration = function(time, conf) {
|
17 | return oldPlaylistWithDuration(time, videojs.mergeOptions({ extension: '.vtt' }, conf));
|
18 | };
|
19 |
|
20 | QUnit.module('VTTSegmentLoader', function(hooks) {
|
21 | hooks.beforeEach(function(assert) {
|
22 | LoaderCommonHooks.beforeEach.call(this);
|
23 |
|
24 | this.parserCreated = false;
|
25 |
|
26 | window.WebVTT = () => {};
|
27 | window.WebVTT.StringDecoder = () => {};
|
28 | window.WebVTT.Parser = () => {
|
29 | this.parserCreated = true;
|
30 | return {
|
31 | oncue() {},
|
32 | onparsingerror() {},
|
33 | onflush() {},
|
34 | parse() {},
|
35 | flush() {}
|
36 | };
|
37 | };
|
38 |
|
39 |
|
40 | this.syncController.timelines[0] = { time: 0, mapping: 0 };
|
41 | });
|
42 |
|
43 | hooks.afterEach(function(assert) {
|
44 | LoaderCommonHooks.afterEach.call(this);
|
45 |
|
46 | window.WebVTT = oldVTT;
|
47 | });
|
48 |
|
49 | LoaderCommonFactory(VTTSegmentLoader,
|
50 | { loaderType: 'vtt' },
|
51 | (loader) => loader.track(new MockTextTrack()));
|
52 |
|
53 |
|
54 | QUnit.module('Loader VTT', function(nestedHooks) {
|
55 | let loader;
|
56 |
|
57 | nestedHooks.beforeEach(function(assert) {
|
58 | loader = new VTTSegmentLoader(LoaderCommonSettings.call(this, {
|
59 | loaderType: 'vtt'
|
60 | }), {});
|
61 |
|
62 | this.track = new MockTextTrack();
|
63 | });
|
64 |
|
65 | QUnit.test(`load waits until a playlist and track are specified to proceed`,
|
66 | function(assert) {
|
67 | loader.load();
|
68 |
|
69 | assert.equal(loader.state, 'INIT', 'waiting in init');
|
70 | assert.equal(loader.paused(), false, 'not paused');
|
71 |
|
72 | loader.playlist(playlistWithDuration(10));
|
73 | assert.equal(this.requests.length, 0, 'have not made a request yet');
|
74 | loader.track(this.track);
|
75 | this.clock.tick(1);
|
76 |
|
77 | assert.equal(this.requests.length, 1, 'made a request');
|
78 | assert.equal(loader.state, 'WAITING', 'transitioned states');
|
79 | });
|
80 |
|
81 | QUnit.test(`calling track and load begins buffering`, function(assert) {
|
82 | assert.equal(loader.state, 'INIT', 'starts in the init state');
|
83 | loader.playlist(playlistWithDuration(10));
|
84 | assert.equal(loader.state, 'INIT', 'starts in the init state');
|
85 | assert.ok(loader.paused(), 'starts paused');
|
86 |
|
87 | loader.track(this.track);
|
88 | assert.equal(loader.state, 'INIT', 'still in the init state');
|
89 | loader.load();
|
90 | this.clock.tick(1);
|
91 |
|
92 | assert.equal(loader.state, 'WAITING', 'moves to the ready state');
|
93 | assert.ok(!loader.paused(), 'loading is not paused');
|
94 | assert.equal(this.requests.length, 1, 'requested a segment');
|
95 | });
|
96 |
|
97 | QUnit.test('saves segment info to new segment after playlist refresh',
|
98 | function(assert) {
|
99 | let playlist = playlistWithDuration(40);
|
100 | let buffered = videojs.createTimeRanges();
|
101 |
|
102 | loader.buffered_ = () => buffered;
|
103 |
|
104 | playlist.endList = false;
|
105 |
|
106 | loader.playlist(playlist);
|
107 | loader.track(this.track);
|
108 | loader.load();
|
109 | this.clock.tick(1);
|
110 |
|
111 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
112 | assert.equal(loader.pendingSegment_.uri, '0.vtt', 'first segment pending');
|
113 | assert.equal(loader.pendingSegment_.segment.uri,
|
114 | '0.vtt',
|
115 | 'correct segment reference');
|
116 |
|
117 |
|
118 | this.requests[0].response = new Uint8Array(10).buffer;
|
119 | this.requests.shift().respond(200, null, '');
|
120 | buffered = videojs.createTimeRanges([[0, 10]]);
|
121 | this.clock.tick(1);
|
122 |
|
123 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
124 | assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment pending');
|
125 | assert.equal(loader.pendingSegment_.segment.uri,
|
126 | '1.vtt',
|
127 | 'correct segment reference');
|
128 |
|
129 |
|
130 | let playlistUpdated = playlistWithDuration(40);
|
131 |
|
132 | playlistUpdated.segments.shift();
|
133 | playlistUpdated.mediaSequence++;
|
134 | loader.playlist(playlistUpdated);
|
135 |
|
136 | assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment still pending');
|
137 | assert.equal(loader.pendingSegment_.segment.uri,
|
138 | '1.vtt',
|
139 | 'correct segment reference');
|
140 |
|
141 |
|
142 | loader.parseVTTCues_ = (segmentInfo) => {
|
143 | segmentInfo.cues = [];
|
144 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
145 | };
|
146 |
|
147 | this.requests[0].response = new Uint8Array(10).buffer;
|
148 | this.requests.shift().respond(200, null, '');
|
149 |
|
150 | assert.ok(playlistUpdated.segments[0].empty,
|
151 | 'set empty on segment of new playlist');
|
152 | assert.ok(!playlist.segments[1].empty,
|
153 | 'did not set empty on segment of old playlist');
|
154 | });
|
155 |
|
156 | QUnit.test(
|
157 | 'saves segment info to old segment after playlist refresh if segment fell off',
|
158 | function(assert) {
|
159 | let playlist = playlistWithDuration(40);
|
160 | let buffered = videojs.createTimeRanges();
|
161 |
|
162 | loader.buffered_ = () => buffered;
|
163 |
|
164 | playlist.endList = false;
|
165 |
|
166 | loader.playlist(playlist);
|
167 | loader.track(this.track);
|
168 | loader.load();
|
169 | this.clock.tick(1);
|
170 |
|
171 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
172 | assert.equal(loader.pendingSegment_.uri, '0.vtt', 'first segment pending');
|
173 | assert.equal(loader.pendingSegment_.segment.uri,
|
174 | '0.vtt',
|
175 | 'correct segment reference');
|
176 |
|
177 |
|
178 | this.requests[0].response = new Uint8Array(10).buffer;
|
179 | this.requests.shift().respond(200, null, '');
|
180 | buffered = videojs.createTimeRanges([[0, 10]]);
|
181 | this.clock.tick(1);
|
182 |
|
183 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
184 | assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment pending');
|
185 | assert.equal(loader.pendingSegment_.segment.uri,
|
186 | '1.vtt',
|
187 | 'correct segment reference');
|
188 |
|
189 |
|
190 | let playlistUpdated = playlistWithDuration(40);
|
191 |
|
192 | playlistUpdated.segments.shift();
|
193 | playlistUpdated.segments.shift();
|
194 | playlistUpdated.mediaSequence += 2;
|
195 | loader.playlist(playlistUpdated);
|
196 |
|
197 | assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment still pending');
|
198 | assert.equal(loader.pendingSegment_.segment.uri,
|
199 | '1.vtt',
|
200 | 'correct segment reference');
|
201 |
|
202 |
|
203 | loader.parseVTTCues_ = (segmentInfo) => {
|
204 | segmentInfo.cues = [];
|
205 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
206 | };
|
207 |
|
208 | this.requests[0].response = new Uint8Array(10).buffer;
|
209 | this.requests.shift().respond(200, null, '');
|
210 |
|
211 | assert.ok(playlist.segments[1].empty,
|
212 | 'set empty on segment of old playlist');
|
213 | assert.ok(!playlistUpdated.segments[0].empty,
|
214 | 'no empty info for first segment of new playlist');
|
215 | });
|
216 |
|
217 | QUnit.test('waits for syncController to have sync info for the timeline of the vtt' +
|
218 | 'segment being requested before loading', function(assert) {
|
219 | let playlist = playlistWithDuration(40);
|
220 | let loadedSegment = false;
|
221 |
|
222 | loader.loadSegment_ = () => {
|
223 | loader.state = 'WAITING';
|
224 | loadedSegment = true;
|
225 | };
|
226 | loader.checkBuffer_ = () => {
|
227 | return { mediaIndex: 2, timeline: 2, segment: { } };
|
228 | };
|
229 |
|
230 | loader.playlist(playlist);
|
231 | loader.track(this.track);
|
232 | loader.load();
|
233 |
|
234 | assert.equal(loader.state, 'READY', 'loader is ready at start');
|
235 | assert.ok(!loadedSegment, 'no segment requests made yet');
|
236 |
|
237 | this.clock.tick(1);
|
238 |
|
239 | assert.equal(loader.state,
|
240 | 'WAITING_ON_TIMELINE',
|
241 | 'loader waiting for timeline info');
|
242 | assert.ok(!loadedSegment, 'no segment requests made yet');
|
243 |
|
244 |
|
245 | loader.syncController_.timelines[2] = { time: 20, mapping: -10 };
|
246 | loader.syncController_.trigger('timestampoffset');
|
247 |
|
248 | assert.equal(loader.state,
|
249 | 'READY',
|
250 | 'ready after sync controller reports timeline info');
|
251 | assert.ok(!loadedSegment, 'no segment requests made yet');
|
252 |
|
253 | this.clock.tick(1);
|
254 |
|
255 | assert.equal(loader.state, 'WAITING', 'loader waiting on segment request');
|
256 | assert.ok(loadedSegment, 'made call to load segment on new timeline');
|
257 | });
|
258 |
|
259 | QUnit.test('waits for vtt.js to be loaded before attempting to parse cues',
|
260 | function(assert) {
|
261 | const vttjs = window.WebVTT;
|
262 | let playlist = playlistWithDuration(40);
|
263 | let parsedCues = false;
|
264 |
|
265 | delete window.WebVTT;
|
266 |
|
267 | loader.handleUpdateEnd_ = () => {
|
268 | parsedCues = true;
|
269 | loader.state = 'READY';
|
270 | };
|
271 |
|
272 | let vttjsCallback = () => {};
|
273 |
|
274 | this.track.tech_ = {
|
275 | one(event, callback) {
|
276 | if (event === 'vttjsloaded') {
|
277 | vttjsCallback = callback;
|
278 | }
|
279 | },
|
280 | trigger(event) {
|
281 | if (event === 'vttjsloaded') {
|
282 | vttjsCallback();
|
283 | }
|
284 | },
|
285 | off() {}
|
286 | };
|
287 |
|
288 | loader.playlist(playlist);
|
289 | loader.track(this.track);
|
290 | loader.load();
|
291 |
|
292 | assert.equal(loader.state, 'READY', 'loader is ready at start');
|
293 | assert.ok(!parsedCues, 'no cues parsed yet');
|
294 |
|
295 | this.clock.tick(1);
|
296 |
|
297 | assert.equal(loader.state, 'WAITING', 'loader is waiting on segment request');
|
298 | assert.ok(!parsedCues, 'no cues parsed yet');
|
299 |
|
300 | this.requests[0].response = new Uint8Array(10).buffer;
|
301 | this.requests.shift().respond(200, null, '');
|
302 |
|
303 | this.clock.tick(1);
|
304 |
|
305 | assert.equal(loader.state,
|
306 | 'WAITING_ON_VTTJS',
|
307 | 'loader is waiting for vttjs to be loaded');
|
308 | assert.ok(!parsedCues, 'no cues parsed yet');
|
309 |
|
310 | window.WebVTT = vttjs;
|
311 |
|
312 | loader.subtitlesTrack_.tech_.trigger('vttjsloaded');
|
313 |
|
314 | assert.equal(loader.state, 'READY', 'loader is ready to load next segment');
|
315 | assert.ok(parsedCues, 'parsed cues');
|
316 | });
|
317 |
|
318 | QUnit.test('uses timestampmap from vtt header to set cue and segment timing',
|
319 | function(assert) {
|
320 | const cues = [
|
321 | { startTime: 10, endTime: 12 },
|
322 | { startTime: 14, endTime: 16 },
|
323 | { startTime: 15, endTime: 19 }
|
324 | ];
|
325 | const expectedCueTimes = [
|
326 | { startTime: 14, endTime: 16 },
|
327 | { startTime: 18, endTime: 20 },
|
328 | { startTime: 19, endTime: 23 }
|
329 | ];
|
330 | const expectedSegment = {
|
331 | duration: 10
|
332 | };
|
333 | const expectedPlaylist = {
|
334 | mediaSequence: 100,
|
335 | syncInfo: { mediaSequence: 102, time: 9 }
|
336 | };
|
337 | const mappingObj = {
|
338 | time: 0,
|
339 | mapping: -10
|
340 | };
|
341 | const playlist = { mediaSequence: 100 };
|
342 | const segment = { duration: 10 };
|
343 | const segmentInfo = {
|
344 | timestampmap: { MPEGTS: 1260000, LOCAL: 0 },
|
345 | mediaIndex: 2,
|
346 | cues,
|
347 | segment
|
348 | };
|
349 |
|
350 | loader.updateTimeMapping_(segmentInfo, mappingObj, playlist);
|
351 |
|
352 | assert.deepEqual(cues,
|
353 | expectedCueTimes,
|
354 | 'adjusted cue timing based on timestampmap');
|
355 | assert.deepEqual(segment,
|
356 | expectedSegment,
|
357 | 'set segment start and end based on cue content');
|
358 | assert.deepEqual(playlist,
|
359 | expectedPlaylist,
|
360 | 'set syncInfo for playlist based on learned segment start');
|
361 | });
|
362 |
|
363 | QUnit.test('loader logs vtt.js ParsingErrors and does not trigger an error event',
|
364 | function(assert) {
|
365 | let playlist = playlistWithDuration(40);
|
366 |
|
367 | window.WebVTT.Parser = () => {
|
368 | this.parserCreated = true;
|
369 | return {
|
370 | oncue() {},
|
371 | onparsingerror() {},
|
372 | onflush() {},
|
373 | parse() {
|
374 |
|
375 | this.onparsingerror({ message: 'BAD CUE'});
|
376 | this.oncue({ startTime: 5, endTime: 6});
|
377 | this.onparsingerror({ message: 'BAD --> CUE' });
|
378 | },
|
379 | flush() {}
|
380 | };
|
381 | };
|
382 |
|
383 | loader.playlist(playlist);
|
384 | loader.track(this.track);
|
385 | loader.load();
|
386 |
|
387 | this.clock.tick(1);
|
388 |
|
389 | const vttString = `
|
390 | WEBVTT
|
391 |
|
392 | 00:00:03.000 -> 00:00:05.000
|
393 | <i>BAD CUE</i>
|
394 |
|
395 | 00:00:05.000 --> 00:00:06.000
|
396 | <b>GOOD CUE</b>
|
397 |
|
398 | 00:00:07.000 --> 00:00:10.000
|
399 | <i>BAD --> CUE</i>
|
400 | `;
|
401 |
|
402 |
|
403 | this.requests[0].response =
|
404 | new Uint8Array(vttString.split('').map(char => char.charCodeAt(0)));
|
405 | this.requests.shift().respond(200, null, '');
|
406 |
|
407 | this.clock.tick(1);
|
408 |
|
409 | assert.equal(loader.subtitlesTrack_.cues.length,
|
410 | 1,
|
411 | 'only appended the one good cue');
|
412 | assert.equal(this.env.log.warn.callCount,
|
413 | 2,
|
414 | 'logged two warnings, one for each invalid cue');
|
415 | this.env.log.warn.callCount = 0;
|
416 | });
|
417 |
|
418 | QUnit.test('Cues that overlap segment boundaries',
|
419 | function(assert) {
|
420 | let playlist = playlistWithDuration(20);
|
421 |
|
422 | loader.parseVTTCues_ = (segmentInfo) => {
|
423 | segmentInfo.cues = [{ startTime: 0, endTime: 5}, { startTime: 5, endTime: 15}];
|
424 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
425 | };
|
426 |
|
427 | loader.playlist(playlist);
|
428 | loader.track(this.track);
|
429 | loader.load();
|
430 |
|
431 | this.clock.tick(1);
|
432 |
|
433 | this.requests[0].response = new Uint8Array(10).buffer;
|
434 | this.requests.shift().respond(200, null, '');
|
435 |
|
436 | this.clock.tick(1);
|
437 |
|
438 | assert.equal(this.track.cues.length, 2, 'segment length should be 2');
|
439 |
|
440 | loader.parseVTTCues_ = (segmentInfo) => {
|
441 | segmentInfo.cues = [{ startTime: 5, endTime: 15}, { startTime: 15, endTime: 20}];
|
442 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
443 | };
|
444 |
|
445 | this.clock.tick(1);
|
446 |
|
447 | this.requests[0].response = new Uint8Array(10).buffer;
|
448 | this.requests.shift().respond(200, null, '');
|
449 |
|
450 | this.clock.tick(1);
|
451 |
|
452 | assert.equal(this.track.cues.length, 3, 'segment length should be 3');
|
453 | assert.equal(this.track.cues[0].startTime, 0, 'First cue starttime should be 0');
|
454 | assert.equal(this.track.cues[1].startTime, 5, 'Second cue starttime should be 5');
|
455 | assert.equal(this.track.cues[2].startTime, 15, 'Third cue starttime should be 15');
|
456 | });
|
457 |
|
458 | QUnit.test('loader does not re-request segments that contain no subtitles',
|
459 | function(assert) {
|
460 | let playlist = playlistWithDuration(60);
|
461 |
|
462 | playlist.endList = false;
|
463 |
|
464 | loader.parseVTTCues_ = (segmentInfo) => {
|
465 |
|
466 | segmentInfo.cues = [];
|
467 | };
|
468 |
|
469 | loader.currentTime_ = () => {
|
470 | return 30;
|
471 | };
|
472 |
|
473 | loader.playlist(playlist);
|
474 | loader.track(this.track);
|
475 | loader.load();
|
476 |
|
477 | this.clock.tick(1);
|
478 |
|
479 | assert.equal(loader.pendingSegment_.mediaIndex,
|
480 | 2,
|
481 | 'requesting initial segment guess');
|
482 |
|
483 | this.requests[0].response = new Uint8Array(10).buffer;
|
484 | this.requests.shift().respond(200, null, '');
|
485 |
|
486 | this.clock.tick(1);
|
487 |
|
488 | assert.ok(playlist.segments[2].empty, 'marked empty segment as empty');
|
489 | assert.equal(loader.pendingSegment_.mediaIndex,
|
490 | 3,
|
491 | 'walked forward skipping requesting empty segment');
|
492 | });
|
493 |
|
494 | QUnit.test('loader triggers error event on fatal vtt.js errors', function(assert) {
|
495 | let playlist = playlistWithDuration(40);
|
496 | let errors = 0;
|
497 |
|
498 | loader.parseVTTCues_ = () => {
|
499 | throw new Error('fatal error');
|
500 | };
|
501 | loader.on('error', () => errors++);
|
502 |
|
503 | loader.playlist(playlist);
|
504 | loader.track(this.track);
|
505 | loader.load();
|
506 |
|
507 | assert.equal(errors, 0, 'no error at loader start');
|
508 |
|
509 | this.clock.tick(1);
|
510 |
|
511 |
|
512 | this.requests[0].response = new Uint8Array(10).buffer;
|
513 | this.requests.shift().respond(200, null, '');
|
514 |
|
515 | this.clock.tick(1);
|
516 |
|
517 | assert.equal(errors, 1, 'triggered error when parser emmitts fatal error');
|
518 | assert.ok(loader.paused(), 'loader paused when encountering fatal error');
|
519 | assert.equal(loader.state, 'READY', 'loader reset after error');
|
520 | });
|
521 |
|
522 | QUnit.test('loader triggers error event when vtt.js fails to load', function(assert) {
|
523 | let playlist = playlistWithDuration(40);
|
524 | let errors = 0;
|
525 |
|
526 | delete window.WebVTT;
|
527 | let vttjsCallback = () => {};
|
528 |
|
529 | this.track.tech_ = {
|
530 | one(event, callback) {
|
531 | if (event === 'vttjserror') {
|
532 | vttjsCallback = callback;
|
533 | }
|
534 | },
|
535 | trigger(event) {
|
536 | if (event === 'vttjserror') {
|
537 | vttjsCallback();
|
538 | }
|
539 | },
|
540 | off() {}
|
541 | };
|
542 |
|
543 | loader.on('error', () => errors++);
|
544 |
|
545 | loader.playlist(playlist);
|
546 | loader.track(this.track);
|
547 | loader.load();
|
548 |
|
549 | assert.equal(loader.state, 'READY', 'loader is ready at start');
|
550 | assert.equal(errors, 0, 'no errors yet');
|
551 |
|
552 | this.clock.tick(1);
|
553 |
|
554 | assert.equal(loader.state, 'WAITING', 'loader is waiting on segment request');
|
555 | assert.equal(errors, 0, 'no errors yet');
|
556 |
|
557 | this.requests[0].response = new Uint8Array(10).buffer;
|
558 | this.requests.shift().respond(200, null, '');
|
559 |
|
560 | this.clock.tick(1);
|
561 |
|
562 | assert.equal(loader.state,
|
563 | 'WAITING_ON_VTTJS',
|
564 | 'loader is waiting for vttjs to be loaded');
|
565 | assert.equal(errors, 0, 'no errors yet');
|
566 |
|
567 | loader.subtitlesTrack_.tech_.trigger('vttjserror');
|
568 |
|
569 | assert.equal(loader.state, 'READY', 'loader is reset to ready');
|
570 | assert.ok(loader.paused(), 'loader is paused after error');
|
571 | assert.equal(errors, 1, 'loader triggered error when vtt.js load triggers error');
|
572 | });
|
573 |
|
574 | });
|
575 | });
|