Skip to content

Commit 97e02fb

Browse files
authored
test: fix IE11 encrypted VTT tests by using an actual encrypted VTT segment (#1291)
* Update script and docs for creating subtitlesEncrypted.vtt
1 parent 721e1bf commit 97e02fb

7 files changed

+213
-144
lines changed

docs/creating-content.md

+46
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,52 @@ $ mv init-stream0.webm webmVideoInit.webm
203203
$ mv chunk-stream0-00001.webm webmVideo.webm
204204
```
205205

206+
### subtitlesEncrypted.vtt
207+
208+
Run subtitles.vtt through subtle crypto. As an example:
209+
210+
```javascript
211+
const fs = require('fs');
212+
const { subtle } = require('crypto').webcrypto;
213+
214+
// first segment has media index 0, so should have the following IV
215+
const DEFAULT_IV = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
216+
217+
const getCryptoKey = async (bytes, iv = DEFAULT_IV) => {
218+
const algorithm = { name: 'AES-CBC', iv };
219+
const extractable = true;
220+
const usages = ['encrypt', 'decrypt'];
221+
222+
return subtle.importKey('raw', bytes, algorithm, extractable, usages);
223+
};
224+
225+
const run = async () => {
226+
const keyFilePath = process.argv[2];
227+
const segmentFilePath = process.argv[3];
228+
229+
const keyBytes = fs.readFileSync(keyFilePath);
230+
const segmentBytes = fs.readFileSync(segmentFilePath);
231+
232+
const key = await getCryptoKey(keyBytes);
233+
const encryptedBytes = await subtle.encrypt({
234+
name: 'AES-CBC',
235+
iv: DEFAULT_IV,
236+
}, key, segmentBytes);
237+
238+
fs.writeFileSync('./encrypted.vtt', new Buffer(encryptedBytes));
239+
240+
console.log(`Wrote ${encryptedBytes.length} bytes to encrypted.vtt:`);
241+
};
242+
243+
run();
244+
```
245+
246+
To use the script:
247+
248+
```
249+
$ node index.js encryptionKey.key subtitles.vtt
250+
```
251+
206252
## Other useful commands
207253

208254
### Joined (audio and video) initialization segment (for HLS)

scripts/create-test-data.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const getManifests = () => (fs.readdirSync(manifestsDir) || [])
2121
.map((f) => path.resolve(manifestsDir, f));
2222

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

2727
const buildManifestString = function() {

test/loader-common.js

+143-139
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import {
2424
muxed as muxedSegment,
2525
mp4Video as mp4VideoSegment,
2626
mp4VideoInit as mp4VideoInitSegment,
27-
videoOneSecond as tsVideoSegment,
28-
encrypted as encryptedSegment,
29-
encryptionKey
27+
videoOneSecond as tsVideoSegment
3028
} from 'create-test-data!segments';
3129

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

@@ -1503,6 +1506,142 @@ export const LoaderCommonFactory = ({
15031506
);
15041507
});
15051508

1509+
QUnit.module('Segment Key Caching');
1510+
1511+
QUnit.test('segmentKey will cache new encrypted keys with cacheEncryptionKeys true', function(assert) {
1512+
loader.cacheEncryptionKeys_ = true;
1513+
1514+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1515+
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
1516+
loader.load();
1517+
this.clock.tick(1);
1518+
1519+
const keyCache = loader.keyCache_;
1520+
const bytes = new Uint32Array([1, 2, 3, 4]);
1521+
1522+
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1523+
1524+
const result = loader.segmentKey({resolvedUri: 'key.php', bytes});
1525+
1526+
assert.deepEqual(result, {resolvedUri: 'key.php'}, 'gets by default');
1527+
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
1528+
assert.deepEqual(keyCache['key.php'].bytes, bytes, 'key has been cached');
1529+
});
1530+
});
1531+
1532+
QUnit.test('segmentKey will not cache encrypted keys with cacheEncryptionKeys false', function(assert) {
1533+
loader.cacheEncryptionKeys_ = false;
1534+
1535+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1536+
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
1537+
loader.load();
1538+
this.clock.tick(1);
1539+
1540+
const keyCache = loader.keyCache_;
1541+
const bytes = new Uint32Array([1, 2, 3, 4]);
1542+
1543+
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1544+
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
1545+
1546+
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1547+
});
1548+
});
1549+
1550+
QUnit.test('segment requests use cached keys when available', function(assert) {
1551+
loader.cacheEncryptionKeys_ = true;
1552+
1553+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1554+
return new Promise((resolve, reject) => {
1555+
loader.one('appended', resolve);
1556+
loader.one('error', reject);
1557+
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));
1558+
1559+
// make the keys the same
1560+
loader.playlist_.segments[1].key =
1561+
videojs.mergeOptions({}, loader.playlist_.segments[0].key);
1562+
// give 2nd key an iv
1563+
loader.playlist_.segments[1].key.iv = new Uint32Array([0, 1, 2, 3]);
1564+
1565+
loader.load();
1566+
this.clock.tick(1);
1567+
1568+
assert.strictEqual(this.requests.length, 2, 'one request');
1569+
assert.strictEqual(this.requests[0].uri, '0-key.php', 'key request');
1570+
assert.strictEqual(this.requests[1].uri, '0.ts', 'segment request');
1571+
1572+
// key response
1573+
standardXHRResponse(this.requests.shift(), encryptedSegmentKeyFn());
1574+
this.clock.tick(1);
1575+
1576+
// segment
1577+
standardXHRResponse(this.requests.shift(), encryptedSegmentFn());
1578+
this.clock.tick(1);
1579+
1580+
// decryption tick for syncWorker
1581+
this.clock.tick(1);
1582+
1583+
// tick for web worker segment probe
1584+
this.clock.tick(1);
1585+
});
1586+
}).then(() => {
1587+
assert.deepEqual(loader.keyCache_['0-key.php'], {
1588+
resolvedUri: '0-key.php',
1589+
bytes: new Uint32Array([609867320, 2355137646, 2410040447, 480344904])
1590+
}, 'previous key was cached');
1591+
1592+
this.clock.tick(1);
1593+
assert.deepEqual(loader.pendingSegment_.segment.key, {
1594+
resolvedUri: '0-key.php',
1595+
uri: '0-key.php',
1596+
iv: new Uint32Array([0, 1, 2, 3])
1597+
}, 'used cached key for request and own initialization vector');
1598+
1599+
assert.strictEqual(this.requests.length, 1, 'one request');
1600+
assert.strictEqual(this.requests[0].uri, '1.ts', 'only segment request');
1601+
});
1602+
});
1603+
1604+
QUnit.test('segment requests make key requests when key isn\'t cached', function(assert) {
1605+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1606+
return new Promise((resolve, reject) => {
1607+
loader.one('appended', resolve);
1608+
loader.one('error', reject);
1609+
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));
1610+
1611+
loader.load();
1612+
this.clock.tick(1);
1613+
1614+
assert.strictEqual(this.requests.length, 2, 'one request');
1615+
assert.strictEqual(this.requests[0].uri, '0-key.php', 'key request');
1616+
assert.strictEqual(this.requests[1].uri, '0.ts', 'segment request');
1617+
1618+
// key response
1619+
standardXHRResponse(this.requests.shift(), encryptedSegmentKeyFn());
1620+
this.clock.tick(1);
1621+
1622+
// segment
1623+
standardXHRResponse(this.requests.shift(), encryptedSegmentFn());
1624+
this.clock.tick(1);
1625+
1626+
// decryption tick for syncWorker
1627+
this.clock.tick(1);
1628+
});
1629+
}).then(() => {
1630+
this.clock.tick(1);
1631+
1632+
assert.notOk(loader.keyCache_['0-key.php'], 'not cached');
1633+
1634+
assert.deepEqual(loader.pendingSegment_.segment.key, {
1635+
resolvedUri: '1-key.php',
1636+
uri: '1-key.php'
1637+
}, 'used cached key for request and own initialization vector');
1638+
1639+
assert.strictEqual(this.requests.length, 2, 'two requests');
1640+
assert.strictEqual(this.requests[0].uri, '1-key.php', 'key request');
1641+
assert.strictEqual(this.requests[1].uri, '1.ts', 'segment request');
1642+
});
1643+
});
1644+
15061645
QUnit.module('Loading Calculation');
15071646

15081647
QUnit.test('requests the first segment with an empty buffer', function(assert) {
@@ -1695,140 +1834,5 @@ export const LoaderCommonFactory = ({
16951834

16961835
assert.notOk(loader.playlist_.syncInfo, 'did not set sync info on new playlist');
16971836
});
1698-
1699-
QUnit.test('segmentKey will cache new encrypted keys with cacheEncryptionKeys true', function(assert) {
1700-
loader.cacheEncryptionKeys_ = true;
1701-
1702-
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1703-
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
1704-
loader.load();
1705-
this.clock.tick(1);
1706-
1707-
const keyCache = loader.keyCache_;
1708-
const bytes = new Uint32Array([1, 2, 3, 4]);
1709-
1710-
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1711-
1712-
const result = loader.segmentKey({resolvedUri: 'key.php', bytes});
1713-
1714-
assert.deepEqual(result, {resolvedUri: 'key.php'}, 'gets by default');
1715-
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
1716-
assert.deepEqual(keyCache['key.php'].bytes, bytes, 'key has been cached');
1717-
});
1718-
});
1719-
1720-
QUnit.test('segmentKey will not cache encrypted keys with cacheEncryptionKeys false', function(assert) {
1721-
loader.cacheEncryptionKeys_ = false;
1722-
1723-
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1724-
loader.playlist(playlistWithDuration(10, { isEncrypted: true }));
1725-
loader.load();
1726-
this.clock.tick(1);
1727-
1728-
const keyCache = loader.keyCache_;
1729-
const bytes = new Uint32Array([1, 2, 3, 4]);
1730-
1731-
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1732-
loader.segmentKey({resolvedUri: 'key.php', bytes}, true);
1733-
1734-
assert.strictEqual(Object.keys(keyCache).length, 0, 'no keys have been cached');
1735-
});
1736-
});
1737-
1738-
QUnit.test('new segment requests will use cached keys', function(assert) {
1739-
loader.cacheEncryptionKeys_ = true;
1740-
1741-
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1742-
return new Promise((resolve, reject) => {
1743-
loader.one('appended', resolve);
1744-
loader.one('error', reject);
1745-
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));
1746-
1747-
// make the keys the same
1748-
loader.playlist_.segments[1].key =
1749-
videojs.mergeOptions({}, loader.playlist_.segments[0].key);
1750-
// give 2nd key an iv
1751-
loader.playlist_.segments[1].key.iv = new Uint32Array([0, 1, 2, 3]);
1752-
1753-
loader.load();
1754-
this.clock.tick(1);
1755-
1756-
assert.strictEqual(this.requests.length, 2, 'one request');
1757-
assert.strictEqual(this.requests[0].uri, '0-key.php', 'key request');
1758-
assert.strictEqual(this.requests[1].uri, '0.ts', 'segment request');
1759-
1760-
// key response
1761-
standardXHRResponse(this.requests.shift(), encryptionKey());
1762-
this.clock.tick(1);
1763-
1764-
// segment
1765-
standardXHRResponse(this.requests.shift(), encryptedSegment());
1766-
this.clock.tick(1);
1767-
1768-
// decryption tick for syncWorker
1769-
this.clock.tick(1);
1770-
1771-
// tick for web worker segment probe
1772-
this.clock.tick(1);
1773-
});
1774-
}).then(() => {
1775-
assert.deepEqual(loader.keyCache_['0-key.php'], {
1776-
resolvedUri: '0-key.php',
1777-
bytes: new Uint32Array([609867320, 2355137646, 2410040447, 480344904])
1778-
}, 'previous key was cached');
1779-
1780-
this.clock.tick(1);
1781-
assert.deepEqual(loader.pendingSegment_.segment.key, {
1782-
resolvedUri: '0-key.php',
1783-
uri: '0-key.php',
1784-
iv: new Uint32Array([0, 1, 2, 3])
1785-
}, 'used cached key for request and own initialization vector');
1786-
1787-
assert.strictEqual(this.requests.length, 1, 'one request');
1788-
assert.strictEqual(this.requests[0].uri, '1.ts', 'only segment request');
1789-
});
1790-
});
1791-
1792-
QUnit.test('new segment request keys every time', function(assert) {
1793-
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1794-
return new Promise((resolve, reject) => {
1795-
loader.one('appended', resolve);
1796-
loader.one('error', reject);
1797-
loader.playlist(playlistWithDuration(20, { isEncrypted: true }));
1798-
1799-
loader.load();
1800-
this.clock.tick(1);
1801-
1802-
assert.strictEqual(this.requests.length, 2, 'one request');
1803-
assert.strictEqual(this.requests[0].uri, '0-key.php', 'key request');
1804-
assert.strictEqual(this.requests[1].uri, '0.ts', 'segment request');
1805-
1806-
// key response
1807-
standardXHRResponse(this.requests.shift(), encryptionKey());
1808-
this.clock.tick(1);
1809-
1810-
// segment
1811-
standardXHRResponse(this.requests.shift(), encryptedSegment());
1812-
this.clock.tick(1);
1813-
1814-
// decryption tick for syncWorker
1815-
this.clock.tick(1);
1816-
1817-
});
1818-
}).then(() => {
1819-
this.clock.tick(1);
1820-
1821-
assert.notOk(loader.keyCache_['0-key.php'], 'not cached');
1822-
1823-
assert.deepEqual(loader.pendingSegment_.segment.key, {
1824-
resolvedUri: '1-key.php',
1825-
uri: '1-key.php'
1826-
}, 'used cached key for request and own initialization vector');
1827-
1828-
assert.strictEqual(this.requests.length, 2, 'two requests');
1829-
assert.strictEqual(this.requests[0].uri, '1-key.php', 'key request');
1830-
assert.strictEqual(this.requests[1].uri, '1.ts', 'segment request');
1831-
});
1832-
});
18331837
});
18341838
};

0 commit comments

Comments
 (0)