Skip to content

Commit

Permalink
feat: parse key attributes for Widevine HLS (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-barstow authored and misteroneill committed Jun 25, 2019
1 parent f606b7c commit d835fa8
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import Stream from './stream';
import LineStream from './line-stream';
import ParseStream from './parse-stream';
import decodeB64ToUint8Array from './utils/decode';

/**
* A parser for M3U8 files. The current interpretation of the input is
Expand Down Expand Up @@ -49,6 +50,9 @@ export default class Parser extends Stream {
'CLOSED-CAPTIONS': {},
'SUBTITLES': {}
};
// This is the Widevine UUID from DASH IF IOP. The same exact string is
// used in MPDs with Widevine encrypted streams.
const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed';
// group segments into numbered timelines delineated by discontinuities
let currentTimeline = 0;

Expand Down Expand Up @@ -143,6 +147,55 @@ export default class Parser extends Stream {
});
return;
}

// check if the content is encrypted for Widevine
// Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
if (entry.attributes.KEYFORMAT === widevineUuid) {
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];

if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
this.trigger('warn', {
message: 'invalid key method provided for Widevine'
});
return;
}

if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
this.trigger('warn', {
message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
});
}

if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
this.trigger('warn', {
message: 'invalid key URI provided for Widevine'
});
return;
}

if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
this.trigger('warn', {
message: 'invalid key ID provided for Widevine'
});
return;
}

// if Widevine key attributes are valid, store them as `contentProtection`
// on the manifest to emulate Widevine tag structure in a DASH mpd
this.manifest.contentProtection = {
'com.widevine.alpha': {
attributes: {
schemeIdUri: entry.attributes.KEYFORMAT,
// remove '0x' from the key id string
keyId: entry.attributes.KEYID.substring(2)
},
// decode the base64-encoded PSSH box
pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1])
}
};
return;
}

if (!entry.attributes.METHOD) {
this.trigger('warn', {
message: 'defaulting key method to AES-128'
Expand Down
11 changes: 11 additions & 0 deletions src/utils/decode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import window from 'global/window';

export default function decodeB64ToUint8Array(b64Text) {
const decodedString = window.atob(b64Text || '');
const array = new Uint8Array(decodedString.length);

for (let i = 0; i < decodedString.length; i++) {
array[i] = decodedString.charCodeAt(i);
}
return array;
}
111 changes: 111 additions & 0 deletions test/m3u8.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,7 @@ QUnit.test('parses prefixed with 0x or 0X #EXT-X-KEY:IV tags', function(assert)
0x90abcdef
]), 'parsed an IV value with 0X');
});

// #EXT-X-START
QUnit.test('parses EXT-X-START tags', function(assert) {
const manifest = '#EXT-X-START:TIME-OFFSET=1.1\n';
Expand Down Expand Up @@ -1173,6 +1174,116 @@ QUnit.test('parses FORCED attribute', function(assert) {
'parsed FORCED attribute');
});

QUnit.test('parses Widevine #EXT-X-KEY attributes and attaches to manifest', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
'#EXTINF:5,',
'ex1.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.push(manifest);

assert.ok(parser.manifest.contentProtection, 'contentProtection property added');
assert.equal(
parser.manifest.contentProtection['com.widevine.alpha'].attributes.schemeIdUri,
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
'schemeIdUri set correctly'
);
assert.equal(
parser.manifest.contentProtection['com.widevine.alpha'].attributes.keyId,
'800AACAA522958AE888062B5695DB6BF',
'keyId set correctly'
);
assert.equal(
parser.manifest.contentProtection['com.widevine.alpha'].pssh.byteLength,
62,
'base64 URI decoded to TypedArray'
);
});

QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if METHOD is invalid', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#EXT-X-KEY:METHOD=NONE,' +
'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
'#EXTINF:5,',
'ex1.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.push(manifest);

assert.notOk(parser.manifest.contentProtection, 'contentProtection not added');
});

QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if URI is invalid', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
'URI="AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
'#EXTINF:5,',
'ex1.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.push(manifest);

assert.notOk(parser.manifest.contentProtection, 'contentProtection not added');
});

QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if KEYID is invalid', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=800AACAA522958AE888062B5695DB6BF,' +
'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
'#EXTINF:5,',
'ex1.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.push(manifest);

assert.notOk(parser.manifest.contentProtection, 'contentProtection not added');
});

QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if KEYFORMAT is not Widevine UUID', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
'KEYFORMATVERSIONS="1",KEYFORMAT="invalid-keyformat"',
'#EXTINF:5,',
'ex1.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.push(manifest);

assert.notOk(parser.manifest.contentProtection, 'contentProtection not added');
});

QUnit.module('m3u8s');

QUnit.test('parses static manifests as expected', function(assert) {
Expand Down

0 comments on commit d835fa8

Please sign in to comment.