You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1428 lines
40 KiB

import QUnit from 'qunit';
import videojs from 'video.js';
import sinon from 'sinon';
import {mediaSegmentRequest, REQUEST_ERRORS} from '../src/media-segment-request';
import xhrFactory from '../src/xhr';
import {
useFakeEnvironment,
standardXHRResponse,
downloadProgress
} from './test-helpers';
import TransmuxWorker from 'worker!../src/transmuxer-worker.worker.js';
import Decrypter from 'worker!../src/decrypter-worker.worker.js';
import {
aacWithoutId3 as aacWithoutId3Segment,
aacWithId3 as aacWithId3Segment,
ac3WithId3 as ac3WithId3Segment,
ac3WithoutId3 as ac3WithoutId3Segment,
video as videoSegment,
audio as audioSegment,
mp4Video,
mp4VideoInit,
muxed as muxedSegment,
muxedString as muxedSegmentString,
caption as captionSegment,
captionString as captionSegmentString,
id3String as id3SegmentString,
id3 as id3Segment,
webmVideo,
webmVideoInit
} from 'create-test-data!segments';
// needed for plugin registration
import '../src/videojs-http-streaming';
const sharedHooks = {
beforeEach(assert) {
this.env = useFakeEnvironment(assert);
this.clock = this.env.clock;
this.requests = this.env.requests;
this.xhr = xhrFactory();
this.realDecrypter = new Decrypter();
this.mockDecrypter = {
listeners: [],
postMessage(message) {
const newMessage = Object.create(message);
newMessage.decrypted = message.encrypted;
this.listeners.forEach((fn)=>fn({
data: newMessage
}));
},
addEventListener(event, listener) {
this.listeners.push(listener);
},
removeEventListener(event, listener) {
this.listeners = this.listeners.filter((fn)=>fn !== listener);
}
};
this.xhrOptions = {
timeout: 1000
};
this.noop = () => {};
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
// Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
// we have to use clock.tick to get the expected side effects of
// SegmentLoader#handleAppendsDone_
this.clock.tick(1);
};
this.createTransmuxer = (isPartial) => {
const transmuxer = new TransmuxWorker();
transmuxer.postMessage({
action: 'init',
options: {
remux: false,
keepOriginalTimestamps: true,
handlePartialData: isPartial
}
});
return transmuxer;
};
},
afterEach(assert) {
this.realDecrypter.terminate();
this.env.restore();
if (this.transmuxer) {
this.transmuxer.terminate();
}
}
};
QUnit.module('Media Segment Request - make it to transmuxer', {
beforeEach(assert) {
sharedHooks.beforeEach.call(this, assert);
this.calls = {};
this.options = {
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {},
handlePartialData: false
};
[
'progress',
'trackInfo',
'timingInfo',
'id3',
'captions',
'data',
'videoSegmentTimingInfo'
].forEach((name) => {
this.calls[name] = 0;
this.options[`${name}Fn`] = () => this.calls[name]++;
});
},
afterEach(assert) {
this.transmuxer = this.options.segment.transmuxer;
sharedHooks.afterEach.call(this, assert);
}
});
QUnit.test('ac3 without id3 segments will not make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.ac3';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 0,
trackInfo: 1,
progress: 1,
timingInfo: 0,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ac3', 'segment-request');
this.standardXHRResponse(this.requests[0], ac3WithoutId3Segment());
});
QUnit.test('ac3 with id3 segments will not make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.ac3';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 0,
trackInfo: 1,
progress: 1,
timingInfo: 0,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ac3', 'segment-request');
this.standardXHRResponse(this.requests[0], ac3WithId3Segment());
});
QUnit.test('muxed ts segments will make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.ts';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 2,
trackInfo: 1,
progress: 1,
timingInfo: 4,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 1
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ts', 'segment-request');
this.standardXHRResponse(this.requests[0], muxedSegment());
});
QUnit.test('video ts segments will make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.ts';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 1
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ts', 'segment-request');
this.standardXHRResponse(this.requests[0], videoSegment());
});
QUnit.test('audio ts segments will make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.ts';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ts', 'segment-request');
this.standardXHRResponse(this.requests[0], audioSegment());
});
QUnit.test('aac with id3 will make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.aac';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.aac', 'segment-request');
this.standardXHRResponse(this.requests[0], aacWithId3Segment());
});
QUnit.test('aac without id3 will make it to the transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer();
this.options.segment.resolvedUri = 'foo.aac';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.aac', 'segment-request');
this.standardXHRResponse(this.requests[0], aacWithoutId3Segment());
});
QUnit.test('ac3 without id3 segments will not make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.ac3';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 0,
trackInfo: 1,
progress: 1,
timingInfo: 0,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ac3', 'segment-request');
this.standardXHRResponse(this.requests[0], ac3WithoutId3Segment());
});
QUnit.test('ac3 with id3 segments will not make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.ac3';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 0,
trackInfo: 1,
progress: 1,
timingInfo: 0,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ac3', 'segment-request');
this.standardXHRResponse(this.requests[0], ac3WithId3Segment());
});
QUnit.test('muxed ts segments will make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.ts';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 3,
trackInfo: 1,
progress: 1,
timingInfo: 4,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ts', 'segment-request');
this.standardXHRResponse(this.requests[0], muxedSegment());
});
QUnit.test('video ts segments will make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.ts';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 2,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.ts', 'segment-request');
this.standardXHRResponse(this.requests[0], videoSegment());
});
QUnit.test('audio ts segments will make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.aac';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.aac', 'segment-request');
this.standardXHRResponse(this.requests[0], audioSegment());
});
QUnit.test('aac with id3 will make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.aac';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.aac', 'segment-request');
this.standardXHRResponse(this.requests[0], aacWithId3Segment());
});
QUnit.test('aac without id3 will make it to the partial transmuxer', function(assert) {
const done = assert.async();
this.options.segment.transmuxer = this.createTransmuxer(true);
this.options.segment.resolvedUri = 'foo.aac';
this.options.doneFn = () => {
assert.deepEqual(this.calls, {
data: 1,
trackInfo: 1,
progress: 1,
timingInfo: 2,
captions: 0,
id3: 0,
videoSegmentTimingInfo: 0
}, 'calls as expected');
done();
};
mediaSegmentRequest(this.options);
assert.equal(this.requests[0].uri, 'foo.aac', 'segment-request');
this.standardXHRResponse(this.requests[0], aacWithoutId3Segment());
});
QUnit.module('Media Segment Request', {
beforeEach(assert) {
sharedHooks.beforeEach.call(this, assert);
},
afterEach(assert) {
sharedHooks.afterEach.call(this, assert);
}
});
QUnit.test('cancels outstanding segment request on abort', function(assert) {
let aborts = 0;
const abort = mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.noop,
segment: { resolvedUri: '0-test.ts' },
abortFn: () => aborts++,
progressFn: this.noop,
doneFn: this.noop
});
// Simulate Firefox's handling of aborted segments -
// Firefox sets the response to an empty array buffer if the xhr type is 'arraybuffer'
// and no data was received
this.requests[0].response = new ArrayBuffer();
abort();
this.clock.tick(1);
assert.equal(this.requests.length, 1, 'there is only one request');
assert.equal(this.requests[0].uri, '0-test.ts', 'the request is for a segment');
assert.ok(this.requests[0].aborted, 'aborted the first request');
assert.equal(aborts, 1, 'one abort');
});
QUnit.test('cancels outstanding key requests on abort', function(assert) {
let aborts = 0;
const abort = mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.noop,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php'
}
},
abortFn: () => aborts++,
progressFn: this.noop,
doneFn: this.noop
});
assert.equal(this.requests.length, 2, 'there are two requests');
const keyReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
// Fulfill the segment request
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(10).buffer);
abort();
this.clock.tick(1);
assert.ok(keyReq.aborted, 'aborted the key request');
assert.equal(aborts, 1, 'one abort');
});
QUnit.test('cancels outstanding key requests on failure', function(assert) {
let keyReq;
const done = assert.async();
assert.expect(7);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.noop,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php'
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.ok(keyReq.aborted, 'aborted the key request');
assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'segment request failed');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
keyReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
// Fulfill the segment request
segmentReq.respond(500, null, '');
});
QUnit.test('cancels outstanding key requests on timeout', function(assert) {
let keyReq;
const done = assert.async();
assert.expect(7);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.noop,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php'
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.ok(keyReq.aborted, 'aborted the key request');
assert.equal(error.code, REQUEST_ERRORS.TIMEOUT, 'key request failed');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
keyReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
// Timeout request
this.clock.tick(2000);
});
QUnit.test(
'does not wait for other requests to finish when one request errors',
function(assert) {
let keyReq;
let abortedKeyReq = false;
const done = assert.async();
assert.expect(8);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.noop,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php'
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(keyReq.aborted, 'did not run original abort function');
assert.ok(abortedKeyReq, 'ran overridden abort function');
assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'request failed');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
keyReq = this.requests.shift();
// Typically, an abort will run the error algorithm for an XHR, however, in certain
// cases (e.g., if the request is unsent), the error algorithm will not be run and
// the request will never "finish." In order to mimic this behavior, override the
// default abort function so that it doesn't finish.
keyReq.abort = () => {
abortedKeyReq = true;
};
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
segmentReq.respond(500, null, '');
}
);
QUnit.test('the key response is converted to the correct format', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
assert.expect(9);
this.mockDecrypter.postMessage = (message) => {
const key = new Uint32Array(
message.key.bytes,
message.key.byteOffset,
message.key.byteLength / 4
);
assert.deepEqual(
key,
new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
'passed the specified segment key'
);
postMessage.call(this.mockDecrypter, message);
};
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
IV: [0, 0, 0, 1]
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.equal(
this.mockDecrypter.listeners.length,
0,
'all decryption webworker listeners are unbound'
);
// verify stats
assert.equal(segmentData.stats.bytesReceived, 10, '10 bytes');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
const keyReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(10).buffer);
keyReq.responseType = 'arraybuffer';
keyReq.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
});
QUnit.test('segment with key has bytes decrypted', function(assert) {
const done = assert.async();
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.realDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.bytes, 'decrypted bytes in segment');
assert.ok(segmentData.key.bytes, 'key bytes in segment');
assert.equal(
segmentData.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);
// verify stats
assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
const keyReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(8).buffer);
keyReq.responseType = 'arraybuffer';
keyReq.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
});
QUnit.test('segment with key bytes does not request key again', function(assert) {
const done = assert.async();
mediaSegmentRequest({xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.realDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
bytes: new Uint32Array([0, 2, 3, 1]),
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.bytes, 'decrypted bytes in segment');
assert.ok(segmentData.key.bytes, 'key bytes in segment');
assert.equal(
segmentData.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);
// verify stats
assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes');
done();
}});
assert.equal(this.requests.length, 1, 'there is one request');
const segmentReq = this.requests.shift();
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(8).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
});
QUnit.test('key 404 calls back with error', function(assert) {
const done = assert.async();
let segmentReq;
assert.expect(11);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.realDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.ok(segmentReq.aborted, 'segment request aborted');
assert.ok(error, 'there is an error');
assert.equal(error.status, 404, 'error status matches response code');
assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'error code set to FAILURE');
assert.notOk(segmentData.bytes, 'no bytes in segment');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
const keyReq = this.requests.shift();
segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
assert.notOk(segmentReq.aborted, 'segment request not aborted');
keyReq.respond(404, null, '');
});
QUnit.test('key 500 calls back with error', function(assert) {
const done = assert.async();
let segmentReq;
assert.expect(11);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.realDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.ok(segmentReq.aborted, 'segment request aborted');
assert.ok(error, 'there is an error');
assert.equal(error.status, 500, 'error status matches response code');
assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'error code set to FAILURE');
assert.notOk(segmentData.bytes, 'no bytes in segment');
done();
}
});
assert.equal(this.requests.length, 2, 'there are two requests');
const keyReq = this.requests.shift();
segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');
assert.notOk(segmentReq.aborted, 'segment request not aborted');
keyReq.respond(500, null, '');
});
QUnit.test(
'waits for every request to finish before the callback is run',
function(assert) {
const done = assert.async();
assert.expect(10);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.realDecrypter,
segment: {
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-init.dat'
}
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.bytes, 'decrypted bytes in segment');
assert.ok(segmentData.map.bytes, 'init segment bytes in map');
// verify stats
assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes');
done();
}
});
assert.equal(this.requests.length, 3, 'there are three requests');
const keyReq = this.requests.shift();
const initReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(initReq.uri, '0-init.dat', 'the second request is for the init segment');
assert.equal(segmentReq.uri, '0-test.ts', 'the third request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(8).buffer);
this.clock.tick(200);
initReq.responseType = 'arraybuffer';
initReq.respond(200, null, mp4VideoInit().buffer);
this.clock.tick(200);
keyReq.responseType = 'arraybuffer';
keyReq.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
}
);
QUnit.test('non-TS segment will get parsed for captions', function(assert) {
const done = assert.async();
let gotCaption = false;
let gotData = false;
const captions = [{foo: 'bar'}];
const transmuxer = new videojs.EventTarget();
transmuxer.postMessage = (event) => {
if (event.action === 'pushMp4Captions') {
transmuxer.trigger({
type: 'message',
data: {
action: 'mp4Captions',
data: event.data,
captions
}
});
}
};
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer,
resolvedUri: 'mp4Video.mp4',
map: {
resolvedUri: 'mp4VideoInit.mp4'
},
isFmp4: true
},
progressFn: this.noop,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: (segment, _captions) => {
gotCaption = true;
assert.equal(captions, _captions, 'captions as expected');
},
dataFn: (segment, segmentData) => {
gotData = true;
assert.ok(segment.map.bytes, 'init segment bytes in map');
assert.ok(segment.map.tracks, 'added tracks');
assert.ok(segment.map.tracks.video, 'added video track');
},
doneFn: () => {
assert.ok(gotCaption, 'received caption event');
assert.ok(gotData, 'received data event');
transmuxer.off();
done();
},
handlePartialData: false
});
assert.equal(this.requests.length, 2, 'there are two requests');
const initReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(initReq.uri, 'mp4VideoInit.mp4', 'the first request is for the init segment');
assert.equal(segmentReq.uri, 'mp4Video.mp4', 'the second request is for a segment');
this.standardXHRResponse(initReq, mp4VideoInit());
this.standardXHRResponse(segmentReq, mp4Video());
});
QUnit.test('webm segment calls back with error', function(assert) {
const done = assert.async();
let gotData = false;
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'webmVideo.mp4',
map: {
resolvedUri: 'webmVideoInit.mp4'
},
isFmp4: true
},
progressFn: this.noop,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: this.noop,
dataFn: (segment, segmentData) => {
gotData = true;
},
doneFn: (error) => {
assert.notOk(gotData, 'did not receive data event');
assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'receieved error status');
assert.equal(
error.message,
'Found unsupported webm container for initialization segment at URL: webmVideoInit.mp4',
'receieved error message'
);
done();
},
handlePartialData: false
});
assert.equal(this.requests.length, 2, 'there are two requests');
const initReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(
initReq.uri,
'webmVideoInit.mp4',
'the first request is for the init segment'
);
assert.equal(segmentReq.uri, 'webmVideo.mp4', 'the second request is for a segment');
this.standardXHRResponse(segmentReq, webmVideo());
this.standardXHRResponse(initReq, webmVideoInit());
});
QUnit.test('non-TS segment will get parsed for captions on next segment request if init is late', function(assert) {
const done = assert.async();
let gotCaption = 0;
let gotData = 0;
const captions = [{foo: 'bar'}];
const transmuxer = new videojs.EventTarget();
transmuxer.postMessage = (event) => {
if (event.action === 'pushMp4Captions') {
transmuxer.trigger({
type: 'message',
data: {
action: 'mp4Captions',
data: event.data,
captions
}
});
}
};
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer,
resolvedUri: 'mp4Video.mp4',
map: {
resolvedUri: 'mp4VideoInit.mp4'
}
},
progressFn: this.noop,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: (segment, _captions) => {
gotCaption++;
// verify the caption parser
assert.deepEqual(
captions,
_captions,
'the expected captions were received'
);
},
dataFn: (segment, segmentData) => {
gotData++;
assert.ok(segmentData, 'init segment bytes in map');
assert.ok(segment.map.tracks, 'added tracks');
assert.ok(segment.map.tracks.video, 'added video track');
},
doneFn: () => {
assert.equal(gotCaption, 1, 'received caption event');
assert.equal(gotData, 1, 'received data event');
transmuxer.off();
done();
},
handlePartialData: false
});
assert.equal(this.requests.length, 2, 'there are two requests');
const initReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(initReq.uri, 'mp4VideoInit.mp4', 'the first request is for the init segment');
assert.equal(segmentReq.uri, 'mp4Video.mp4', 'the second request is for a segment');
// Simulate receiving the media first
this.standardXHRResponse(segmentReq, mp4Video());
// Simulate receiving the init segment after the media
this.standardXHRResponse(initReq, mp4VideoInit());
});
QUnit.test('callbacks fire for TS segment with partial data', function(assert) {
const progressSpy = sinon.spy();
const trackInfoSpy = sinon.spy();
const timingInfoSpy = sinon.spy();
const dataSpy = sinon.spy();
const done = assert.async();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'muxed.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: trackInfoSpy,
timingInfoFn: timingInfoSpy,
id3Fn: this.noop,
captionsFn: this.noop,
dataFn: dataSpy,
doneFn: () => {
// sinon will fire a second progress event at the end of the request (as specified
// by the xhr standard)
assert.strictEqual(progressSpy.callCount, 2, 'saw a progress event');
assert.ok(trackInfoSpy.callCount, 'got trackInfo');
assert.ok(timingInfoSpy.callCount, 'got timingInfo');
assert.ok(dataSpy.callCount, 'got data event');
done();
},
handlePartialData: true
});
const request = this.requests.shift();
// Need to take enough of the segment to trigger a data event
const partialResponse = muxedSegmentString().substring(0, 1700);
request.responseType = 'arraybuffer';
// simulates progress event
downloadProgress(request, partialResponse);
this.standardXHRResponse(request, muxedSegment());
});
// TODO: tests after this one appear to fail
QUnit.test('data callback does not fire if too little partial data', function(assert) {
const progressSpy = sinon.spy();
const dataSpy = sinon.spy();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'muxed.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: this.noop,
dataFn: dataSpy,
doneFn: this.noop,
handlePartialData: true
});
const request = this.requests.shift();
request.responseType = 'arraybuffer';
// less data than needed for a data event to be fired
const partialResponse = muxedSegmentString().substring(0, 1000);
// simulates progress event
downloadProgress(request, partialResponse);
this.clock.tick(1);
assert.ok(progressSpy.callCount, 'got a progress event');
assert.notOk(dataSpy.callCount, 'did not get data event');
});
// TODO test only worked with the completion of a segment request. It should be rewritten
// to account for partial data only.
QUnit.skip('caption callback fires for TS segment with partial data', function(assert) {
const progressSpy = sinon.spy();
const captionSpy = sinon.spy();
const dataSpy = sinon.spy();
const done = assert.async();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'caption.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: captionSpy,
dataFn: dataSpy,
doneFn: () => {
// sinon will fire a second progress event at the end of the request (as specified
// by the xhr standard)
assert.strictEqual(progressSpy.callCount, 2, 'saw a progress event');
assert.strictEqual(captionSpy.callCount, 1, 'got one caption back');
assert.ok(dataSpy.callCount, 'got data event');
done();
},
handlePartialData: true
});
const request = this.requests.shift();
request.responseType = 'arraybuffer';
// Need to take enough of the segment to trigger
// a data and caption event
const partialResponse = captionSegmentString().substring(0, 190000);
// simulates progress event
downloadProgress(request, partialResponse);
this.standardXHRResponse(request, captionSegment());
});
// TODO test only worked with the completion of a segment request. It should be rewritten
// to account for partial data only.
QUnit.skip('caption callback does not fire if partial data has no captions', function(assert) {
const progressSpy = sinon.spy();
const captionSpy = sinon.spy();
const dataSpy = sinon.spy();
const done = assert.async();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'caption.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: this.noop,
captionsFn: captionSpy,
dataFn: dataSpy,
doneFn: () => {
// sinon will fire a second progress event at the end of the request (as specified
// by the xhr standard)
assert.strictEqual(progressSpy.callCount, 2, 'saw a progress event');
assert.strictEqual(captionSpy.callCount, 0, 'got no caption back');
assert.ok(dataSpy.callCount, 'got data event');
done();
},
handlePartialData: true
});
const request = this.requests.shift();
request.responseType = 'arraybuffer';
// Need to take enough of the segment to trigger a data event
const partialResponse = muxedSegmentString().substring(0, 1700);
// simulates progress event
downloadProgress(request, partialResponse);
this.standardXHRResponse(request, muxedSegment());
});
// TODO test only worked with the completion of a segment request. It should be rewritten
// to account for partial data only.
QUnit.skip('id3 callback fires for TS segment with partial data', function(assert) {
const progressSpy = sinon.spy();
const id3Spy = sinon.spy();
const dataSpy = sinon.spy();
const done = assert.async();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'id3.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: id3Spy,
captionsFn: this.noop,
dataFn: dataSpy,
doneFn: () => {
assert.strictEqual(progressSpy.callCount, 1, 'saw 1 progress event');
assert.strictEqual(id3Spy.callCount, 1, 'got one id3Frame back');
assert.ok(dataSpy.callCount, 'got data event');
done();
},
handlePartialData: true
});
const request = this.requests.shift();
request.responseType = 'arraybuffer';
// Need to take enough of the segment to trigger
// a data and id3Frame event
const partialResponse = id3SegmentString().substring(0, 900);
// simulates progress event
downloadProgress(request, partialResponse);
// note that this test only worked with the completion of the segment request
// it should be fixed to account for only partial data
this.standardXHRResponse(request, id3Segment());
});
// TODO test only worked with the completion of a segment request. It should be rewritten
// to account for partial data only.
QUnit.skip('id3 callback does not fire if partial data has no ID3 tags', function(assert) {
const progressSpy = sinon.spy();
const id3Spy = sinon.spy();
const dataSpy = sinon.spy();
const done = assert.async();
this.transmuxer = this.createTransmuxer(true);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
resolvedUri: 'id3.ts',
transmuxer: this.transmuxer
},
progressFn: progressSpy,
trackInfoFn: this.noop,
timingInfoFn: this.noop,
id3Fn: id3Spy,
captionsFn: this.noop,
dataFn: dataSpy,
doneFn: () => {
// sinon will fire a second progress event at the end of the request (as specified
// by the xhr standard)
assert.strictEqual(progressSpy.callCount, 2, 'saw a progress event');
assert.strictEqual(id3Spy.callCount, 0, 'got no id3Frames back');
assert.ok(dataSpy.callCount, 'got data event');
done();
},
handlePartialData: true
});
const request = this.requests.shift();
request.responseType = 'arraybuffer';
// Need to take enough of the segment to trigger a data event
const partialResponse = muxedSegmentString().substring(0, 1700);
// simulates progress event
downloadProgress(request, partialResponse);
// note that this test only worked with the completion of the segment request
// it should be fixed to account for only partial data
this.standardXHRResponse(request, muxedSegment());
});