Browse Source

feat: Add support for encrypted init segments (#1132)

pull/1135/head
Brandon Casey 4 years ago
committed by GitHub
parent
commit
4449ed58a4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      scripts/sources.json
  2. 312
      src/media-segment-request.js
  3. 4
      src/playlist-loader.js
  4. 548
      test/media-segment-request.test.js
  5. 2
      test/playback.test.js
  6. 9
      test/playlist-loader.test.js

6
scripts/sources.json

@ -370,6 +370,12 @@
"mimetype": "application/dash+xml",
"features": []
},
{
"name": "encrypted init segment",
"uri": "https://d2zihajmogu5jn.cloudfront.net/encrypted-init-segment/master.m3u8",
"mimetype": "application/x-mpegurl",
"features": []
},
{
"name": "Dash data uri for https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd",
"uri": "data:application/dash+xml;charset=utf-8,%3CMPD%20mediaPresentationDuration=%22PT634.566S%22%20minBufferTime=%22PT2.00S%22%20profiles=%22urn:hbbtv:dash:profile:isoff-live:2012,urn:mpeg:dash:profile:isoff-live:2011%22%20type=%22static%22%20xmlns=%22urn:mpeg:dash:schema:mpd:2011%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22urn:mpeg:DASH:schema:MPD:2011%20DASH-MPD.xsd%22%3E%20%3CBaseURL%3Ehttps://dash.akamaized.net/akamai/bbb_30fps/%3C/BaseURL%3E%20%3CPeriod%3E%20%20%3CAdaptationSet%20mimeType=%22video/mp4%22%20contentType=%22video%22%20subsegmentAlignment=%22true%22%20subsegmentStartsWithSAP=%221%22%20par=%2216:9%22%3E%20%20%20%3CSegmentTemplate%20duration=%22120%22%20timescale=%2230%22%20media=%22$RepresentationID$/$RepresentationID$_$Number$.m4v%22%20startNumber=%221%22%20initialization=%22$RepresentationID$/$RepresentationID$_0.m4v%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1024x576_2500k%22%20codecs=%22avc1.64001f%22%20bandwidth=%223134488%22%20width=%221024%22%20height=%22576%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1280x720_4000k%22%20codecs=%22avc1.64001f%22%20bandwidth=%224952892%22%20width=%221280%22%20height=%22720%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1920x1080_8000k%22%20codecs=%22avc1.640028%22%20bandwidth=%229914554%22%20width=%221920%22%20height=%221080%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_320x180_200k%22%20codecs=%22avc1.64000d%22%20bandwidth=%22254320%22%20width=%22320%22%20height=%22180%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_320x180_400k%22%20codecs=%22avc1.64000d%22%20bandwidth=%22507246%22%20width=%22320%22%20height=%22180%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_480x270_600k%22%20codecs=%22avc1.640015%22%20bandwidth=%22759798%22%20width=%22480%22%20height=%22270%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_640x360_1000k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221254758%22%20width=%22640%22%20height=%22360%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_640x360_800k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221013310%22%20width=%22640%22%20height=%22360%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_768x432_1500k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221883700%22%20width=%22768%22%20height=%22432%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_3840x2160_12000k%22%20codecs=%22avc1.640033%22%20bandwidth=%2214931538%22%20width=%223840%22%20height=%222160%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%3C/AdaptationSet%3E%20%20%3CAdaptationSet%20mimeType=%22audio/mp4%22%20contentType=%22audio%22%20subsegmentAlignment=%22true%22%20subsegmentStartsWithSAP=%221%22%3E%20%20%20%3CAccessibility%20schemeIdUri=%22urn:tva:metadata:cs:AudioPurposeCS:2007%22%20value=%226%22/%3E%20%20%20%3CRole%20schemeIdUri=%22urn:mpeg:dash:role:2011%22%20value=%22main%22/%3E%20%20%20%3CSegmentTemplate%20duration=%22192512%22%20timescale=%2248000%22%20media=%22$RepresentationID$/$RepresentationID$_$Number$.m4a%22%20startNumber=%221%22%20initialization=%22$RepresentationID$/$RepresentationID$_0.m4a%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_a64k%22%20codecs=%22mp4a.40.5%22%20bandwidth=%2267071%22%20audioSamplingRate=%2248000%22%3E%20%20%20%20%3CAudioChannelConfiguration%20schemeIdUri=%22urn:mpeg:dash:23003:3:audio_channel_configuration:2011%22%20value=%222%22/%3E%20%20%20%3C/Representation%3E%20%20%3C/AdaptationSet%3E%20%3C/Period%3E%3C/MPD%3E",

312
src/media-segment-request.js

@ -98,6 +98,15 @@ const handleErrors = (error, request) => {
};
}
if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) {
return {
status: request.status,
message: 'Empty HLS response at URL: ' + request.uri,
code: REQUEST_ERRORS.FAILURE,
xhr: request
};
}
return null;
};
@ -107,10 +116,11 @@ const handleErrors = (error, request) => {
*
* @param {Object} segment - a simplified copy of the segmentInfo object
* from SegmentLoader
* @param {Array} objects - objects to add the key bytes to.
* @param {Function} finishProcessingFn - a callback to execute to continue processing
* this request
*/
const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => {
const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, request) => {
const response = request.response;
const errorObj = handleErrors(error, request);
@ -128,57 +138,33 @@ const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => {
}
const view = new DataView(response);
segment.key.bytes = new Uint32Array([
const bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
return finishProcessingFn(null, segment);
};
/**
* Handle init-segment responses
*
* @param {Object} segment - a simplified copy of the segmentInfo object
* from SegmentLoader
* @param {Function} finishProcessingFn - a callback to execute to continue processing
* this request
*/
const handleInitSegmentResponse =
({segment, finishProcessingFn}) => (error, request) => {
const response = request.response;
const errorObj = handleErrors(error, request);
if (errorObj) {
return finishProcessingFn(errorObj, segment);
for (let i = 0; i < objects.length; i++) {
objects[i].bytes = bytes;
}
// stop processing if received empty content
if (response.byteLength === 0) {
return finishProcessingFn({
status: request.status,
message: 'Empty HLS segment content at URL: ' + request.uri,
code: REQUEST_ERRORS.FAILURE,
xhr: request
}, segment);
}
segment.map.bytes = new Uint8Array(request.response);
return finishProcessingFn(null, segment);
};
const parseInitSegment = (segment, callback) => {
const type = detectContainerForBytes(segment.map.bytes);
// TODO: We should also handle ts init segments here, but we
// only know how to parse mp4 init segments at the moment
if (type !== 'mp4') {
return finishProcessingFn({
status: request.status,
message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${request.uri}`,
code: REQUEST_ERRORS.FAILURE,
const uri = segment.map.resolvedUri || segment.map.uri;
return callback({
internal: true,
xhr: request
}, segment);
message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${uri}`,
code: REQUEST_ERRORS.FAILURE
});
}
workerCallback({
@ -206,8 +192,46 @@ const handleInitSegmentResponse =
});
return finishProcessingFn(null, segment);
return callback(null);
}
});
};
/**
* Handle init-segment responses
*
* @param {Object} segment - a simplified copy of the segmentInfo object
* from SegmentLoader
* @param {Function} finishProcessingFn - a callback to execute to continue processing
* this request
*/
const handleInitSegmentResponse =
({segment, finishProcessingFn}) => (error, request) => {
const errorObj = handleErrors(error, request);
if (errorObj) {
return finishProcessingFn(errorObj, segment);
}
const bytes = new Uint8Array(request.response);
// init segment is encypted, we will have to wait
// until the key request is done to decrypt.
if (segment.map.key) {
segment.map.encryptedBytes = bytes;
return finishProcessingFn(null, segment);
}
segment.map.bytes = bytes;
parseInitSegment(segment, function(parseError) {
if (parseError) {
parseError.xhr = request;
parseError.status = request.status;
return finishProcessingFn(parseError, segment);
}
finishProcessingFn(null, segment);
});
};
@ -226,7 +250,6 @@ const handleSegmentResponse = ({
finishProcessingFn,
responseType
}) => (error, request) => {
const response = request.response;
const errorObj = handleErrors(error, request);
if (errorObj) {
@ -243,16 +266,6 @@ const handleSegmentResponse = ({
request.response :
stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0));
// stop processing if received empty content
if (response.byteLength === 0) {
return finishProcessingFn({
status: request.status,
message: 'Empty HLS segment content at URL: ' + request.uri,
code: REQUEST_ERRORS.FAILURE,
xhr: request
}, segment);
}
segment.stats = getRequestStats(request);
if (segment.key) {
@ -538,6 +551,42 @@ const handleSegmentBytes = ({
});
};
const decrypt = function({id, key, encryptedBytes, decryptionWorker}, callback) {
const decryptionHandler = (event) => {
if (event.data.source === id) {
decryptionWorker.removeEventListener('message', decryptionHandler);
const decrypted = event.data.decrypted;
callback(new Uint8Array(
decrypted.bytes,
decrypted.byteOffset,
decrypted.byteLength
));
}
};
decryptionWorker.addEventListener('message', decryptionHandler);
let keyBytes;
if (key.bytes.slice) {
keyBytes = key.bytes.slice();
} else {
keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes));
}
// incrementally decrypt the bytes
decryptionWorker.postMessage(createTransferableMessage({
source: id,
encrypted: encryptedBytes,
key: keyBytes,
iv: key.iv
}), [
encryptedBytes.buffer,
keyBytes.buffer
]);
};
/**
* Decrypt the segment via the decryption web worker
*
@ -576,55 +625,29 @@ const decryptSegment = ({
dataFn,
doneFn
}) => {
const decryptionHandler = (event) => {
if (event.data.source === segment.requestId) {
decryptionWorker.removeEventListener('message', decryptionHandler);
const decrypted = event.data.decrypted;
segment.bytes = new Uint8Array(
decrypted.bytes,
decrypted.byteOffset,
decrypted.byteLength
);
handleSegmentBytes({
segment,
bytes: segment.bytes,
trackInfoFn,
timingInfoFn,
videoSegmentTimingInfoFn,
audioSegmentTimingInfoFn,
id3Fn,
captionsFn,
isEndOfTimeline,
endedTimelineFn,
dataFn,
doneFn
});
}
};
decryptionWorker.addEventListener('message', decryptionHandler);
let keyBytes;
if (segment.key.bytes.slice) {
keyBytes = segment.key.bytes.slice();
} else {
keyBytes = new Uint32Array(Array.prototype.slice.call(segment.key.bytes));
}
// this is an encrypted segment
// incrementally decrypt the segment
decryptionWorker.postMessage(createTransferableMessage({
source: segment.requestId,
encrypted: segment.encryptedBytes,
key: keyBytes,
iv: segment.key.iv
}), [
segment.encryptedBytes.buffer,
keyBytes.buffer
]);
decrypt({
id: segment.requestId,
key: segment.key,
encryptedBytes: segment.encryptedBytes,
decryptionWorker
}, (decryptedBytes) => {
segment.bytes = decryptedBytes;
handleSegmentBytes({
segment,
bytes: segment.bytes,
trackInfoFn,
timingInfoFn,
videoSegmentTimingInfoFn,
audioSegmentTimingInfoFn,
id3Fn,
captionsFn,
isEndOfTimeline,
endedTimelineFn,
dataFn,
doneFn
});
});
};
/**
@ -700,13 +723,27 @@ const waitForCompletion = ({
count += 1;
if (count === activeXhrs.length) {
// Keep track of when *all* of the requests have completed
segment.endOfAllRequests = Date.now();
if (segment.encryptedBytes) {
return decryptSegment({
decryptionWorker,
const segmentFinish = function() {
if (segment.encryptedBytes) {
return decryptSegment({
decryptionWorker,
segment,
trackInfoFn,
timingInfoFn,
videoSegmentTimingInfoFn,
audioSegmentTimingInfoFn,
id3Fn,
captionsFn,
isEndOfTimeline,
endedTimelineFn,
dataFn,
doneFn
});
}
// Otherwise, everything is ready just continue
handleSegmentBytes({
segment,
bytes: segment.bytes,
trackInfoFn,
timingInfoFn,
videoSegmentTimingInfoFn,
@ -718,22 +755,35 @@ const waitForCompletion = ({
dataFn,
doneFn
});
};
// Keep track of when *all* of the requests have completed
segment.endOfAllRequests = Date.now();
if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) {
return decrypt({
decryptionWorker,
// add -init to the "id" to differentiate between segment
// and init segment decryption, just in case they happen
// at the same time at some point in the future.
id: segment.requestId + '-init',
encryptedBytes: segment.map.encryptedBytes,
key: segment.map.key
}, (decryptedBytes) => {
segment.map.bytes = decryptedBytes;
parseInitSegment(segment, (parseError) => {
if (parseError) {
abortAll(activeXhrs);
return doneFn(parseError, segment);
}
segmentFinish();
});
});
}
// Otherwise, everything is ready just continue
handleSegmentBytes({
segment,
bytes: segment.bytes,
trackInfoFn,
timingInfoFn,
videoSegmentTimingInfoFn,
audioSegmentTimingInfoFn,
id3Fn,
captionsFn,
isEndOfTimeline,
endedTimelineFn,
dataFn,
doneFn
});
segmentFinish();
}
};
};
@ -912,11 +962,16 @@ export const mediaSegmentRequest = ({
// optionally, request the decryption key
if (segment.key && !segment.key.bytes) {
const objects = [segment.key];
if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) {
objects.push(segment.map.key);
}
const keyRequestOptions = videojs.mergeOptions(xhrOptions, {
uri: segment.key.resolvedUri,
responseType: 'arraybuffer'
});
const keyRequestCallback = handleKeyResponse(segment, finishProcessingFn);
const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn);
const keyXhr = xhr(keyRequestOptions, keyRequestCallback);
activeXhrs.push(keyXhr);
@ -924,15 +979,24 @@ export const mediaSegmentRequest = ({
// optionally, request the associated media init segment
if (segment.map && !segment.map.bytes) {
const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri);
if (differentMapKey) {
const mapKeyRequestOptions = videojs.mergeOptions(xhrOptions, {
uri: segment.map.key.resolvedUri,
responseType: 'arraybuffer'
});
const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn);
const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback);
activeXhrs.push(mapKeyXhr);
}
const initSegmentOptions = videojs.mergeOptions(xhrOptions, {
uri: segment.map.resolvedUri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment.map)
});
const initSegmentRequestCallback = handleInitSegmentResponse({
segment,
finishProcessingFn
});
const initSegmentRequestCallback = handleInitSegmentResponse({segment, finishProcessingFn});
const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
activeXhrs.push(initSegmentXhr);

4
src/playlist-loader.js

@ -188,6 +188,10 @@ export const resolveSegmentUris = (segment, baseUri) => {
if (segment.map && !segment.map.resolvedUri) {
segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
}
if (segment.map && segment.map.key && !segment.map.key.resolvedUri) {
segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri);
}
if (segment.parts && segment.parts.length) {
segment.parts.forEach((p) => {
if (p.resolvedUri) {

548
test/media-segment-request.test.js

@ -708,6 +708,554 @@ QUnit.test('key 500 calls back with error', function(assert) {
keyReq.respond(500, null, '');
});
QUnit.test('init segment with key has bytes decrypted', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
this.transmuxer = this.createTransmuxer();
// mock decrypting the init segment.
this.mockDecrypter.postMessage = (message) => {
message.encrypted.bytes = mp4VideoInit().buffer;
message.encrypted.byteLength = message.encrypted.bytes.byteLength;
message.encrypted.byteOffset = 0;
return postMessage.call(this.mockDecrypter, message);
};
let trackInfo;
const timingInfo = {};
let data;
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
map: {
resolvedUri: '0-map.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn(segment, _trackInfo) {
trackInfo = _trackInfo;
},
timingInfoFn(segment, type, prop, value) {
timingInfo[type] = timingInfo[type] || {};
timingInfo[type][prop] = value;
},
dataFn(segment, _data) {
data = _data;
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.map.bytes, 'decrypted bytes in map');
assert.ok(segmentData.map.key.bytes, 'key bytes in map');
assert.equal(
segmentData.map.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);
// verify stats
assert.equal(segmentData.stats.bytesReceived, 6198, '6198 bytes');
assert.ok(data, 'got data');
assert.ok(trackInfo, 'got track info');
assert.ok(Object.keys(timingInfo).length, 'got timing info');
done();
}
});
assert.equal(this.requests.length, 3, 'there are three requests');
const keyReq = this.requests.shift();
const mapReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(mapReq.uri, '0-map.mp4', 'the second request is for a map');
assert.equal(segmentReq.uri, '0-test.mp4', 'the third request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, mp4Video().buffer);
mapReq.responseType = 'arraybuffer';
mapReq.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/init segment share a key and get decrypted', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
this.transmuxer = this.createTransmuxer();
// mock decrypting the init segment.
this.mockDecrypter.postMessage = (message) => {
// segment is 9, init is 8
if (message.encrypted.byteLength === 8) {
message.encrypted.bytes = mp4VideoInit().buffer;
} else {
message.encrypted.bytes = mp4Video();
}
message.encrypted.byteLength = message.encrypted.bytes.byteLength;
message.encrypted.byteOffset = 0;
return postMessage.call(this.mockDecrypter, message);
};
let trackInfo;
const timingInfo = {};
let data;
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-map.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn(segment, _trackInfo) {
trackInfo = _trackInfo;
},
timingInfoFn(segment, type, prop, value) {
timingInfo[type] = timingInfo[type] || {};
timingInfo[type][prop] = value;
},
dataFn(segment, _data) {
data = _data;
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.map.bytes, 'decrypted bytes in map');
assert.ok(segmentData.map.key.bytes, 'key bytes in map');
assert.equal(
segmentData.map.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);
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, 9, '9 bytes');
assert.equal(segmentData.key.bytes, segmentData.map.key.bytes, 'keys are the same');
assert.ok(data, 'got data');
assert.ok(trackInfo, 'got track info');
assert.ok(Object.keys(timingInfo).length, 'got timing info');
done();
}
});
assert.equal(this.requests.length, 3, 'there are three requests');
const keyReq = this.requests.shift();
const mapReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(mapReq.uri, '0-map.mp4', 'the second request is for a map');
assert.equal(segmentReq.uri, '0-test.mp4', 'the third request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(9).buffer);
mapReq.responseType = 'arraybuffer';
mapReq.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/init segment different key and get decrypted', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
this.transmuxer = this.createTransmuxer();
// mock decrypting the init segment.
this.mockDecrypter.postMessage = (message) => {
// segment is 9, init is 8
if (message.encrypted.byteLength === 8) {
message.encrypted.bytes = mp4VideoInit().buffer;
} else {
message.encrypted.bytes = mp4Video();
}
message.encrypted.byteLength = message.encrypted.bytes.byteLength;
message.encrypted.byteOffset = 0;
return postMessage.call(this.mockDecrypter, message);
};
let trackInfo;
const timingInfo = {};
let data;
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-map.mp4',
key: {
resolvedUri: '1-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn(segment, _trackInfo) {
trackInfo = _trackInfo;
},
timingInfoFn(segment, type, prop, value) {
timingInfo[type] = timingInfo[type] || {};
timingInfo[type][prop] = value;
},
dataFn(segment, _data) {
data = _data;
},
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.map.bytes, 'decrypted bytes in map');
assert.ok(segmentData.map.key.bytes, 'key bytes in map');
assert.equal(
segmentData.map.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);
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'
);
assert.notEqual(segmentData.key.bytes, segmentData.map.key.bytes, 'keys are different');
// verify stats
assert.equal(segmentData.stats.bytesReceived, 9, '9 bytes');
assert.ok(data, 'got data');
assert.ok(trackInfo, 'got track info');
assert.ok(Object.keys(timingInfo).length, 'got timing info');
done();
}
});
assert.equal(this.requests.length, 4, 'there are four requests');
const keyReq = this.requests.shift();
const keyReq2 = this.requests.shift();
const mapReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(keyReq2.uri, '1-key.php', 'the second request is for a key');
assert.equal(mapReq.uri, '0-map.mp4', 'the third request is for a map');
assert.equal(segmentReq.uri, '0-test.mp4', 'the forth request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(9).buffer);
mapReq.responseType = 'arraybuffer';
mapReq.respond(200, null, new Uint8Array(8).buffer);
keyReq.responseType = 'arraybuffer';
keyReq.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
keyReq2.responseType = 'arraybuffer';
keyReq2.respond(200, null, new Uint32Array([4, 5, 6, 7]).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
});
QUnit.test('encrypted init segment parse error', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
this.transmuxer = this.createTransmuxer();
// mock decrypting the init segment.
this.mockDecrypter.postMessage = (message) => {
// segment is 9, init is 8
if (message.encrypted.byteLength === 8) {
// Responding with a webm segment is something we do not
// support. so this will be an error.
message.encrypted.bytes = webmVideoInit().buffer;
} else {
message.encrypted.bytes = mp4Video();
}
message.encrypted.byteLength = message.encrypted.bytes.byteLength;
message.encrypted.byteOffset = 0;
return postMessage.call(this.mockDecrypter, message);
};
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-map.mp4',
key: {
resolvedUri: '1-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn: this.noop,
timingInfoFn: this.noop,
dataFn: this.noop,
progressFn: this.noop,
doneFn: (error, segmentData) => {
// decrypted webm init segment caused this error.
assert.ok(error, 'error for invalid init segment');
done();
}
});
assert.equal(this.requests.length, 4, 'there are four requests');
const keyReq = this.requests.shift();
const keyReq2 = this.requests.shift();
const mapReq = this.requests.shift();
const segmentReq = this.requests.shift();
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(keyReq2.uri, '1-key.php', 'the second request is for a key');
assert.equal(mapReq.uri, '0-map.mp4', 'the third request is for a map');
assert.equal(segmentReq.uri, '0-test.mp4', 'the forth request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(9).buffer);
mapReq.responseType = 'arraybuffer';
mapReq.respond(200, null, new Uint8Array(8).buffer);
keyReq.responseType = 'arraybuffer';
keyReq.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
keyReq2.responseType = 'arraybuffer';
keyReq2.respond(200, null, new Uint32Array([0, 1, 2, 3]).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
});
QUnit.test('encrypted init segment request failure', function(assert) {
const done = assert.async();
this.transmuxer = this.createTransmuxer();
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-map.mp4',
key: {
resolvedUri: '1-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn: this.noop,
timingInfoFn: this.noop,
dataFn: this.noop,
progressFn: this.noop,
doneFn: (error, segmentData) => {
assert.ok(error, 'errored');
this.requests.forEach(function(request) {
assert.ok(request.aborted, 'request aborted');
});
done();
}
});
assert.equal(this.requests.length, 4, 'there are four requests');
const keyReq = this.requests[0];
const keyReq2 = this.requests[1];
const mapReq = this.requests[2];
const segmentReq = this.requests[3];
assert.equal(keyReq.uri, '0-key.php', 'the first request is for a key');
assert.equal(keyReq2.uri, '1-key.php', 'the second request is for a key');
assert.equal(mapReq.uri, '0-map.mp4', 'the third request is for a map');
assert.equal(segmentReq.uri, '0-test.mp4', 'the forth request is for a segment');
mapReq.responseType = 'arraybuffer';
mapReq.respond(500, null, new Uint8Array(8).buffer);
// Allow the decrypter to decrypt
this.clock.tick(100);
});
QUnit.test('encrypted init segment with decrypted bytes not re-requested', function(assert) {
const done = assert.async();
const postMessage = this.mockDecrypter.postMessage;
this.transmuxer = this.createTransmuxer();
// mock decrypting the init segment.
this.mockDecrypter.postMessage = (message) => {
message.encrypted.bytes = mp4Video();
message.encrypted.byteLength = message.encrypted.bytes.byteLength;
message.encrypted.byteOffset = 0;
return postMessage.call(this.mockDecrypter, message);
};
let trackInfo;
const timingInfo = {};
let data;
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
decryptionWorker: this.mockDecrypter,
segment: {
transmuxer: this.transmuxer,
resolvedUri: '0-test.mp4',
key: {
resolvedUri: '0-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
},
map: {
resolvedUri: '0-map.mp4',
bytes: mp4VideoInit().buffer,
timescales: {
1: 30000
},
tracks: {
video: {
id: 1,
timescale: 30000,
type: 'video',
codec: 'avc1.64001e'
}
},
key: {
resolvedUri: '1-key.php',
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
}
},
trackInfoFn(segment, _trackInfo) {
trackInfo = _trackInfo;
},
timingInfoFn(segment, type, prop, value) {
timingInfo[type] = timingInfo[type] || {};
timingInfo[type][prop] = value;
},
dataFn(segment, _data) {
data = _data;
},
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, 9, '9 bytes');
assert.ok(data, 'got data');
assert.ok(trackInfo, 'got track info');
assert.ok(Object.keys(timingInfo).length, 'got timing info');
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.mp4', 'the second request is for a segment');
segmentReq.responseType = 'arraybuffer';
segmentReq.respond(200, null, new Uint8Array(9).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(
'waits for every request to finish before the callback is run',
function(assert) {

2
test/playback.test.js

@ -50,7 +50,7 @@ QUnit.module('Playback', {
// uncomment these lines when deugging
// videojs.log.level('debug');
this.fixture.style.position = 'inherit';
// this.fixture.style.position = 'inherit';
video.setAttribute('controls', '');
video.setAttribute('muted', '');

9
test/playlist-loader.test.js

@ -582,7 +582,10 @@ QUnit.module('Playlist Loader', function(hooks) {
uri: 'key-2-uri'
},
map: {
uri: 'map-2-uri'
uri: 'map-2-uri',
key: {
uri: 'key-map-uri'
}
}
}, {
duration: 11,
@ -619,6 +622,10 @@ QUnit.module('Playlist Loader', function(hooks) {
},
map: {
uri: 'map-2-uri',
key: {
uri: 'key-map-uri',
resolvedUri: urlTo('key-map-uri')
},
resolvedUri: urlTo('map-2-uri')
}
}, {

Loading…
Cancel
Save