UNPKG

17 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('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 // mock empty segment
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 // state WAITING for segment response
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});