Skip to content

Commit

Permalink
feat(HLS): Add automatically keyId-key for identity format
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Feb 28, 2024
1 parent 543dafc commit 7741dcd
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 59 deletions.
1 change: 1 addition & 0 deletions karma.conf.js
Expand Up @@ -241,6 +241,7 @@ module.exports = (config) => {
{pattern: 'test/test/assets/hls-raw-ac3/*', included: false},
{pattern: 'test/test/assets/hls-raw-ec3/*', included: false},
{pattern: 'test/test/assets/hls-raw-mp3/*', included: false},
{pattern: 'test/test/assets/hls-sample-aes/*', included: false},
{pattern: 'test/test/assets/hls-ts-aac/*', included: false},
{pattern: 'test/test/assets/hls-ts-ac3/*', included: false},
{pattern: 'test/test/assets/hls-ts-ec3/*', included: false},
Expand Down
178 changes: 123 additions & 55 deletions lib/hls/hls_parser.js
Expand Up @@ -437,7 +437,8 @@ shaka.hls.HlsParser = class {

const mediaSequenceToStartTime =
this.getMediaSequenceToStartTimeFor_(streamInfo);
const {keyIds, drmInfos} = this.parseDrmInfo_(playlist, stream.mimeType);
const {keyIds, drmInfos} = await this.parseDrmInfo_(
playlist, stream.mimeType, streamInfo.getUris, mediaVariables);

const keysAreEqual =
(a, b) => a.size === b.size && [...a].every((value) => b.has(value));
Expand Down Expand Up @@ -719,12 +720,12 @@ shaka.hls.HlsParser = class {
/** @type {!Array.<!shaka.extern.Stream>} */
let imageStreams = [];

const getUris = () => {
return [uri];
};

// Parsing a media playlist results in a single-variant stream.
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
const getUris = () => {
return [uri];
};

// Get necessary info for this stream. These are things we would normally
// find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
const basicInfo =
Expand Down Expand Up @@ -806,8 +807,9 @@ shaka.hls.HlsParser = class {
this.parseCodecs_(variantTags);

this.parseClosedCaptions_(mediaTags);
variants =
this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
variants = await this.createVariantsForTags_(
variantTags, sessionKeyTags, mediaTags, getUris,
this.globalVariables_);
textStreams = this.parseTexts_(mediaTags);
imageStreams = await this.parseImages_(imageTags, iFrameTags);
}
Expand Down Expand Up @@ -1314,37 +1316,48 @@ shaka.hls.HlsParser = class {
* from the playlist.
* @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
* playlist.
* @return {!Array.<!shaka.extern.Variant>}
* @param {function():!Array.<string>} getUris
* @param {?Map.<string, string>=} variables
* @return {!Promise.<!Array.<!shaka.extern.Variant>>}
* @private
*/
createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
async createVariantsForTags_(tags, sessionKeyTags, mediaTags, getUris,
variables) {
// 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' && !this.isAesMethod_(method)) {
// 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';
// 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';
let drmInfo = null;
if (method == 'NONE' || this.isAesMethod_(method)) {
continue;
} else if (keyFormat == 'identity') {
// eslint-disable-next-line no-await-in-loop
drmInfo = await this.identityDrmParser_(
drmTag, /* mimeType= */ '', getUris,
/* initSegmentRef= */ null, variables);
} else {
const drmParser =
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];

const drmInfo = drmParser ?
drmInfo = drmParser ?
drmParser(drmTag, /* mimeType= */ '') : null;
if (drmInfo) {
if (drmInfo.keyIds) {
for (const keyId of drmInfo.keyIds) {
keyIds.add(keyId);
}
}
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);
}
drmInfos.push(drmInfo);
} else {
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
}
}
}
Expand Down Expand Up @@ -2506,7 +2519,7 @@ shaka.hls.HlsParser = class {
}

const {drmInfos, keyIds, encrypted, aesEncrypted} =
this.parseDrmInfo_(playlist, mimeType);
await this.parseDrmInfo_(playlist, mimeType, getUris, mediaVariables);

if (encrypted && !drmInfos.length && !aesEncrypted) {
throw new shaka.util.Error(
Expand Down Expand Up @@ -2721,22 +2734,31 @@ shaka.hls.HlsParser = class {
/**
* @param {!shaka.hls.Playlist} playlist
* @param {string} mimeType
* @return {{
* @param {function():!Array.<string>} getUris
* @param {?Map.<string, string>=} variables
* @return {Promise.<{
* drmInfos: !Array.<shaka.extern.DrmInfo>,
* keyIds: !Set.<string>,
* encrypted: boolean,
* aesEncrypted: boolean
* }}
* }>}
* @private
*/
parseDrmInfo_(playlist, mimeType) {
/** @type {!Array.<!shaka.hls.Tag>} */
const drmTags = [];
async parseDrmInfo_(playlist, mimeType, getUris, variables) {
/** @type {!Map<!shaka.hls.Tag, ?shaka.media.InitSegmentReference>} */
const drmTagsMap = new Map();
if (playlist.segments) {
for (const segment of playlist.segments) {
const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
'EXT-X-KEY');
drmTags.push(...segmentKeyTags);
let initSegmentRef = null;
if (segmentKeyTags.length) {
initSegmentRef = this.getInitSegmentReference_(playlist,
segment.tags, getUris, variables);
for (const segmentKeyTag of segmentKeyTags) {
drmTagsMap.set(segmentKeyTag, initSegmentRef);
}
}
}
}

Expand All @@ -2747,34 +2769,42 @@ shaka.hls.HlsParser = class {
const drmInfos = [];
const keyIds = new Set();

for (const drmTag of drmTags) {
for (const [key, value] of drmTagsMap) {
const drmTag = /** @type {!shaka.hls.Tag} */ (key);
const initSegmentRef =
/** @type {?shaka.media.InitSegmentReference} */ (value);
const method = drmTag.getRequiredAttrValue('METHOD');
if (method != 'NONE') {
encrypted = true;

// 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';
let drmInfo = null;

if (this.isAesMethod_(method)) {
// These keys are handled separately.
aesEncrypted = true;
} else if (keyFormat == 'identity') {
// eslint-disable-next-line no-await-in-loop
drmInfo = await this.identityDrmParser_(
drmTag, mimeType, getUris, initSegmentRef, variables);
} else {
// 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);
}
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);
}
drmInfos.push(drmInfo);
} else {
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
}
}
}
Expand Down Expand Up @@ -4152,10 +4182,15 @@ shaka.hls.HlsParser = class {
* See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
*
* @param {!shaka.hls.Tag} drmTag
* @return {?shaka.extern.DrmInfo}
* @param {string} mimeType
* @param {function():!Array.<string>} getUris
* @param {?shaka.media.InitSegmentReference} initSegmentRef
* @param {?Map.<string, string>=} variables
* @return {!Promise.<?shaka.extern.DrmInfo>}
* @private
*/
static identityDrmParser_(drmTag, mimeType) {
async identityDrmParser_(drmTag, mimeType, getUris, initSegmentRef,
variables) {
if (mimeType == 'video/mp2t') {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
Expand All @@ -4179,15 +4214,50 @@ shaka.hls.HlsParser = class {
return null;
}

const keyUris = shaka.hls.Utils.constructSegmentUris(
getUris(), drmTag.getRequiredAttrValue('URI'), variables);

const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
const request = shaka.net.NetworkingEngine.makeRequest(
keyUris, this.config_.retryParameters);
const keyResponse =
await this.makeNetworkRequest_(request, requestType);

const key = shaka.util.Uint8ArrayUtils.toHex(keyResponse.data);

// NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
// provide a key ID anywhere. So although we could use the 'URI' attribute
// to fetch the actual 16-byte key, without a key ID, we can't provide this
// automatically to the ClearKey CDM. Instead, the application will have
// to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
// automatically to the ClearKey CDM. By default we assume that keyId is 0,
// but we will try to get key ID from Init Segment.
// If the application want override this behavior will have to use
// player.configure('drm.clearKeys', { ... }) to provide the key IDs
// and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
// provide a ClearKey license server URI.
return shaka.util.ManifestParserUtils.createDrmInfo(
'org.w3.clearkey', /* initDatas= */ null);
let keyId = '00000000000000000000000000000000';

if (initSegmentRef) {
const initSegmentRequest = shaka.util.Networking.createSegmentRequest(
initSegmentRef.getUris(),
initSegmentRef.getStartByte(),
initSegmentRef.getEndByte(),
this.config_.retryParameters);
const initType =
shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
const initResponse = await this.makeNetworkRequest_(
initSegmentRequest, requestType, {type: initType});

const defaultKID = shaka.media.SegmentUtils.getDefaultKID(
initResponse.data);
if (defaultKID) {
keyId = defaultKID;
}
}

const clearkeys = new Map();
clearkeys.set(keyId, key);

return shaka.util.ManifestParserUtils.createDrmInfoFromClearKeys(clearkeys);
}
};

Expand Down Expand Up @@ -4368,8 +4438,6 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
shaka.hls.HlsParser.widevineDrmParser_,
'com.microsoft.playready':
shaka.hls.HlsParser.playreadyDrmParser_,
'identity':
shaka.hls.HlsParser.identityDrmParser_,
};


Expand Down
28 changes: 28 additions & 0 deletions lib/media/segment_utils.js
Expand Up @@ -457,6 +457,34 @@ shaka.media.SegmentUtils = class {
}
return codecs.filter((codec) => codec != dolbyVision);
}

/**
* @param {!BufferSource} data
* @return {?string}
*/
static getDefaultKID(data) {
const Mp4Parser = shaka.util.Mp4Parser;

let defaultKID = null;
new Mp4Parser()
.box('moov', Mp4Parser.children)
.box('trak', Mp4Parser.children)
.box('mdia', Mp4Parser.children)
.box('minf', Mp4Parser.children)
.box('stbl', Mp4Parser.children)
.fullBox('stsd', Mp4Parser.sampleDescription)
.box('encv', Mp4Parser.visualSampleEntry)
.box('enca', Mp4Parser.audioSampleEntry)
.box('sinf', Mp4Parser.children)
.box('schi', Mp4Parser.children)
.fullBox('tenc', (box) => {
const parsedTENCBox = shaka.util.Mp4BoxParsers.parseTENC(box.reader);
defaultKID = parsedTENCBox.defaultKID;
})

.parse(data, /* partialOkay= */ true);
return defaultKID;
}
};


Expand Down
29 changes: 29 additions & 0 deletions lib/util/mp4_box_parsers.js
Expand Up @@ -487,6 +487,24 @@ shaka.util.Mp4BoxParsers = class {
return {codec};
}

/**
* Parses a TENC box.
* @param {!shaka.util.DataViewReader} reader
* @return {!shaka.util.ParsedTENCBox}
*/
static parseTENC(reader) {
reader.readUint8(); // reserved
reader.readUint8();
reader.readUint8(); // default_isProtected
reader.readUint8(); // default_Per_Sample_IV_Size
let defaultKID = '';
for (let i = 0; i <16; i++) {
const hex = reader.readUint8().toString(16);
defaultKID += (hex.length === 1 ? '0' + hex : hex);
}
return {defaultKID};
}

/**
* Parses a HDLR box.
* @param {!shaka.util.DataViewReader} reader
Expand Down Expand Up @@ -699,6 +717,17 @@ shaka.util.ParsedTKHDBox;
*/
shaka.util.ParsedFRMABox;

/**
* @typedef {{
* defaultKID: string
* }}
*
* @property {string} defaultKID
*
* @exportDoc
*/
shaka.util.ParsedTENCBox;

/**
* @typedef {{
* channelCount: number,
Expand Down
16 changes: 16 additions & 0 deletions test/hls/hls_parser_integration.js
Expand Up @@ -71,4 +71,20 @@ describe('HlsParser', () => {

await player.unload();
});

it('supports SAMPLE-AES identity streaming', async () => {
await player.load('/base/test/test/assets/hls-sample-aes/index.m3u8');
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Play for 10 seconds, but stop early if the video ends. If it takes
// longer than 30 seconds, fail the test.
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 10, 30);

await player.unload();
});
});

0 comments on commit 7741dcd

Please sign in to comment.