1 | import QUnit from 'qunit';
|
2 | import {
|
3 | default as SegmentLoader,
|
4 | illegalMediaSwitch,
|
5 | safeBackBufferTrimTime
|
6 | } from '../src/segment-loader';
|
7 | import videojs from 'video.js';
|
8 | import mp4probe from 'mux.js/lib/mp4/probe';
|
9 | import {
|
10 | playlistWithDuration,
|
11 | MockTextTrack
|
12 | } from './test-helpers.js';
|
13 | import {
|
14 | LoaderCommonHooks,
|
15 | LoaderCommonSettings,
|
16 | LoaderCommonFactory
|
17 | } from './loader-common.js';
|
18 | import sinon from 'sinon';
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | const ogAddSegmentMetadataCue_ = SegmentLoader.prototype.addSegmentMetadataCue_;
|
24 |
|
25 | SegmentLoader.prototype.addSegmentMetadataCue_ = function() {};
|
26 |
|
27 | QUnit.module('SegmentLoader Isolated Functions');
|
28 |
|
29 | QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert) {
|
30 | let startingMedia = { containsAudio: true, containsVideo: true };
|
31 | let newSegmentMedia = { containsAudio: true, containsVideo: true };
|
32 |
|
33 | assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
34 | 'no error when muxed to muxed');
|
35 |
|
36 | startingMedia = { containsAudio: true, containsVideo: true };
|
37 | newSegmentMedia = { containsAudio: false, containsVideo: false };
|
38 | assert.notOk(illegalMediaSwitch('audio', startingMedia, newSegmentMedia),
|
39 | 'no error when not main loader type');
|
40 |
|
41 | startingMedia = { containsAudio: true, containsVideo: false };
|
42 | newSegmentMedia = { containsAudio: true, containsVideo: false };
|
43 | assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
44 | 'no error when audio only to audio only');
|
45 |
|
46 | startingMedia = { containsAudio: false, containsVideo: true };
|
47 | newSegmentMedia = { containsAudio: false, containsVideo: true };
|
48 | assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
49 | 'no error when video only to video only');
|
50 |
|
51 | startingMedia = { containsAudio: false, containsVideo: true };
|
52 | newSegmentMedia = { containsAudio: true, containsVideo: true };
|
53 | assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
54 | 'no error when video only to muxed');
|
55 |
|
56 | startingMedia = { containsAudio: true, containsVideo: true };
|
57 | newSegmentMedia = { containsAudio: false, containsVideo: false };
|
58 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
59 | 'Neither audio nor video found in segment.',
|
60 | 'error when neither audio nor video');
|
61 |
|
62 | startingMedia = { containsAudio: true, containsVideo: false };
|
63 | newSegmentMedia = { containsAudio: false, containsVideo: false };
|
64 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
65 | 'Neither audio nor video found in segment.',
|
66 | 'error when audio only to neither audio nor video');
|
67 |
|
68 | startingMedia = { containsAudio: false, containsVideo: true };
|
69 | newSegmentMedia = { containsAudio: false, containsVideo: false };
|
70 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
71 | 'Neither audio nor video found in segment.',
|
72 | 'error when video only to neither audio nor video');
|
73 |
|
74 | startingMedia = { containsAudio: true, containsVideo: false };
|
75 | newSegmentMedia = { containsAudio: true, containsVideo: true };
|
76 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
77 | 'Video found in segment when we expected only audio.' +
|
78 | ' We can\'t switch to a stream with video from an audio only stream.' +
|
79 | ' To get rid of this message, please add codec information to the' +
|
80 | ' manifest.',
|
81 | 'error when audio only to muxed');
|
82 |
|
83 | startingMedia = { containsAudio: true, containsVideo: true };
|
84 | newSegmentMedia = { containsAudio: true, containsVideo: false };
|
85 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
86 | 'Only audio found in segment when we expected video.' +
|
87 | ' We can\'t switch to audio only from a stream that had video.' +
|
88 | ' To get rid of this message, please add codec information to the' +
|
89 | ' manifest.',
|
90 | 'error when muxed to audio only');
|
91 |
|
92 | startingMedia = { containsAudio: true, containsVideo: false };
|
93 | newSegmentMedia = { containsAudio: false, containsVideo: true };
|
94 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
95 | 'Video found in segment when we expected only audio.' +
|
96 | ' We can\'t switch to a stream with video from an audio only stream.' +
|
97 | ' To get rid of this message, please add codec information to the' +
|
98 | ' manifest.',
|
99 | 'error when audio only to video only');
|
100 |
|
101 | startingMedia = { containsAudio: false, containsVideo: true };
|
102 | newSegmentMedia = { containsAudio: true, containsVideo: false };
|
103 | assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
|
104 | 'Only audio found in segment when we expected video.' +
|
105 | ' We can\'t switch to audio only from a stream that had video.' +
|
106 | ' To get rid of this message, please add codec information to the' +
|
107 | ' manifest.',
|
108 | 'error when video only to audio only');
|
109 | });
|
110 |
|
111 | QUnit.test('safeBackBufferTrimTime determines correct safe removeToTime',
|
112 | function(assert) {
|
113 | let seekable = videojs.createTimeRanges([[75, 120]]);
|
114 | let targetDuration = 10;
|
115 | let currentTime = 70;
|
116 |
|
117 | assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 40,
|
118 | 'uses 30s before current time if currentTime is before seekable start');
|
119 |
|
120 | currentTime = 110;
|
121 |
|
122 | assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 75,
|
123 | 'uses seekable start if currentTime is after seekable start');
|
124 |
|
125 | currentTime = 80;
|
126 |
|
127 | assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 70,
|
128 | 'uses target duration before currentTime if currentTime is after seekable but' +
|
129 | 'within target duration');
|
130 | });
|
131 |
|
132 | QUnit.module('SegmentLoader', function(hooks) {
|
133 | hooks.beforeEach(LoaderCommonHooks.beforeEach);
|
134 | hooks.afterEach(LoaderCommonHooks.afterEach);
|
135 |
|
136 | LoaderCommonFactory(SegmentLoader,
|
137 | { loaderType: 'main' },
|
138 | (loader) => loader.mimeType('video/mp2t'));
|
139 |
|
140 |
|
141 | QUnit.module('Loader Main', function(nestedHooks) {
|
142 | let loader;
|
143 |
|
144 | nestedHooks.beforeEach(function(assert) {
|
145 | this.segmentMetadataTrack = new MockTextTrack();
|
146 | this.startTime = sinon.stub(mp4probe, 'startTime');
|
147 | this.mimeType = 'video/mp2t';
|
148 |
|
149 | loader = new SegmentLoader(LoaderCommonSettings.call(this, {
|
150 | loaderType: 'main',
|
151 | segmentMetadataTrack: this.segmentMetadataTrack
|
152 | }), {});
|
153 |
|
154 |
|
155 | this.updateend = function() {
|
156 | if (loader.mediaSource_) {
|
157 | loader.mediaSource_.sourceBuffers[0].trigger('updateend');
|
158 | }
|
159 | };
|
160 | });
|
161 |
|
162 | nestedHooks.afterEach(function(assert) {
|
163 | this.startTime.restore();
|
164 | });
|
165 |
|
166 | QUnit.test(`load waits until a playlist and mime type are specified to proceed`,
|
167 | function(assert) {
|
168 | loader.load();
|
169 |
|
170 | assert.equal(loader.state, 'INIT', 'waiting in init');
|
171 | assert.equal(loader.paused(), false, 'not paused');
|
172 |
|
173 | loader.playlist(playlistWithDuration(10));
|
174 | assert.equal(this.requests.length, 0, 'have not made a request yet');
|
175 | loader.mimeType(this.mimeType);
|
176 | this.clock.tick(1);
|
177 |
|
178 | assert.equal(this.requests.length, 1, 'made a request');
|
179 | assert.equal(loader.state, 'WAITING', 'transitioned states');
|
180 | });
|
181 |
|
182 | QUnit.test(`calling mime type and load begins buffering`, function(assert) {
|
183 | assert.equal(loader.state, 'INIT', 'starts in the init state');
|
184 | loader.playlist(playlistWithDuration(10));
|
185 | assert.equal(loader.state, 'INIT', 'starts in the init state');
|
186 | assert.ok(loader.paused(), 'starts paused');
|
187 |
|
188 | loader.mimeType(this.mimeType);
|
189 | assert.equal(loader.state, 'INIT', 'still in the init state');
|
190 | loader.load();
|
191 | this.clock.tick(1);
|
192 |
|
193 | assert.equal(loader.state, 'WAITING', 'moves to the ready state');
|
194 | assert.ok(!loader.paused(), 'loading is not paused');
|
195 | assert.equal(this.requests.length, 1, 'requested a segment');
|
196 | });
|
197 |
|
198 | QUnit.test('only appends one segment at a time', function(assert) {
|
199 | loader.playlist(playlistWithDuration(10));
|
200 | loader.mimeType(this.mimeType);
|
201 | loader.load();
|
202 | this.clock.tick(1);
|
203 |
|
204 |
|
205 | this.clock.tick(100);
|
206 | this.requests[0].response = new Uint8Array(10).buffer;
|
207 | this.requests.shift().respond(200, null, '');
|
208 |
|
209 |
|
210 | this.clock.tick(20 * 1000);
|
211 |
|
212 | assert.equal(this.mediaSource.sourceBuffers[0].updates_.filter(
|
213 | update => update.append).length, 1, 'only one append');
|
214 | assert.equal(this.requests.length, 0, 'only made one request');
|
215 |
|
216 |
|
217 | assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
|
218 | assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
|
219 | assert.equal(loader.mediaRequests, 1, '1 request');
|
220 | });
|
221 |
|
222 | QUnit.test('updates timestamps when segments do not start at zero', function(assert) {
|
223 | let playlist = playlistWithDuration(10);
|
224 |
|
225 | playlist.segments.forEach((segment) => {
|
226 | segment.map = {
|
227 | resolvedUri: 'init.mp4',
|
228 | byterange: { length: Infinity, offset: 0 }
|
229 | };
|
230 | });
|
231 | loader.playlist(playlist);
|
232 | loader.mimeType(this.mimeType);
|
233 | loader.load();
|
234 |
|
235 | this.startTime.returns(11);
|
236 |
|
237 | this.clock.tick(100);
|
238 |
|
239 | this.requests[0].response = new Uint8Array(10).buffer;
|
240 | this.requests.shift().respond(200, null, '');
|
241 |
|
242 | this.requests[0].response = new Uint8Array(10).buffer;
|
243 | this.requests.shift().respond(200, null, '');
|
244 |
|
245 | assert.equal(loader.sourceUpdater_.timestampOffset(), -11, 'set timestampOffset');
|
246 | assert.equal(playlist.segments[0].start,
|
247 | 0,
|
248 | 'segment start time not shifted by mp4 start time');
|
249 | assert.equal(playlist.segments[0].end,
|
250 | 10,
|
251 | 'segment end time not shifted by mp4 start time');
|
252 | });
|
253 |
|
254 | QUnit.test('triggers syncinfoupdate before attempting a resync', function(assert) {
|
255 | let syncInfoUpdates = 0;
|
256 |
|
257 | loader.playlist(playlistWithDuration(20));
|
258 | loader.mimeType(this.mimeType);
|
259 | loader.load();
|
260 | this.clock.tick(1);
|
261 |
|
262 | this.seekable = videojs.createTimeRanges([[0, 10]]);
|
263 | this.syncController.probeSegmentInfo = (segmentInfo) => {
|
264 | let segment = segmentInfo.segment;
|
265 |
|
266 | segment.end = 10;
|
267 | };
|
268 | loader.on('syncinfoupdate', () => {
|
269 | syncInfoUpdates++;
|
270 |
|
271 | this.seekable = videojs.createTimeRanges([[200, 210]]);
|
272 |
|
273 | this.currentTime = 210;
|
274 | });
|
275 |
|
276 | this.requests[0].response = new Uint8Array(10).buffer;
|
277 | this.requests.shift().respond(200, null, '');
|
278 | this.updateend();
|
279 | this.clock.tick(1);
|
280 |
|
281 | assert.equal(loader.mediaIndex, null, 'mediaIndex reset by seek to seekable');
|
282 | assert.equal(syncInfoUpdates, 1, 'syncinfoupdate was triggered');
|
283 | });
|
284 |
|
285 | QUnit.test('abort does not cancel segment processing in progress', function(assert) {
|
286 | loader.playlist(playlistWithDuration(20));
|
287 | loader.mimeType(this.mimeType);
|
288 | loader.load();
|
289 | this.clock.tick(1);
|
290 |
|
291 | this.requests[0].response = new Uint8Array(10).buffer;
|
292 | this.requests.shift().respond(200, null, '');
|
293 |
|
294 | loader.abort();
|
295 | this.clock.tick(1);
|
296 |
|
297 | assert.equal(loader.state, 'APPENDING', 'still appending');
|
298 |
|
299 |
|
300 | assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
|
301 | assert.equal(loader.mediaRequests, 1, '1 request');
|
302 | });
|
303 |
|
304 | QUnit.test('sets the timestampOffset on timeline change', function(assert) {
|
305 | let playlist = playlistWithDuration(40);
|
306 | let buffered = videojs.createTimeRanges();
|
307 | let hlsTimestampOffsetEvents = 0;
|
308 |
|
309 | loader.on('timestampoffset', () => {
|
310 | hlsTimestampOffsetEvents++;
|
311 | });
|
312 |
|
313 | loader.buffered_ = () => buffered;
|
314 |
|
315 | playlist.discontinuityStarts = [1];
|
316 | playlist.segments[1].timeline = 1;
|
317 | loader.playlist(playlist);
|
318 | loader.mimeType(this.mimeType);
|
319 | loader.load();
|
320 | this.clock.tick(1);
|
321 |
|
322 |
|
323 | this.requests[0].response = new Uint8Array(10).buffer;
|
324 | this.requests.shift().respond(200, null, '');
|
325 | buffered = videojs.createTimeRanges([[0, 10]]);
|
326 | this.updateend();
|
327 | this.clock.tick(1);
|
328 |
|
329 | assert.equal(hlsTimestampOffsetEvents, 0,
|
330 | 'no hls-timestamp-offset event was fired');
|
331 |
|
332 | this.requests[0].response = new Uint8Array(10).buffer;
|
333 | this.requests.shift().respond(200, null, '');
|
334 | assert.equal(loader.mediaSource_.sourceBuffers[0].timestampOffset,
|
335 | 10,
|
336 | 'set timestampOffset');
|
337 |
|
338 |
|
339 | assert.equal(loader.mediaBytesTransferred, 20, '20 bytes');
|
340 | assert.equal(loader.mediaRequests, 2, '2 requests');
|
341 | assert.equal(hlsTimestampOffsetEvents, 1,
|
342 | 'an hls-timestamp-offset event was fired');
|
343 | });
|
344 |
|
345 | QUnit.test('tracks segment end times as they are buffered', function(assert) {
|
346 | let playlist = playlistWithDuration(20);
|
347 |
|
348 | loader.syncController_.probeTsSegment_ = function(segmentInfo) {
|
349 | return { start: 0, end: 9.5 };
|
350 | };
|
351 |
|
352 | loader.playlist(playlist);
|
353 | loader.mimeType(this.mimeType);
|
354 | loader.load();
|
355 | this.clock.tick(1);
|
356 |
|
357 | this.requests[0].response = new Uint8Array(10).buffer;
|
358 | this.requests.shift().respond(200, null, '');
|
359 |
|
360 | this.updateend();
|
361 | this.clock.tick(1);
|
362 |
|
363 | assert.equal(playlist.segments[0].end, 9.5, 'updated duration');
|
364 |
|
365 |
|
366 | assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
|
367 | assert.equal(loader.mediaRequests, 1, '1 request');
|
368 | });
|
369 |
|
370 | QUnit.test('loader triggers segmenttimemapping before appending segment',
|
371 | function(assert) {
|
372 | let playlist = playlistWithDuration(20);
|
373 | let segmenttimemappings = 0;
|
374 | let timingInfo = { hasMapping: false };
|
375 |
|
376 | this.syncController.probeSegmentInfo = () => timingInfo;
|
377 |
|
378 | loader.on('segmenttimemapping', function() {
|
379 | segmenttimemappings++;
|
380 | });
|
381 |
|
382 | loader.playlist(playlist);
|
383 | loader.mimeType(this.mimeType);
|
384 | loader.load();
|
385 | this.clock.tick(1);
|
386 |
|
387 | assert.equal(segmenttimemappings, 0, 'no events before segment downloaded');
|
388 |
|
389 |
|
390 | this.requests[0].response = new Uint8Array(10).buffer;
|
391 | this.requests.shift().respond(200, null, '');
|
392 |
|
393 | assert.equal(segmenttimemappings, 0,
|
394 | 'did not trigger segmenttimemappings with unsuccessful probe');
|
395 |
|
396 | this.updateend();
|
397 | this.clock.tick(1);
|
398 |
|
399 | assert.equal(segmenttimemappings, 0, 'no events before segment downloaded');
|
400 |
|
401 | timingInfo.hasMapping = true;
|
402 | this.syncController.timelines[0] = { mapping: 0 };
|
403 |
|
404 |
|
405 | this.requests[0].response = new Uint8Array(10).buffer;
|
406 | this.requests.shift().respond(200, null, '');
|
407 |
|
408 | assert.equal(segmenttimemappings, 1,
|
409 | 'triggered segmenttimemappings with successful probe');
|
410 | });
|
411 |
|
412 | QUnit.test('adds cues with segment information to the segment-metadata track ' +
|
413 | 'as they are buffered',
|
414 | function(assert) {
|
415 | const track = loader.segmentMetadataTrack_;
|
416 | let playlist = playlistWithDuration(50);
|
417 | let probeResponse;
|
418 | let expectedCue;
|
419 |
|
420 | loader.addSegmentMetadataCue_ = ogAddSegmentMetadataCue_;
|
421 | loader.syncController_.probeTsSegment_ = function(segmentInfo) {
|
422 | return probeResponse;
|
423 | };
|
424 |
|
425 | loader.playlist(playlist);
|
426 | loader.mimeType(this.mimeType);
|
427 | loader.load();
|
428 | this.clock.tick(1);
|
429 |
|
430 | assert.ok(!track.cues.length,
|
431 | 'segment-metadata track empty when no segments appended');
|
432 |
|
433 |
|
434 | probeResponse = { start: 0, end: 9.5 };
|
435 | this.requests[0].response = new Uint8Array(10).buffer;
|
436 | this.requests.shift().respond(200, null, '');
|
437 | this.updateend();
|
438 | this.clock.tick(1);
|
439 | expectedCue = {
|
440 | uri: '0.ts',
|
441 | timeline: 0,
|
442 | playlist: 'playlist.m3u8',
|
443 | start: 0,
|
444 | end: 9.5
|
445 | };
|
446 |
|
447 | assert.equal(track.cues.length, 1, 'one cue added for segment');
|
448 | assert.deepEqual(track.cues[0].value, expectedCue,
|
449 | 'added correct segment info to cue');
|
450 |
|
451 | probeResponse = { start: 9.56, end: 19.2 };
|
452 | this.requests[0].response = new Uint8Array(10).buffer;
|
453 | this.requests.shift().respond(200, null, '');
|
454 | this.updateend();
|
455 | this.clock.tick(1);
|
456 | expectedCue = {
|
457 | uri: '1.ts',
|
458 | timeline: 0,
|
459 | playlist: 'playlist.m3u8',
|
460 | start: 9.56,
|
461 | end: 19.2
|
462 | };
|
463 |
|
464 | assert.equal(track.cues.length, 2, 'one cue added for segment');
|
465 | assert.deepEqual(track.cues[1].value, expectedCue,
|
466 | 'added correct segment info to cue');
|
467 |
|
468 | probeResponse = { start: 19.24, end: 28.99 };
|
469 | this.requests[0].response = new Uint8Array(10).buffer;
|
470 | this.requests.shift().respond(200, null, '');
|
471 | this.updateend();
|
472 | this.clock.tick(1);
|
473 | expectedCue = {
|
474 | uri: '2.ts',
|
475 | timeline: 0,
|
476 | playlist: 'playlist.m3u8',
|
477 | start: 19.24,
|
478 | end: 28.99
|
479 | };
|
480 |
|
481 | assert.equal(track.cues.length, 3, 'one cue added for segment');
|
482 | assert.deepEqual(track.cues[2].value, expectedCue,
|
483 | 'added correct segment info to cue');
|
484 |
|
485 |
|
486 |
|
487 | probeResponse = { start: 19.21, end: 28.98 };
|
488 | this.requests[0].response = new Uint8Array(10).buffer;
|
489 | this.requests.shift().respond(200, null, '');
|
490 | this.updateend();
|
491 | this.clock.tick(1);
|
492 | expectedCue = {
|
493 | uri: '3.ts',
|
494 | timeline: 0,
|
495 | playlist: 'playlist.m3u8',
|
496 | start: 19.21,
|
497 | end: 28.98
|
498 | };
|
499 |
|
500 | assert.equal(track.cues.length, 3, 'overlapped cue removed, new one added');
|
501 | assert.deepEqual(track.cues[2].value, expectedCue,
|
502 | 'added correct segment info to cue');
|
503 |
|
504 |
|
505 | probeResponse = { start: 30, end: void 0 };
|
506 | this.requests[0].response = new Uint8Array(10).buffer;
|
507 | this.requests.shift().respond(200, null, '');
|
508 | this.updateend();
|
509 | this.clock.tick(1);
|
510 |
|
511 | assert.equal(track.cues.length, 3, 'no cue added');
|
512 |
|
513 |
|
514 | assert.equal(loader.mediaBytesTransferred, 50, '50 bytes');
|
515 | assert.equal(loader.mediaRequests, 5, '5 requests');
|
516 | });
|
517 |
|
518 | QUnit.test('fires ended at the end of a playlist', function(assert) {
|
519 | let endOfStreams = 0;
|
520 | let buffered = videojs.createTimeRanges();
|
521 |
|
522 | loader.buffered_ = () => buffered;
|
523 |
|
524 | loader.playlist(playlistWithDuration(10));
|
525 | loader.mimeType(this.mimeType);
|
526 | loader.load();
|
527 | this.clock.tick(1);
|
528 |
|
529 | loader.mediaSource_ = {
|
530 | readyState: 'open',
|
531 | sourceBuffers: this.mediaSource.sourceBuffers
|
532 | };
|
533 |
|
534 | loader.on('ended', () => endOfStreams++);
|
535 |
|
536 | this.requests[0].response = new Uint8Array(10).buffer;
|
537 | this.requests.shift().respond(200, null, '');
|
538 | buffered = videojs.createTimeRanges([[0, 10]]);
|
539 | this.updateend();
|
540 | this.clock.tick(1);
|
541 |
|
542 | assert.equal(endOfStreams, 1, 'triggered ended');
|
543 |
|
544 |
|
545 | assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
|
546 | assert.equal(loader.mediaRequests, 1, '1 request');
|
547 | });
|
548 |
|
549 | QUnit.test('endOfStream happens even after a rendition switch', function(assert) {
|
550 | let endOfStreams = 0;
|
551 | let bandwidthupdates = 0;
|
552 | let buffered = videojs.createTimeRanges();
|
553 |
|
554 | loader.buffered_ = () => buffered;
|
555 |
|
556 | loader.playlist(playlistWithDuration(20));
|
557 | loader.mimeType(this.mimeType);
|
558 | loader.load();
|
559 | this.clock.tick(1);
|
560 |
|
561 | loader.mediaSource_ = {
|
562 | readyState: 'open',
|
563 | sourceBuffers: this.mediaSource.sourceBuffers
|
564 | };
|
565 |
|
566 | loader.on('ended', () => endOfStreams++);
|
567 |
|
568 | loader.on('bandwidthupdate', () => {
|
569 | bandwidthupdates++;
|
570 |
|
571 | loader.resetEverything();
|
572 | });
|
573 |
|
574 | this.requests[0].response = new Uint8Array(10).buffer;
|
575 | this.requests.shift().respond(200, null, '');
|
576 | buffered = videojs.createTimeRanges([[0, 10]]);
|
577 | this.updateend();
|
578 | this.clock.tick(10);
|
579 |
|
580 | this.requests[0].response = new Uint8Array(10).buffer;
|
581 | this.requests.shift().respond(200, null, '');
|
582 | buffered = videojs.createTimeRanges([[0, 10]]);
|
583 | this.updateend();
|
584 |
|
585 | assert.equal(bandwidthupdates, 1, 'triggered bandwidthupdate');
|
586 | assert.equal(endOfStreams, 1, 'triggered ended');
|
587 | });
|
588 |
|
589 | QUnit.test('live playlists do not trigger ended', function(assert) {
|
590 | let endOfStreams = 0;
|
591 | let playlist;
|
592 | let buffered = videojs.createTimeRanges();
|
593 |
|
594 | loader.buffered_ = () => buffered;
|
595 |
|
596 | playlist = playlistWithDuration(10);
|
597 | playlist.endList = false;
|
598 | loader.playlist(playlist);
|
599 | loader.mimeType(this.mimeType);
|
600 | loader.load();
|
601 | this.clock.tick(1);
|
602 |
|
603 | loader.mediaSource_ = {
|
604 | readyState: 'open',
|
605 | sourceBuffers: this.mediaSource.sourceBuffers
|
606 | };
|
607 |
|
608 | loader.on('ended', () => endOfStreams++);
|
609 |
|
610 | this.requests[0].response = new Uint8Array(10).buffer;
|
611 | this.requests.shift().respond(200, null, '');
|
612 | buffered = videojs.createTimeRanges([[0, 10]]);
|
613 | this.updateend();
|
614 | this.clock.tick(1);
|
615 |
|
616 | assert.equal(endOfStreams, 0, 'did not trigger ended');
|
617 |
|
618 |
|
619 | assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
|
620 | assert.equal(loader.mediaRequests, 1, '1 request');
|
621 | });
|
622 |
|
623 | QUnit.test('saves segment info to new segment after playlist refresh',
|
624 | function(assert) {
|
625 | let playlist = playlistWithDuration(40);
|
626 | let buffered = videojs.createTimeRanges();
|
627 |
|
628 | loader.buffered_ = () => buffered;
|
629 |
|
630 | playlist.endList = false;
|
631 |
|
632 | loader.playlist(playlist);
|
633 | loader.mimeType(this.mimeType);
|
634 | loader.load();
|
635 | this.clock.tick(1);
|
636 |
|
637 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
638 | assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
|
639 | assert.equal(loader.pendingSegment_.segment.uri,
|
640 | '0.ts',
|
641 | 'correct segment reference');
|
642 |
|
643 |
|
644 | this.requests[0].response = new Uint8Array(10).buffer;
|
645 | this.requests.shift().respond(200, null, '');
|
646 | buffered = videojs.createTimeRanges([[0, 10]]);
|
647 | this.updateend();
|
648 | this.clock.tick(1);
|
649 |
|
650 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
651 | assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
|
652 | assert.equal(loader.pendingSegment_.segment.uri,
|
653 | '1.ts',
|
654 | 'correct segment reference');
|
655 |
|
656 |
|
657 | let playlistUpdated = playlistWithDuration(40);
|
658 |
|
659 | playlistUpdated.segments.shift();
|
660 | playlistUpdated.mediaSequence++;
|
661 | loader.playlist(playlistUpdated);
|
662 |
|
663 | assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
|
664 | assert.equal(loader.pendingSegment_.segment.uri,
|
665 | '1.ts',
|
666 | 'correct segment reference');
|
667 |
|
668 |
|
669 |
|
670 | loader.syncController_.probeSegmentInfo = (segmentInfo) => {
|
671 | segmentInfo.segment.start = 10;
|
672 | segmentInfo.segment.end = 20;
|
673 | };
|
674 |
|
675 | this.requests[0].response = new Uint8Array(10).buffer;
|
676 | this.requests.shift().respond(200, null, '');
|
677 |
|
678 | assert.equal(playlistUpdated.segments[0].start,
|
679 | 10,
|
680 | 'set start on segment of new playlist');
|
681 | assert.equal(playlistUpdated.segments[0].end,
|
682 | 20,
|
683 | 'set end on segment of new playlist');
|
684 | assert.ok(!playlist.segments[1].start,
|
685 | 'did not set start on segment of old playlist');
|
686 | assert.ok(!playlist.segments[1].end, 'did not set end on segment of old playlist');
|
687 | });
|
688 |
|
689 | QUnit.test(
|
690 | 'saves segment info to old segment after playlist refresh if segment fell off',
|
691 | function(assert) {
|
692 | let playlist = playlistWithDuration(40);
|
693 | let buffered = videojs.createTimeRanges();
|
694 |
|
695 | loader.buffered_ = () => buffered;
|
696 |
|
697 | playlist.endList = false;
|
698 |
|
699 | loader.playlist(playlist);
|
700 | loader.mimeType(this.mimeType);
|
701 | loader.load();
|
702 | this.clock.tick(1);
|
703 |
|
704 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
705 | assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
|
706 | assert.equal(loader.pendingSegment_.segment.uri,
|
707 | '0.ts',
|
708 | 'correct segment reference');
|
709 |
|
710 |
|
711 | this.requests[0].response = new Uint8Array(10).buffer;
|
712 | this.requests.shift().respond(200, null, '');
|
713 | buffered = videojs.createTimeRanges([[0, 10]]);
|
714 | this.updateend();
|
715 | this.clock.tick(1);
|
716 |
|
717 | assert.equal(loader.state, 'WAITING', 'in waiting state');
|
718 | assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
|
719 | assert.equal(loader.pendingSegment_.segment.uri,
|
720 | '1.ts',
|
721 | 'correct segment reference');
|
722 |
|
723 |
|
724 | let playlistUpdated = playlistWithDuration(40);
|
725 |
|
726 | playlistUpdated.segments.shift();
|
727 | playlistUpdated.segments.shift();
|
728 | playlistUpdated.mediaSequence += 2;
|
729 | loader.playlist(playlistUpdated);
|
730 |
|
731 | assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
|
732 | assert.equal(loader.pendingSegment_.segment.uri,
|
733 | '1.ts',
|
734 | 'correct segment reference');
|
735 |
|
736 |
|
737 |
|
738 | loader.syncController_.probeSegmentInfo = (segmentInfo) => {
|
739 | segmentInfo.segment.start = 10;
|
740 | segmentInfo.segment.end = 20;
|
741 | };
|
742 |
|
743 | this.requests[0].response = new Uint8Array(10).buffer;
|
744 | this.requests.shift().respond(200, null, '');
|
745 |
|
746 | assert.equal(playlist.segments[1].start,
|
747 | 10,
|
748 | 'set start on segment of old playlist');
|
749 | assert.equal(playlist.segments[1].end,
|
750 | 20,
|
751 | 'set end on segment of old playlist');
|
752 | assert.ok(!playlistUpdated.segments[0].start,
|
753 | 'no start info for first segment of new playlist');
|
754 | assert.ok(!playlistUpdated.segments[0].end,
|
755 | 'no end info for first segment of new playlist');
|
756 | });
|
757 |
|
758 | QUnit.test('errors when trying to switch from audio and video to audio only',
|
759 | function(assert) {
|
760 | const playlist = playlistWithDuration(40);
|
761 | const errors = [];
|
762 |
|
763 | loader.on('error', () => errors.push(loader.error()));
|
764 |
|
765 | loader.playlist(playlist);
|
766 | loader.mimeType(this.mimeType);
|
767 | loader.load();
|
768 | this.clock.tick(1);
|
769 | loader.syncController_.probeSegmentInfo = () => {
|
770 | return {
|
771 | start: 0,
|
772 | end: 10,
|
773 | containsAudio: true,
|
774 | containsVideo: true
|
775 | };
|
776 | };
|
777 | this.requests[0].response = new Uint8Array(10).buffer;
|
778 | this.requests.shift().respond(200, null, '');
|
779 | loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
|
780 | this.updateend();
|
781 | this.clock.tick(1);
|
782 |
|
783 | assert.equal(errors.length, 0, 'no errors');
|
784 |
|
785 | loader.syncController_.probeSegmentInfo = () => {
|
786 | return {
|
787 | start: 10,
|
788 | end: 20,
|
789 | containsAudio: true,
|
790 | containsVideo: false
|
791 | };
|
792 | };
|
793 | this.requests[0].response = new Uint8Array(10).buffer;
|
794 | this.requests.shift().respond(200, null, '');
|
795 |
|
796 | assert.equal(errors.length, 1, 'one error');
|
797 | assert.equal(errors[0].message,
|
798 | 'Only audio found in segment when we expected video.' +
|
799 | ' We can\'t switch to audio only from a stream that had video.' +
|
800 | ' To get rid of this message, please add codec information to the' +
|
801 | ' manifest.',
|
802 | 'correct error message');
|
803 | });
|
804 |
|
805 | QUnit.test('errors when trying to switch from audio only to audio and video',
|
806 | function(assert) {
|
807 | const playlist = playlistWithDuration(40);
|
808 | const errors = [];
|
809 |
|
810 | loader.on('error', () => errors.push(loader.error()));
|
811 |
|
812 | loader.playlist(playlist);
|
813 | loader.mimeType(this.mimeType);
|
814 | loader.load();
|
815 | this.clock.tick(1);
|
816 | loader.syncController_.probeSegmentInfo = () => {
|
817 | return {
|
818 | start: 0,
|
819 | end: 10,
|
820 | containsAudio: true,
|
821 | containsVideo: false
|
822 | };
|
823 | };
|
824 | this.requests[0].response = new Uint8Array(10).buffer;
|
825 | this.requests.shift().respond(200, null, '');
|
826 | loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
|
827 | this.updateend();
|
828 | this.clock.tick(1);
|
829 |
|
830 | assert.equal(errors.length, 0, 'no errors');
|
831 |
|
832 | loader.syncController_.probeSegmentInfo = () => {
|
833 | return {
|
834 | start: 10,
|
835 | end: 20,
|
836 | containsAudio: true,
|
837 | containsVideo: true
|
838 | };
|
839 | };
|
840 | this.requests[0].response = new Uint8Array(10).buffer;
|
841 | this.requests.shift().respond(200, null, '');
|
842 |
|
843 | assert.equal(errors.length, 1, 'one error');
|
844 | assert.equal(errors[0].message,
|
845 | 'Video found in segment when we expected only audio.' +
|
846 | ' We can\'t switch to a stream with video from an audio only stream.' +
|
847 | ' To get rid of this message, please add codec information to the' +
|
848 | ' manifest.',
|
849 | 'correct error message');
|
850 | });
|
851 |
|
852 | QUnit.test('no error when not switching from audio and video', function(assert) {
|
853 | const playlist = playlistWithDuration(40);
|
854 | const errors = [];
|
855 |
|
856 | loader.on('error', () => errors.push(loader.error()));
|
857 |
|
858 | loader.playlist(playlist);
|
859 | loader.mimeType(this.mimeType);
|
860 | loader.load();
|
861 | this.clock.tick(1);
|
862 | loader.syncController_.probeSegmentInfo = () => {
|
863 | return {
|
864 | start: 0,
|
865 | end: 10,
|
866 | containsAudio: true,
|
867 | containsVideo: true
|
868 | };
|
869 | };
|
870 | this.requests[0].response = new Uint8Array(10).buffer;
|
871 | this.requests.shift().respond(200, null, '');
|
872 | loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
|
873 | this.updateend();
|
874 | this.clock.tick(1);
|
875 |
|
876 | assert.equal(errors.length, 0, 'no errors');
|
877 |
|
878 | loader.syncController_.probeSegmentInfo = () => {
|
879 | return {
|
880 | start: 10,
|
881 | end: 20,
|
882 | containsAudio: true,
|
883 | containsVideo: true
|
884 | };
|
885 | };
|
886 | this.requests[0].response = new Uint8Array(10).buffer;
|
887 | this.requests.shift().respond(200, null, '');
|
888 |
|
889 | assert.equal(errors.length, 0, 'no errors');
|
890 | });
|
891 | });
|
892 | });
|