Skip to content

Commit

Permalink
feat(HLS): Add support for EXT-X-SESSION-KEY tag (#4655)
Browse files Browse the repository at this point in the history
Closes #917
  • Loading branch information
avelad authored Nov 8, 2022
1 parent 5bde080 commit 172c9f8
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 4 deletions.
57 changes: 53 additions & 4 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@ shaka.hls.HlsParser = class {
/** @type {!Array.<!shaka.hls.Tag>} */
const imageTags = Utils.filterTagsByName(
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
/** @type {!Array.<!shaka.hls.Tag>} */
const sessionKeyTags = Utils.filterTagsByName(
playlist.tags, 'EXT-X-SESSION-KEY');

this.parseCodecs_(variantTags);

Expand Down Expand Up @@ -702,7 +705,7 @@ shaka.hls.HlsParser = class {
// start time from audio/video streams and reuse for text streams.
this.createStreamInfosFromMediaTags_(mediaTags);
this.parseClosedCaptions_(mediaTags);
variants = this.createVariantsForTags_(variantTags);
variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
textStreams = this.parseTexts_(mediaTags);
imageStreams = await this.parseImages_(imageTags);
}
Expand Down Expand Up @@ -981,10 +984,43 @@ shaka.hls.HlsParser = class {

/**
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
* from the playlist.
* @return {!Array.<!shaka.extern.Variant>}
* @private
*/
createVariantsForTags_(tags) {
createVariantsForTags_(tags, sessionKeyTags) {
// EXT-X-SESSION-KEY processing
const drmInfos = [];
const keyIds = new Set();
if (sessionKeyTags.length > 0) {
for (const drmTag of sessionKeyTags) {
const method = drmTag.getRequiredAttrValue('METHOD');
if (method != 'NONE' && method != 'AES-128') {
// According to the HLS spec, KEYFORMAT is optional and implicitly
// defaults to "identity".
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
const keyFormat =
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
const drmParser =
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];

const drmInfo = drmParser ?
drmParser(drmTag, /* mimeType= */ '') : null;
if (drmInfo) {
if (drmInfo.keyIds) {
for (const keyId of drmInfo.keyIds) {
keyIds.add(keyId);
}
}
drmInfos.push(drmInfo);
} else {
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
}
}
}
}

// Create variants for each variant tag.
const allVariants = tags.map((tag) => {
const frameRate = tag.getAttributeValue('FRAME-RATE');
Expand All @@ -1009,7 +1045,9 @@ shaka.hls.HlsParser = class {
width,
height,
frameRate,
videoRange);
videoRange,
drmInfos,
keyIds);
});
let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
// Filter out null variants.
Expand Down Expand Up @@ -1251,11 +1289,14 @@ shaka.hls.HlsParser = class {
* @param {?string} height
* @param {?string} frameRate
* @param {?string} videoRange
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
* @param {!Set.<string>} keyIds
* @return {!Array.<!shaka.extern.Variant>}
* @private
*/
createVariants_(
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
drmInfos, keyIds) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const DrmEngine = shaka.media.DrmEngine;

Expand All @@ -1281,7 +1322,15 @@ shaka.hls.HlsParser = class {
for (const audioInfo of audioInfos) {
for (const videoInfo of videoInfos) {
const audioStream = audioInfo ? audioInfo.stream : null;
if (audioStream) {
audioStream.drmInfos = drmInfos;
audioStream.keyIds = keyIds;
}
const videoStream = videoInfo ? videoInfo.stream : null;
if (videoStream) {
videoStream.drmInfos = drmInfos;
videoStream.keyIds = keyIds;
}
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
const videoStreamUri =
Expand Down
153 changes: 153 additions & 0 deletions test/hls/hls_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2918,6 +2918,159 @@ describe('HlsParser', () => {
expect(newDrmInfoSpy).toHaveBeenCalled();
});

describe('constructs DrmInfo with EXT-X-SESSION-KEY', () => {
it('for Widevine', async () => {
const initDataBase64 =
'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE=';

const keyId = 'abc123';

const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYID=0X' + keyId + ',',
'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",',
'URI="data:text/plain;base64,',
initDataBase64, '",\n',
].join('');

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.addDrmInfo('com.widevine.alpha', (drmInfo) => {
drmInfo.addCencInitData(initDataBase64);
drmInfo.keyIds.add(keyId);
});
});
});
manifest.sequenceMode = true;
});

fakeNetEngine.setResponseText('test:/master', master);

const actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});

it('for PlayReady', async () => {
const initDataBase64 =
'AAAAKXBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAAlQbGF5cmVhZHk=';

const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYFORMAT="com.microsoft.playready",',
'URI="data:text/plain;base64,UGxheXJlYWR5",\n',
].join('');

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.addDrmInfo('com.microsoft.playready', (drmInfo) => {
drmInfo.addCencInitData(initDataBase64);
});
});
});
manifest.sequenceMode = true;
});

fakeNetEngine.setResponseText('test:/master', master);

const actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});

it('for FairPlay', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYFORMAT="com.apple.streamingkeydelivery",',
'URI="skd://f93d4e700d7ddde90529a27735d9e7cb",\n',
].join('');

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.addDrmInfo('com.apple.fps', (drmInfo) => {
drmInfo.addInitData('sinf', new Uint8Array(0));
});
});
});
manifest.sequenceMode = true;
});

fakeNetEngine.setResponseText('test:/master', master);

const actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});

it('for ClearKey with explicit KEYFORMAT', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYFORMAT="identity",',
'URI="key.bin",\n',
].join('');

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.addDrmInfo('org.w3.clearkey');
});
});
manifest.sequenceMode = true;
});

fakeNetEngine.setResponseText('test:/master', master);

const actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});

it('for ClearKey without explicit KEYFORMAT', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
'URI="key.bin",\n',
].join('');

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.addDrmInfo('org.w3.clearkey');
});
});
manifest.sequenceMode = true;
});

fakeNetEngine.setResponseText('test:/master', master);

const actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});
});

it('falls back to mp4 if HEAD request fails', async () => {
const master = [
'#EXTM3U\n',
Expand Down

0 comments on commit 172c9f8

Please sign in to comment.