Skip to content

Commit

Permalink
fix: cache aes keys for text tracks (#973) (#1228)
Browse files Browse the repository at this point in the history
* fix: cache aes keys for text tracks (#973)

* Move encryption key caching tests to common loader tests to cover VTT loader

Co-authored-by: Garrett Singer <gesinger@gmail.com>
  • Loading branch information
phloxic and gesinger authored May 26, 2022
1 parent 5223427 commit 721e1bf
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 138 deletions.
5 changes: 5 additions & 0 deletions src/vtt-segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ export default class VTTSegmentLoader extends SegmentLoader {
// maintain functionality between segment loaders
this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);

// if this request included a segment key, save that data in the cache
if (simpleSegment.key) {
this.segmentKey(simpleSegment.key, true);
}

this.state = 'APPENDING';

// used for tests
Expand Down
139 changes: 138 additions & 1 deletion test/loader-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {
muxed as muxedSegment,
mp4Video as mp4VideoSegment,
mp4VideoInit as mp4VideoInitSegment,
videoOneSecond as tsVideoSegment
videoOneSecond as tsVideoSegment,
encrypted as encryptedSegment,
encryptionKey
} from 'create-test-data!segments';

/**
Expand Down Expand Up @@ -1693,5 +1695,140 @@ export const LoaderCommonFactory = ({

assert.notOk(loader.playlist_.syncInfo, 'did not set sync info on new playlist');
});

QUnit.test('segmentKey will cache new encrypted keys with cacheEncryptionKeys true', function(assert) {
loader.cacheEncryptionKeys_ = true;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
loader.load();
this.clock.tick(1);

const keyCache = loader.keyCache_;
const bytes = new Uint32Array([1, 2, 3, 4]);

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

const result = loader.segmentKey({resolvedUri: 'key.php', bytes});

assert.deepEqual(result, {resolvedUri: 'key.php'}, 'gets by default');
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
assert.deepEqual(keyCache['key.php'].bytes, bytes, 'key has been cached');
});
});

QUnit.test('segmentKey will not cache encrypted keys with cacheEncryptionKeys false', function(assert) {
loader.cacheEncryptionKeys_ = false;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
loader.load();
this.clock.tick(1);

const keyCache = loader.keyCache_;
const bytes = new Uint32Array([1, 2, 3, 4]);

assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);

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

QUnit.test('new segment requests will use cached keys', function(assert) {
loader.cacheEncryptionKeys_ = true;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
return new Promise((resolve, reject) => {
loader.one('appended', resolve);
loader.one('error', reject);
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));

// make the keys the same
loader.playlist_.segments[1].key =
videojs.mergeOptions({}, loader.playlist_.segments[0].key);
// give 2nd key an iv
loader.playlist_.segments[1].key.iv = new Uint32Array([0, 1, 2, 3]);

loader.load();
this.clock.tick(1);

assert.strictEqual(this.requests.length, 2, 'one request');
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(), encryptionKey());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegment());
this.clock.tick(1);

// decryption tick for syncWorker
this.clock.tick(1);

// tick for web worker segment probe
this.clock.tick(1);
});
}).then(() => {
assert.deepEqual(loader.keyCache_['0-key.php'], {
resolvedUri: '0-key.php',
bytes: new Uint32Array([609867320, 2355137646, 2410040447, 480344904])
}, 'previous key was cached');

this.clock.tick(1);
assert.deepEqual(loader.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');
});
});

QUnit.test('new segment request keys every time', function(assert) {
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
return new Promise((resolve, reject) => {
loader.one('appended', resolve);
loader.one('error', reject);
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));

loader.load();
this.clock.tick(1);

assert.strictEqual(this.requests.length, 2, 'one request');
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(), encryptionKey());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegment());
this.clock.tick(1);

// decryption tick for syncWorker
this.clock.tick(1);

});
}).then(() => {
this.clock.tick(1);

assert.notOk(loader.keyCache_['0-key.php'], 'not cached');

assert.deepEqual(loader.pendingSegment_.segment.key, {
resolvedUri: '1-key.php',
uri: '1-key.php'
}, 'used cached key for request and own initialization vector');

assert.strictEqual(this.requests.length, 2, 'two requests');
assert.strictEqual(this.requests[0].uri, '1-key.php', 'key request');
assert.strictEqual(this.requests[1].uri, '1.ts', 'segment request');
});
});
});
};
137 changes: 0 additions & 137 deletions test/segment-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ import {
mp4VideoInit as mp4VideoInitSegment,
mp4Audio as mp4AudioSegment,
mp4AudioInit as mp4AudioInitSegment,
encrypted as encryptedSegment,
encryptionKey,
zeroLength as zeroLengthSegment
} from 'create-test-data!segments';
import sinon from 'sinon';
Expand Down Expand Up @@ -1433,141 +1431,6 @@ QUnit.module('SegmentLoader', function(hooks) {
});
});

QUnit.test('segmentKey will cache new encrypted keys with cacheEncryptionKeys true', function(assert) {
loader.cacheEncryptionKeys_ = true;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
loader.load();
this.clock.tick(1);

const keyCache = loader.keyCache_;
const bytes = new Uint32Array([1, 2, 3, 4]);

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

const result = loader.segmentKey({resolvedUri: 'key.php', bytes});

assert.deepEqual(result, {resolvedUri: 'key.php'}, 'gets by default');
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
assert.deepEqual(keyCache['key.php'].bytes, bytes, 'key has been cached');
});
});

QUnit.test('segmentKey will not cache encrypted keys with cacheEncryptionKeys false', function(assert) {
loader.cacheEncryptionKeys_ = false;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
loader.load();
this.clock.tick(1);

const keyCache = loader.keyCache_;
const bytes = new Uint32Array([1, 2, 3, 4]);

assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);

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

QUnit.test('new segment requests will use cached keys', function(assert) {
loader.cacheEncryptionKeys_ = true;

return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
return new Promise((resolve, reject) => {
loader.one('appended', resolve);
loader.one('error', reject);
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));

// make the keys the same
loader.playlist_.segments[1].key =
videojs.mergeOptions({}, loader.playlist_.segments[0].key);
// give 2nd key an iv
loader.playlist_.segments[1].key.iv = new Uint32Array([0, 1, 2, 3]);

loader.load();
this.clock.tick(1);

assert.strictEqual(this.requests.length, 2, 'one request');
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(), encryptionKey());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegment());
this.clock.tick(1);

// decryption tick for syncWorker
this.clock.tick(1);

// tick for web worker segment probe
this.clock.tick(1);
});
}).then(() => {
assert.deepEqual(loader.keyCache_['0-key.php'], {
resolvedUri: '0-key.php',
bytes: new Uint32Array([609867320, 2355137646, 2410040447, 480344904])
}, 'previous key was cached');

this.clock.tick(1);
assert.deepEqual(loader.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');
});
});

QUnit.test('new segment request keys every time', function(assert) {
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
return new Promise((resolve, reject) => {
loader.one('appended', resolve);
loader.one('error', reject);
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));

loader.load();
this.clock.tick(1);

assert.strictEqual(this.requests.length, 2, 'one request');
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(), encryptionKey());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegment());
this.clock.tick(1);

// decryption tick for syncWorker
this.clock.tick(1);

});
}).then(() => {
this.clock.tick(1);

assert.notOk(loader.keyCache_['0-key.php'], 'not cached');

assert.deepEqual(loader.pendingSegment_.segment.key, {
resolvedUri: '1-key.php',
uri: '1-key.php'
}, 'used cached key for request and own initialization vector');

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

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

Expand Down

0 comments on commit 721e1bf

Please sign in to comment.