Skip to content

Commit

Permalink
test: fix IE11 encrypted VTT tests by using an actual encrypted VTT s…
Browse files Browse the repository at this point in the history
…egment (#1291)

* Update script and docs for creating subtitlesEncrypted.vtt
  • Loading branch information
gesinger authored and misteroneill committed Aug 19, 2022
1 parent 66a5b17 commit 57c0e72
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 144 deletions.
46 changes: 46 additions & 0 deletions docs/creating-content.md
Expand Up @@ -203,6 +203,52 @@ $ mv init-stream0.webm webmVideoInit.webm
$ mv chunk-stream0-00001.webm webmVideo.webm
```

### subtitlesEncrypted.vtt

Run subtitles.vtt through subtle crypto. As an example:

```javascript
const fs = require('fs');
const { subtle } = require('crypto').webcrypto;

// first segment has media index 0, so should have the following IV
const DEFAULT_IV = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

const getCryptoKey = async (bytes, iv = DEFAULT_IV) => {
const algorithm = { name: 'AES-CBC', iv };
const extractable = true;
const usages = ['encrypt', 'decrypt'];

return subtle.importKey('raw', bytes, algorithm, extractable, usages);
};

const run = async () => {
const keyFilePath = process.argv[2];
const segmentFilePath = process.argv[3];

const keyBytes = fs.readFileSync(keyFilePath);
const segmentBytes = fs.readFileSync(segmentFilePath);

const key = await getCryptoKey(keyBytes);
const encryptedBytes = await subtle.encrypt({
name: 'AES-CBC',
iv: DEFAULT_IV,
}, key, segmentBytes);

fs.writeFileSync('./encrypted.vtt', new Buffer(encryptedBytes));

console.log(`Wrote ${encryptedBytes.length} bytes to encrypted.vtt:`);
};

run();
```

To use the script:

```
$ node index.js encryptionKey.key subtitles.vtt
```

## Other useful commands

### Joined (audio and video) initialization segment (for HLS)
Expand Down
2 changes: 1 addition & 1 deletion scripts/create-test-data.js
Expand Up @@ -21,7 +21,7 @@ const getManifests = () => (fs.readdirSync(manifestsDir) || [])
.map((f) => path.resolve(manifestsDir, f));

const getSegments = () => (fs.readdirSync(segmentsDir) || [])
.filter((f) => ((/\.(ts|mp4|key|webm|aac|ac3)/).test(path.extname(f))))
.filter((f) => ((/\.(ts|mp4|key|webm|aac|ac3|vtt)/).test(path.extname(f))))
.map((f) => path.resolve(segmentsDir, f));

const buildManifestString = function() {
Expand Down
282 changes: 143 additions & 139 deletions test/loader-common.js
Expand Up @@ -24,9 +24,7 @@ import {
muxed as muxedSegment,
mp4Video as mp4VideoSegment,
mp4VideoInit as mp4VideoInitSegment,
videoOneSecond as tsVideoSegment,
encrypted as encryptedSegment,
encryptionKey
videoOneSecond as tsVideoSegment
} from 'create-test-data!segments';

/**
Expand Down Expand Up @@ -133,7 +131,12 @@ export const LoaderCommonFactory = ({
loaderBeforeEach,
usesAsyncAppends = true,
initSegments = true,
testData = muxedSegment
testData = muxedSegment,
// These need to be functions. If you use a value alone, the bytes may be cleared out
// after decrypting, leaving an empty segment/key. This usage is consistent with other
// segments used in tests.
encryptedSegmentFn,
encryptedSegmentKeyFn
}) => {
let loader;

Expand Down Expand Up @@ -1503,6 +1506,142 @@ export const LoaderCommonFactory = ({
);
});

QUnit.module('Segment Key Caching');

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('segment requests use cached keys when available', 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(), encryptedSegmentKeyFn());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegmentFn());
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('segment requests make key requests when key isn\'t cached', 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(), encryptedSegmentKeyFn());
this.clock.tick(1);

// segment
standardXHRResponse(this.requests.shift(), encryptedSegmentFn());
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.module('Loading Calculation');

QUnit.test('requests the first segment with an empty buffer', function(assert) {
Expand Down Expand Up @@ -1695,140 +1834,5 @@ 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');
});
});
});
};

0 comments on commit 57c0e72

Please sign in to comment.