Skip to content

Commit 54abee4

Browse files
committed
Parse EXT-X-SESSION-KEY tags
1 parent 0ac5902 commit 54abee4

File tree

9 files changed

+181
-113
lines changed

9 files changed

+181
-113
lines changed

api-extractor/report/hls.js.api.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ export class DateRange {
313313
export type DRMSystemOptions = {
314314
audioRobustness?: string;
315315
videoRobustness?: string;
316+
audioEncryptionScheme?: string | null;
317+
videoEncryptionScheme?: string | null;
316318
persistentState?: MediaKeysRequirement;
317319
distinctiveIdentifier?: MediaKeysRequirement;
318320
sessionTypes?: string[];
@@ -1525,6 +1527,8 @@ export class LevelKey implements DecryptData {
15251527
// (undocumented)
15261528
readonly isCommonEncryption: boolean;
15271529
// (undocumented)
1530+
isSupported(): boolean;
1531+
// (undocumented)
15281532
iv: Uint8Array | null;
15291533
// (undocumented)
15301534
key: Uint8Array | null;
@@ -1839,6 +1843,8 @@ export interface ManifestLoadedData {
18391843
// (undocumented)
18401844
sessionData: Record<string, AttrList> | null;
18411845
// (undocumented)
1846+
sessionKeys: LevelKey[] | null;
1847+
// (undocumented)
18421848
stats: LoaderStats;
18431849
// (undocumented)
18441850
subtitles?: MediaPlaylist[];
@@ -2283,20 +2289,20 @@ export interface UserdataSample {
22832289

22842290
// Warnings were encountered during analysis:
22852291
//
2286-
// src/config.ts:79:3 - (ae-forgotten-export) The symbol "MediaKeySessionContext" needs to be exported by the entry point hls.d.ts
2287-
// src/config.ts:94:3 - (ae-forgotten-export) The symbol "DRMSystemsConfiguration" needs to be exported by the entry point hls.d.ts
2288-
// src/config.ts:197:3 - (ae-forgotten-export) The symbol "ILogger" needs to be exported by the entry point hls.d.ts
2289-
// src/config.ts:207:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
2290-
// src/config.ts:208:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
2291-
// src/config.ts:210:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
2292-
// src/config.ts:211:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
2293-
// src/config.ts:212:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
2294-
// src/config.ts:214:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
2295-
// src/config.ts:217:3 - (ae-forgotten-export) The symbol "CMCDController" needs to be exported by the entry point hls.d.ts
2296-
// src/config.ts:219:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
2297-
// src/config.ts:220:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
2298-
// src/config.ts:221:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
2299-
// src/config.ts:222:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts
2292+
// src/config.ts:81:3 - (ae-forgotten-export) The symbol "MediaKeySessionContext" needs to be exported by the entry point hls.d.ts
2293+
// src/config.ts:96:3 - (ae-forgotten-export) The symbol "DRMSystemsConfiguration" needs to be exported by the entry point hls.d.ts
2294+
// src/config.ts:199:3 - (ae-forgotten-export) The symbol "ILogger" needs to be exported by the entry point hls.d.ts
2295+
// src/config.ts:209:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
2296+
// src/config.ts:210:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
2297+
// src/config.ts:212:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
2298+
// src/config.ts:213:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
2299+
// src/config.ts:214:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
2300+
// src/config.ts:216:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
2301+
// src/config.ts:219:3 - (ae-forgotten-export) The symbol "CMCDController" needs to be exported by the entry point hls.d.ts
2302+
// src/config.ts:221:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
2303+
// src/config.ts:222:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
2304+
// src/config.ts:223:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
2305+
// src/config.ts:224:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts
23002306

23012307
// (No @packageDocumentation comment for this package)
23022308

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export type CMCDControllerConfig = {
6060
export type DRMSystemOptions = {
6161
audioRobustness?: string;
6262
videoRobustness?: string;
63+
audioEncryptionScheme?: string | null;
64+
videoEncryptionScheme?: string | null;
6365
persistentState?: MediaKeysRequirement;
6466
distinctiveIdentifier?: MediaKeysRequirement;
6567
sessionTypes?: string[];

src/loader/level-key.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ export class LevelKey implements DecryptData {
5555
this.isCommonEncryption = this.encrypted && method !== 'AES-128';
5656
}
5757

58+
public isSupported(): boolean {
59+
// If it's Segment encryption or No encryption, just select that key system
60+
if (this.method) {
61+
if (this.method === 'AES-128' || this.method === 'NONE') {
62+
return true;
63+
}
64+
switch (this.keyFormat) {
65+
case 'identity':
66+
// Maintain support for clear SAMPLE-AES with MPEG-3 TS
67+
return this.method === 'SAMPLE-AES';
68+
case KeySystemFormats.FAIRPLAY:
69+
case KeySystemFormats.WIDEVINE:
70+
case KeySystemFormats.PLAYREADY:
71+
case KeySystemFormats.CLEARKEY:
72+
return (
73+
[
74+
'ISO-23001-7',
75+
'SAMPLE-AES',
76+
'SAMPLE-AES-CENC',
77+
'SAMPLE-AES-CTR',
78+
].indexOf(this.method) !== -1
79+
);
80+
}
81+
}
82+
return false;
83+
}
84+
5885
public getDecryptData(sn: number | 'initSegment'): LevelKey | null {
5986
if (!this.encrypted || !this.uri) {
6087
return null;

src/loader/m3u8-parser.ts

Lines changed: 70 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { DateRange } from './date-range';
44
import { Fragment, Part } from './fragment';
55
import { LevelDetails } from './level-details';
66
import { LevelKey } from './level-key';
7-
import { KeySystemFormats } from '../utils/mediakeys-helper';
8-
97
import { AttrList } from '../utils/attr-list';
108
import { logger } from '../utils/logger';
119
import type { CodecType } from '../utils/codecs';
@@ -20,9 +18,15 @@ import type { LevelAttributes, LevelParsed } from '../types/level';
2018

2119
type M3U8ParserFragments = Array<Fragment | null>;
2220

21+
type ParsedMultiVariantPlaylist = {
22+
levels: LevelParsed[];
23+
sessionData: Record<string, AttrList> | null;
24+
sessionKeys: LevelKey[] | null;
25+
};
26+
2327
// https://regex101.com is your friend
2428
const MASTER_PLAYLIST_REGEX =
25-
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+/g;
29+
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+|#EXT-X-SESSION-KEY:([^\n\r]*)[\r\n]+/g;
2630
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
2731

2832
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
@@ -84,10 +88,15 @@ export default class M3U8Parser {
8488
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
8589
}
8690

87-
static parseMasterPlaylist(string: string, baseurl: string) {
88-
const levels: Array<LevelParsed> = [];
89-
const levelsWithKnownCodecs: Array<LevelParsed> = [];
91+
static parseMasterPlaylist(
92+
string: string,
93+
baseurl: string
94+
): ParsedMultiVariantPlaylist {
95+
const levels: LevelParsed[] = [];
96+
const levelsWithKnownCodecs: LevelParsed[] = [];
9097
const sessionData: Record<string, AttrList> = {};
98+
const sessionKeys: LevelKey[] = [];
99+
91100
let hasSessionData = false;
92101
MASTER_PLAYLIST_REGEX.lastIndex = 0;
93102

@@ -132,6 +141,17 @@ export default class M3U8Parser {
132141
hasSessionData = true;
133142
sessionData[sessionAttrs['DATA-ID']] = sessionAttrs;
134143
}
144+
} else if (result[4]) {
145+
// '#EXT-X-SESSION-KEY' is found
146+
const keyTag = result[4];
147+
const sessionKey = parseKey(keyTag, baseurl);
148+
if (sessionKey.encrypted && sessionKey.isSupported()) {
149+
sessionKeys.push(sessionKey);
150+
} else {
151+
logger.warn(
152+
`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${keyTag}"`
153+
);
154+
}
135155
}
136156
}
137157
// Filter out levels with unknown codecs if it does not remove all levels
@@ -142,6 +162,7 @@ export default class M3U8Parser {
142162
return {
143163
levels: stripUnknownCodecLevels ? levelsWithKnownCodecs : levels,
144164
sessionData: hasSessionData ? sessionData : null,
165+
sessionKeys: sessionKeys.length ? sessionKeys : null,
145166
};
146167
}
147168

@@ -378,64 +399,21 @@ export default class M3U8Parser {
378399
discontinuityCounter = parseInt(value1);
379400
break;
380401
case 'KEY': {
381-
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
382-
const keyAttrs = new AttrList(value1);
383-
const decryptmethod = keyAttrs.enumeratedString('METHOD');
384-
const decrypturi = keyAttrs.URI;
385-
const decryptiv = keyAttrs.hexadecimalInteger('IV');
386-
const decryptkeyformatversions =
387-
keyAttrs.enumeratedString('KEYFORMATVERSIONS');
388-
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
389-
const decryptkeyformat =
390-
keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
391-
392-
if (
393-
!decryptmethod ||
394-
[
395-
'NONE',
396-
'AES-128',
397-
'ISO-23001-7',
398-
'SAMPLE-AES',
399-
'SAMPLE-AES-CENC',
400-
'SAMPLE-AES-CTR',
401-
].indexOf(decryptmethod) === -1
402-
) {
403-
logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`);
404-
} else {
405-
if (decrypturi && keyAttrs.IV && !decryptiv) {
406-
logger.error(`Invalid IV: ${keyAttrs.IV}`);
402+
const levelKey = parseKey(value1, baseurl);
403+
if (levelKey.isSupported()) {
404+
if (levelKey.method === 'NONE') {
405+
levelkeys = undefined;
406+
break;
407407
}
408-
// If decrypturi is a URI with a scheme, then baseurl will be ignored
409-
// No uri is allowed when METHOD is NONE
410-
const resolvedUri = decrypturi
411-
? M3U8Parser.resolve(decrypturi, baseurl)
412-
: '';
413-
const keyFormatVersions = (
414-
decryptkeyformatversions ? decryptkeyformatversions : '1'
415-
)
416-
.split('/')
417-
.map(Number)
418-
.filter(Number.isFinite);
419-
420-
if (isKeyTagSupported(decryptkeyformat, decryptmethod)) {
421-
if (decryptmethod === 'NONE') {
422-
levelkeys = undefined;
423-
break;
424-
}
425-
if (!levelkeys) {
426-
levelkeys = {};
427-
}
428-
if (levelkeys[decryptkeyformat]) {
429-
levelkeys = Object.assign({}, levelkeys);
430-
}
431-
levelkeys[decryptkeyformat] = new LevelKey(
432-
decryptmethod,
433-
resolvedUri,
434-
decryptkeyformat,
435-
keyFormatVersions,
436-
decryptiv
437-
);
408+
if (!levelkeys) {
409+
levelkeys = {};
438410
}
411+
if (levelkeys[levelKey.keyFormat]) {
412+
levelkeys = Object.assign({}, levelkeys);
413+
}
414+
levelkeys[levelKey.keyFormat] = levelKey;
415+
} else {
416+
logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`);
439417
}
440418
break;
441419
}
@@ -600,25 +578,37 @@ export default class M3U8Parser {
600578
}
601579
}
602580

603-
function isKeyTagSupported(
604-
decryptformat: string,
605-
decryptmethod: string
606-
): boolean {
607-
// If it's Segment encryption or No encryption, just select that key system
608-
if ('AES-128' === decryptmethod || 'NONE' === decryptmethod) {
609-
return true;
610-
}
611-
switch (decryptformat) {
612-
case 'identity':
613-
// maintain support for clear SAMPLE-AES with MPEG-3 TS
614-
return true;
615-
case KeySystemFormats.FAIRPLAY:
616-
case KeySystemFormats.WIDEVINE:
617-
case KeySystemFormats.PLAYREADY:
618-
case KeySystemFormats.CLEARKEY:
619-
return true;
581+
function parseKey(keyTag: string, baseurl: string): LevelKey {
582+
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
583+
const keyAttrs = new AttrList(keyTag);
584+
const decryptmethod = keyAttrs.enumeratedString('METHOD') ?? '';
585+
const decrypturi = keyAttrs.URI;
586+
const decryptiv = keyAttrs.hexadecimalInteger('IV');
587+
const decryptkeyformatversions =
588+
keyAttrs.enumeratedString('KEYFORMATVERSIONS');
589+
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
590+
const decryptkeyformat = keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
591+
592+
if (decrypturi && keyAttrs.IV && !decryptiv) {
593+
logger.error(`Invalid IV: ${keyAttrs.IV}`);
620594
}
621-
return false;
595+
// If decrypturi is a URI with a scheme, then baseurl will be ignored
596+
// No uri is allowed when METHOD is NONE
597+
const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : '';
598+
const keyFormatVersions = (
599+
decryptkeyformatversions ? decryptkeyformatversions : '1'
600+
)
601+
.split('/')
602+
.map(Number)
603+
.filter(Number.isFinite);
604+
605+
return new LevelKey(
606+
decryptmethod,
607+
resolvedUri,
608+
decryptkeyformat,
609+
keyFormatVersions,
610+
decryptiv
611+
);
622612
}
623613

624614
function setCodecs(codecs: Array<string>, level: LevelParsed) {

src/loader/playlist-loader.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,10 @@ class PlaylistLoader implements NetworkComponentAPI {
376376

377377
const url = getResponseUrl(response, context);
378378

379-
const { levels, sessionData } = M3U8Parser.parseMasterPlaylist(string, url);
379+
const { levels, sessionData, sessionKeys } = M3U8Parser.parseMasterPlaylist(
380+
string,
381+
url
382+
);
380383
if (!levels.length) {
381384
this.handleManifestParsingError(
382385
response,
@@ -457,6 +460,7 @@ class PlaylistLoader implements NetworkComponentAPI {
457460
stats,
458461
networkDetails,
459462
sessionData,
463+
sessionKeys,
460464
});
461465
}
462466

@@ -513,6 +517,7 @@ class PlaylistLoader implements NetworkComponentAPI {
513517
stats,
514518
networkDetails,
515519
sessionData: null,
520+
sessionKeys: null,
516521
});
517522
}
518523

src/types/events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { MetadataSample, UserdataSample } from './demuxer';
2222
import type { AttrList } from '../utils/attr-list';
2323
import type { HlsListeners } from '../events';
2424
import { KeyLoaderInfo } from '../loader/key-loader';
25+
import { LevelKey } from '../loader/level-key';
2526

2627
export interface MediaAttachingData {
2728
media: HTMLMediaElement;
@@ -83,6 +84,7 @@ export interface ManifestLoadedData {
8384
levels: LevelParsed[];
8485
networkDetails: any;
8586
sessionData: Record<string, AttrList> | null;
87+
sessionKeys: LevelKey[] | null;
8688
stats: LoaderStats;
8789
subtitles?: MediaPlaylist[];
8890
url: string;

src/utils/mediakeys-helper.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,22 +154,17 @@ function createMediaKeySystemConfigurations(
154154
sessionTypes: drmSystemOptions.sessionTypes || [
155155
drmSystemOptions.sessionType || 'temporary',
156156
],
157-
audioCapabilities: [],
158-
videoCapabilities: [],
159-
};
160-
161-
audioCodecs.forEach((codec) => {
162-
baseConfig.audioCapabilities!.push({
157+
audioCapabilities: audioCodecs.map((codec) => ({
163158
contentType: `audio/mp4; codecs="${codec}"`,
164159
robustness: drmSystemOptions.audioRobustness || '',
165-
});
166-
});
167-
videoCodecs.forEach((codec) => {
168-
baseConfig.videoCapabilities!.push({
160+
encryptionScheme: drmSystemOptions.audioEncryptionScheme || null,
161+
})),
162+
videoCapabilities: videoCodecs.map((codec) => ({
169163
contentType: `video/mp4; codecs="${codec}"`,
170164
robustness: drmSystemOptions.videoRobustness || '',
171-
});
172-
});
165+
encryptionScheme: drmSystemOptions.videoEncryptionScheme || null,
166+
})),
167+
};
173168

174169
return [baseConfig];
175170
}

0 commit comments

Comments
 (0)