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('loader does not re-request segments that contain no subtitles',
|
419 | function(assert) {
|
420 | let playlist = playlistWithDuration(60);
|
421 |
|
422 | playlist.endList = false;
|
423 |
|
424 | loader.parseVTTCues_ = (segmentInfo) => {
|
425 |
|
426 | segmentInfo.cues = [];
|
427 | };
|
428 |
|
429 | loader.currentTime_ = () => {
|
430 | return 30;
|
431 | };
|
432 |
|
433 | loader.playlist(playlist);
|
434 | loader.track(this.track);
|
435 | loader.load();
|
436 |
|
437 | this.clock.tick(1);
|
438 |
|
439 | assert.equal(loader.pendingSegment_.mediaIndex,
|
440 | 2,
|
441 | 'requesting initial segment guess');
|
442 |
|
443 | this.requests[0].response = new Uint8Array(10).buffer;
|
444 | this.requests.shift().respond(200, null, '');
|
445 |
|
446 | this.clock.tick(1);
|
447 |
|
448 | assert.ok(playlist.segments[2].empty, 'marked empty segment as empty');
|
449 | assert.equal(loader.pendingSegment_.mediaIndex,
|
450 | 3,
|
451 | 'walked forward skipping requesting empty segment');
|
452 | });
|
453 |
|
454 | QUnit.test('loader triggers error event on fatal vtt.js errors', function(assert) {
|
455 | let playlist = playlistWithDuration(40);
|
456 | let errors = 0;
|
457 |
|
458 | loader.parseVTTCues_ = () => {
|
459 | throw new Error('fatal error');
|
460 | };
|
461 | loader.on('error', () => errors++);
|
462 |
|
463 | loader.playlist(playlist);
|
464 | loader.track(this.track);
|
465 | loader.load();
|
466 |
|
467 | assert.equal(errors, 0, 'no error at loader start');
|
468 |
|
469 | this.clock.tick(1);
|
470 |
|
471 |
|
472 | this.requests[0].response = new Uint8Array(10).buffer;
|
473 | this.requests.shift().respond(200, null, '');
|
474 |
|
475 | this.clock.tick(1);
|
476 |
|
477 | assert.equal(errors, 1, 'triggered error when parser emmitts fatal error');
|
478 | assert.ok(loader.paused(), 'loader paused when encountering fatal error');
|
479 | assert.equal(loader.state, 'READY', 'loader reset after error');
|
480 | });
|
481 |
|
482 | QUnit.test('loader triggers error event when vtt.js fails to load', function(assert) {
|
483 | let playlist = playlistWithDuration(40);
|
484 | let errors = 0;
|
485 |
|
486 | delete window.WebVTT;
|
487 | let vttjsCallback = () => {};
|
488 |
|
489 | this.track.tech_ = {
|
490 | one(event, callback) {
|
491 | if (event === 'vttjserror') {
|
492 | vttjsCallback = callback;
|
493 | }
|
494 | },
|
495 | trigger(event) {
|
496 | if (event === 'vttjserror') {
|
497 | vttjsCallback();
|
498 | }
|
499 | },
|
500 | off() {}
|
501 | };
|
502 |
|
503 | loader.on('error', () => errors++);
|
504 |
|
505 | loader.playlist(playlist);
|
506 | loader.track(this.track);
|
507 | loader.load();
|
508 |
|
509 | assert.equal(loader.state, 'READY', 'loader is ready at start');
|
510 | assert.equal(errors, 0, 'no errors yet');
|
511 |
|
512 | this.clock.tick(1);
|
513 |
|
514 | assert.equal(loader.state, 'WAITING', 'loader is waiting on segment request');
|
515 | assert.equal(errors, 0, 'no errors yet');
|
516 |
|
517 | this.requests[0].response = new Uint8Array(10).buffer;
|
518 | this.requests.shift().respond(200, null, '');
|
519 |
|
520 | this.clock.tick(1);
|
521 |
|
522 | assert.equal(loader.state,
|
523 | 'WAITING_ON_VTTJS',
|
524 | 'loader is waiting for vttjs to be loaded');
|
525 | assert.equal(errors, 0, 'no errors yet');
|
526 |
|
527 | loader.subtitlesTrack_.tech_.trigger('vttjserror');
|
528 |
|
529 | assert.equal(loader.state, 'READY', 'loader is reset to ready');
|
530 | assert.ok(loader.paused(), 'loader is paused after error');
|
531 | assert.equal(errors, 1, 'loader triggered error when vtt.js load triggers error');
|
532 | });
|
533 |
|
534 | });
|
535 | });
|