Skip to content

Commit

Permalink
add unit and integration tests (#448)
Browse files Browse the repository at this point in the history
* add integration test for cached encryption key

* add unit tests

* adding segment-loader test

* adding some comments per CR and a negative case for cacheEncryptionKeys: false

* negative test for cacheEncryptionKeys:false
  • Loading branch information
ldayananda authored Mar 29, 2019
1 parent 13a749a commit 948a122
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 6 deletions.
4 changes: 2 additions & 2 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,6 @@ export default class SegmentLoader extends videojs.EventTarget {
const id = initSegmentId(map);
let storedMap = this.initSegments_[id];

// TODO: We should use the HTTP Expires header to invalidate our cache per
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
if (set && !storedMap && map.bytes) {
this.initSegments_[id] = storedMap = {
resolvedUri: map.resolvedUri,
Expand Down Expand Up @@ -380,6 +378,8 @@ export default class SegmentLoader extends videojs.EventTarget {
const id = segmentKeyId(key);
let storedKey = this.keyCache_[id];

// TODO: We should use the HTTP Expires header to invalidate our cache per
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) {
this.keyCache_[id] = storedKey = {
resolvedUri: key.resolvedUri,
Expand Down
4 changes: 4 additions & 0 deletions test/configuration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const options = [{
return `#FOO`;
}
}]
}, {
name: 'cacheEncryptionKeys',
default: false,
test: true
}];

const CONFIG_KEYS = Object.keys(Config);
Expand Down
35 changes: 35 additions & 0 deletions test/master-playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,41 @@ QUnit.test('creates appropriate PlaylistLoader for sourceType', function(assert)
'created a dash playlist loader');
});

QUnit.test('passes options to SegmentLoader', function(assert) {
const options = {
url: 'test',
tech: this.player.tech_
};

let controller = new MasterPlaylistController(options);

assert.notOk(controller.mainSegmentLoader_.bandwidth, "bandwidth won't be set by default");
assert.notOk(controller.mainSegmentLoader_.sourceType_, "sourceType won't be set by default");
assert.notOk(controller.mainSegmentLoader_.cacheEncryptionKeys_, "cacheEncryptionKeys won't be set by default");

controller = new MasterPlaylistController(Object.assign({
bandwidth: 3,
cacheEncryptionKeys: true,
sourceType: 'fake-type'
}, options));

assert.strictEqual(
controller.mainSegmentLoader_.bandwidth,
3,
'bandwidth will be set'
);
assert.strictEqual(
controller.mainSegmentLoader_.sourceType_,
'fake-type',
'sourceType will be set'
);
assert.strictEqual(
controller.mainSegmentLoader_.cacheEncryptionKeys_,
true,
'cacheEncryptionKeys will be set'
);
});

QUnit.test('resets SegmentLoader when seeking out of buffer',
function(assert) {
let resets = 0;
Expand Down
53 changes: 52 additions & 1 deletion test/media-segment-request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ QUnit.test('the key response is converted to the correct format', function(asser
QUnit.test('segment with key has bytes decrypted', function(assert) {
const done = assert.async();

assert.expect(8);
mediaSegmentRequest(
this.xhr,
this.xhrOptions,
Expand All @@ -313,6 +312,12 @@ QUnit.test('segment with key has bytes decrypted', function(assert) {
(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');
Expand All @@ -336,6 +341,52 @@ QUnit.test('segment with key has bytes decrypted', function(assert) {
this.clock.tick(100);
});

QUnit.test('segment with key bytes does not request key again', function(assert) {
const done = assert.async();

mediaSegmentRequest(
this.xhr,
this.xhrOptions,
this.realDecrypter,
this.noop,
{
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
bytes: new Uint32Array([0, 2, 3, 1]),
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
this.noop,
(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.response = new Uint8Array(8).buffer;
segmentReq.respond(200, null, '');

// Allow the decrypter to decrypt
this.clock.tick(100);
});

QUnit.test('waits for every request to finish before the callback is run',
function(assert) {
const done = assert.async();
Expand Down
143 changes: 142 additions & 1 deletion test/segment-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import videojs from 'video.js';
import mp4probe from 'mux.js/lib/mp4/probe';
import {
playlistWithDuration,
MockTextTrack
MockTextTrack,
standardXHRResponse
} from './test-helpers.js';
import {
LoaderCommonHooks,
Expand Down Expand Up @@ -251,6 +252,146 @@ QUnit.module('SegmentLoader: M2TS', function(hooks) {
'segment end time not shifted by mp4 start time');
});

QUnit.test('segmentKey will cache new encrypted keys with cacheEncryptionKeys true', function(assert) {
const newLoader = new SegmentLoader(LoaderCommonSettings.call(this, {
loaderType: 'main',
segmentMetadataTrack: this.segmentMetadataTrack,
cacheEncryptionKeys: true
}), {});

newLoader.playlist(playlistWithDuration(10), { isEncrypted: true });
newLoader.mimeType(this.mimeType);
newLoader.load();
this.clock.tick(1);

assert.strictEqual(
Object.keys(newLoader.keyCache_).length,
0,
'no keys have been cached'
);

const result = newLoader.segmentKey({
resolvedUri: 'key.php',
bytes: new Uint32Array([1, 2, 3, 4])
});

assert.deepEqual(
result,
{ resolvedUri: 'key.php' },
'gets by default'
);

newLoader.segmentKey(
{
resolvedUri: 'key.php',
bytes: new Uint32Array([1, 2, 3, 4])
},
true
);

assert.deepEqual(
newLoader.keyCache_['key.php'].bytes,
new Uint32Array([1, 2, 3, 4]),
'key has been cached'
);
});

QUnit.test('segmentKey will not cache encrypted keys with cacheEncryptionKeys false', function(assert) {
const newLoader = new SegmentLoader(LoaderCommonSettings.call(this, {
loaderType: 'main',
segmentMetadataTrack: this.segmentMetadataTrack,
cacheEncryptionKeys: false
}), {});

newLoader.playlist(playlistWithDuration(10), { isEncrypted: true });
newLoader.mimeType(this.mimeType);
newLoader.load();
this.clock.tick(1);

assert.strictEqual(
Object.keys(newLoader.keyCache_).length,
0,
'no keys have been cached'
);

newLoader.segmentKey(
{
resolvedUri: 'key.php',
bytes: new Uint32Array([1, 2, 3, 4])
},
// set = true
true
);

assert.strictEqual(
Object.keys(newLoader.keyCache_).length,
0,
'no keys have been cached since cacheEncryptionKeys is false'
);
});

QUnit.test('new segment requests will use cached keys', function(assert) {
const done = assert.async();
const newLoader = new SegmentLoader(LoaderCommonSettings.call(this, {
loaderType: 'main',
segmentMetadataTrack: this.segmentMetadataTrack,
cacheEncryptionKeys: true
}), {});

newLoader.playlist(playlistWithDuration(20, { isEncrypted: true }));
// make the keys the same
newLoader.playlist_.segments[1].key =
videojs.mergeOptions({}, newLoader.playlist_.segments[0].key);
// give 2nd key an iv
newLoader.playlist_.segments[1].key.iv = new Uint32Array([0, 1, 2, 3]);

newLoader.mimeType(this.mimeType);
newLoader.load();
this.clock.tick(1);

assert.strictEqual(this.requests.length, 2, 'two requests');
assert.strictEqual(this.requests[0].uri, '0-key.php', 'key request');
assert.strictEqual(this.requests[1].uri, '0.ts', 'segment request');

// key response
standardXHRResponse(this.requests.shift(), new Uint32Array([1, 1, 1, 1]));
this.clock.tick(1);
// segment
standardXHRResponse(this.requests.shift(), new Uint32Array([1, 5, 0, 1]));
this.clock.tick(1);

// As the Decrypter is in a web worker, the last function in SegmentLoader is
// the easiest way to listen for the decrypted response
const origHandleSegment = newLoader.handleSegment_.bind(newLoader);

newLoader.handleSegment_ = () => {
origHandleSegment();
this.updateend();
assert.deepEqual(
newLoader.keyCache_['0-key.php'],
{
resolvedUri: '0-key.php',
bytes: new Uint32Array([16777216, 16777216, 16777216, 16777216])
},
'previous key was cached');

this.clock.tick(1);
assert.deepEqual(
newLoader.pendingSegment_.segment.key,
{
resolvedUri: '0-key.php',
uri: '0-key.php',
iv: new Uint32Array([0, 1, 2, 3])
},
'used cached key for request and own initialization vector'
);

assert.strictEqual(this.requests.length, 1, 'one request');
assert.strictEqual(this.requests[0].uri, '1.ts', 'only segment request');
done();
};
});

QUnit.test('triggers syncinfoupdate before attempting a resync', function(assert) {
let syncInfoUpdates = 0;

Expand Down
8 changes: 6 additions & 2 deletions test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,18 +358,22 @@ export const standardXHRResponse = function(request, data) {
contentType = 'video/MP2T';
} else if (/\.mpd/.test(request.url)) {
contentType = 'application/dash+xml';
} else if (request.responseType === 'arraybuffer') {
contentType = 'binary/octet-stream';
}

if (!data) {
data = testDataManifests[manifestName];
}

const isTypedBuffer = data instanceof Uint8Array || data instanceof Uint32Array;

request.response =
// if segment data was passed, use that, otherwise use a placeholder
data instanceof Uint8Array ? data.buffer : new Uint8Array(1024).buffer;
isTypedBuffer ? data.buffer : new Uint8Array(1024).buffer;
request.respond(200,
{ 'Content-Type': contentType },
data instanceof Uint8Array ? '' : data);
isTypedBuffer ? '' : data);
};

export const playlistWithDuration = function(time, conf) {
Expand Down
Loading

0 comments on commit 948a122

Please sign in to comment.