UNPKG

18.4 kBJavaScriptView Raw
1import QUnit from 'qunit';
2import VTTSegmentLoader from '../src/vtt-segment-loader';
3import videojs from 'video.js';
4import {
5 playlistWithDuration as oldPlaylistWithDuration,
6 MockTextTrack
7} from './test-helpers.js';
8import {
9 LoaderCommonHooks,
10 LoaderCommonSettings,
11 LoaderCommonFactory
12} from './loader-common.js';
13
14const oldVTT = window.WebVTT;
15
16const playlistWithDuration = function(time, conf) {
17 return oldPlaylistWithDuration(time, videojs.mergeOptions({ extension: '.vtt' }, conf));
18};
19
20QUnit.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 // mock an initial timeline sync point on the SyncController
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 // Tests specific to the vtt loader go in this module
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 // wrap up the first request to set mediaIndex and start normal live streaming
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 // playlist updated during waiting
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 // mock parseVttCues_ to respond empty cue array
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 // wrap up the first request to set mediaIndex and start normal live streaming
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 // playlist updated during waiting
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 // mock parseVttCues_ to respond empty cue array
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 // simulate the main segment loader finding timeline info for the new timeline
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 // MOCK parsing the cues below
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 // state WAITING for segment response
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 // mock empty segment
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 // state WAITING for segment response
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});