Skip to content

Commit 172c9f8

Browse files
authored
feat(HLS): Add support for EXT-X-SESSION-KEY tag (#4655)
Closes #917
1 parent 5bde080 commit 172c9f8

File tree

2 files changed

+206
-4
lines changed

2 files changed

+206
-4
lines changed

lib/hls/hls_parser.js

+53-4
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,9 @@ shaka.hls.HlsParser = class {
670670
/** @type {!Array.<!shaka.hls.Tag>} */
671671
const imageTags = Utils.filterTagsByName(
672672
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
673+
/** @type {!Array.<!shaka.hls.Tag>} */
674+
const sessionKeyTags = Utils.filterTagsByName(
675+
playlist.tags, 'EXT-X-SESSION-KEY');
673676

674677
this.parseCodecs_(variantTags);
675678

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

982985
/**
983986
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
987+
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
988+
* from the playlist.
984989
* @return {!Array.<!shaka.extern.Variant>}
985990
* @private
986991
*/
987-
createVariantsForTags_(tags) {
992+
createVariantsForTags_(tags, sessionKeyTags) {
993+
// EXT-X-SESSION-KEY processing
994+
const drmInfos = [];
995+
const keyIds = new Set();
996+
if (sessionKeyTags.length > 0) {
997+
for (const drmTag of sessionKeyTags) {
998+
const method = drmTag.getRequiredAttrValue('METHOD');
999+
if (method != 'NONE' && method != 'AES-128') {
1000+
// According to the HLS spec, KEYFORMAT is optional and implicitly
1001+
// defaults to "identity".
1002+
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
1003+
const keyFormat =
1004+
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
1005+
const drmParser =
1006+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1007+
1008+
const drmInfo = drmParser ?
1009+
drmParser(drmTag, /* mimeType= */ '') : null;
1010+
if (drmInfo) {
1011+
if (drmInfo.keyIds) {
1012+
for (const keyId of drmInfo.keyIds) {
1013+
keyIds.add(keyId);
1014+
}
1015+
}
1016+
drmInfos.push(drmInfo);
1017+
} else {
1018+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1019+
}
1020+
}
1021+
}
1022+
}
1023+
9881024
// Create variants for each variant tag.
9891025
const allVariants = tags.map((tag) => {
9901026
const frameRate = tag.getAttributeValue('FRAME-RATE');
@@ -1009,7 +1045,9 @@ shaka.hls.HlsParser = class {
10091045
width,
10101046
height,
10111047
frameRate,
1012-
videoRange);
1048+
videoRange,
1049+
drmInfos,
1050+
keyIds);
10131051
});
10141052
let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
10151053
// Filter out null variants.
@@ -1251,11 +1289,14 @@ shaka.hls.HlsParser = class {
12511289
* @param {?string} height
12521290
* @param {?string} frameRate
12531291
* @param {?string} videoRange
1292+
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1293+
* @param {!Set.<string>} keyIds
12541294
* @return {!Array.<!shaka.extern.Variant>}
12551295
* @private
12561296
*/
12571297
createVariants_(
1258-
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
1298+
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1299+
drmInfos, keyIds) {
12591300
const ContentType = shaka.util.ManifestParserUtils.ContentType;
12601301
const DrmEngine = shaka.media.DrmEngine;
12611302

@@ -1281,7 +1322,15 @@ shaka.hls.HlsParser = class {
12811322
for (const audioInfo of audioInfos) {
12821323
for (const videoInfo of videoInfos) {
12831324
const audioStream = audioInfo ? audioInfo.stream : null;
1325+
if (audioStream) {
1326+
audioStream.drmInfos = drmInfos;
1327+
audioStream.keyIds = keyIds;
1328+
}
12841329
const videoStream = videoInfo ? videoInfo.stream : null;
1330+
if (videoStream) {
1331+
videoStream.drmInfos = drmInfos;
1332+
videoStream.keyIds = keyIds;
1333+
}
12851334
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
12861335
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
12871336
const videoStreamUri =

test/hls/hls_parser_unit.js

+153
Original file line numberDiff line numberDiff line change
@@ -2918,6 +2918,159 @@ describe('HlsParser', () => {
29182918
expect(newDrmInfoSpy).toHaveBeenCalled();
29192919
});
29202920

2921+
describe('constructs DrmInfo with EXT-X-SESSION-KEY', () => {
2922+
it('for Widevine', async () => {
2923+
const initDataBase64 =
2924+
'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE=';
2925+
2926+
const keyId = 'abc123';
2927+
2928+
const master = [
2929+
'#EXTM3U\n',
2930+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
2931+
'RESOLUTION=960x540,FRAME-RATE=60\n',
2932+
'video\n',
2933+
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
2934+
'KEYID=0X' + keyId + ',',
2935+
'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",',
2936+
'URI="data:text/plain;base64,',
2937+
initDataBase64, '",\n',
2938+
].join('');
2939+
2940+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
2941+
manifest.anyTimeline();
2942+
manifest.addPartialVariant((variant) => {
2943+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
2944+
stream.addDrmInfo('com.widevine.alpha', (drmInfo) => {
2945+
drmInfo.addCencInitData(initDataBase64);
2946+
drmInfo.keyIds.add(keyId);
2947+
});
2948+
});
2949+
});
2950+
manifest.sequenceMode = true;
2951+
});
2952+
2953+
fakeNetEngine.setResponseText('test:/master', master);
2954+
2955+
const actual = await parser.start('test:/master', playerInterface);
2956+
expect(actual).toEqual(manifest);
2957+
});
2958+
2959+
it('for PlayReady', async () => {
2960+
const initDataBase64 =
2961+
'AAAAKXBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAAlQbGF5cmVhZHk=';
2962+
2963+
const master = [
2964+
'#EXTM3U\n',
2965+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
2966+
'RESOLUTION=960x540,FRAME-RATE=60\n',
2967+
'video\n',
2968+
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
2969+
'KEYFORMAT="com.microsoft.playready",',
2970+
'URI="data:text/plain;base64,UGxheXJlYWR5",\n',
2971+
].join('');
2972+
2973+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
2974+
manifest.anyTimeline();
2975+
manifest.addPartialVariant((variant) => {
2976+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
2977+
stream.addDrmInfo('com.microsoft.playready', (drmInfo) => {
2978+
drmInfo.addCencInitData(initDataBase64);
2979+
});
2980+
});
2981+
});
2982+
manifest.sequenceMode = true;
2983+
});
2984+
2985+
fakeNetEngine.setResponseText('test:/master', master);
2986+
2987+
const actual = await parser.start('test:/master', playerInterface);
2988+
expect(actual).toEqual(manifest);
2989+
});
2990+
2991+
it('for FairPlay', async () => {
2992+
const master = [
2993+
'#EXTM3U\n',
2994+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
2995+
'RESOLUTION=960x540,FRAME-RATE=60\n',
2996+
'video\n',
2997+
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
2998+
'KEYFORMAT="com.apple.streamingkeydelivery",',
2999+
'URI="skd://f93d4e700d7ddde90529a27735d9e7cb",\n',
3000+
].join('');
3001+
3002+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
3003+
manifest.anyTimeline();
3004+
manifest.addPartialVariant((variant) => {
3005+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
3006+
stream.addDrmInfo('com.apple.fps', (drmInfo) => {
3007+
drmInfo.addInitData('sinf', new Uint8Array(0));
3008+
});
3009+
});
3010+
});
3011+
manifest.sequenceMode = true;
3012+
});
3013+
3014+
fakeNetEngine.setResponseText('test:/master', master);
3015+
3016+
const actual = await parser.start('test:/master', playerInterface);
3017+
expect(actual).toEqual(manifest);
3018+
});
3019+
3020+
it('for ClearKey with explicit KEYFORMAT', async () => {
3021+
const master = [
3022+
'#EXTM3U\n',
3023+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
3024+
'RESOLUTION=960x540,FRAME-RATE=60\n',
3025+
'video\n',
3026+
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
3027+
'KEYFORMAT="identity",',
3028+
'URI="key.bin",\n',
3029+
].join('');
3030+
3031+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
3032+
manifest.anyTimeline();
3033+
manifest.addPartialVariant((variant) => {
3034+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
3035+
stream.addDrmInfo('org.w3.clearkey');
3036+
});
3037+
});
3038+
manifest.sequenceMode = true;
3039+
});
3040+
3041+
fakeNetEngine.setResponseText('test:/master', master);
3042+
3043+
const actual = await parser.start('test:/master', playerInterface);
3044+
expect(actual).toEqual(manifest);
3045+
});
3046+
3047+
it('for ClearKey without explicit KEYFORMAT', async () => {
3048+
const master = [
3049+
'#EXTM3U\n',
3050+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
3051+
'RESOLUTION=960x540,FRAME-RATE=60\n',
3052+
'video\n',
3053+
'#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,',
3054+
'URI="key.bin",\n',
3055+
].join('');
3056+
3057+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
3058+
manifest.anyTimeline();
3059+
manifest.addPartialVariant((variant) => {
3060+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
3061+
stream.addDrmInfo('org.w3.clearkey');
3062+
});
3063+
});
3064+
manifest.sequenceMode = true;
3065+
});
3066+
3067+
fakeNetEngine.setResponseText('test:/master', master);
3068+
3069+
const actual = await parser.start('test:/master', playerInterface);
3070+
expect(actual).toEqual(manifest);
3071+
});
3072+
});
3073+
29213074
it('falls back to mp4 if HEAD request fails', async () => {
29223075
const master = [
29233076
'#EXTM3U\n',

0 commit comments

Comments
 (0)