Skip to content

Commit

Permalink
feat: Add limited support for HLS "identity" key format (shaka-projec…
Browse files Browse the repository at this point in the history
…t#4451)

This feature is not entirely automatic.

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
and keys or `player.configure('drm.servers.org\.w3\.clearkey', ...)`
to provide a ClearKey license server URI.

Closes shaka-project#2146
  • Loading branch information
joeyparrish committed Aug 30, 2022
1 parent 4a197e1 commit b1e81a6
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 23 deletions.
37 changes: 35 additions & 2 deletions lib/hls/hls_parser.js
Expand Up @@ -1610,7 +1610,11 @@ shaka.hls.HlsParser = class {
// These keys are handled separately.
aesEncrypted = true;
} else {
const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
// 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];

Expand Down Expand Up @@ -2660,7 +2664,7 @@ shaka.hls.HlsParser = class {
*/
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
'com.apple.fps', [
{initDataType: 'sinf', initData: new Uint8Array(0)},
{initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
]);

return drmInfo;
Expand Down Expand Up @@ -2739,6 +2743,33 @@ shaka.hls.HlsParser = class {

return drmInfo;
}

/**
* See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
*
* @param {!shaka.hls.Tag} drmTag
* @return {?shaka.extern.DrmInfo}
* @private
*/
static identityDrmParser_(drmTag) {
const method = drmTag.getRequiredAttrValue('METHOD');
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
if (!VALID_METHODS.includes(method)) {
shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
VALID_METHODS.join(', '), '], not', method);
return null;
}

// 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
// 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);
}
};


Expand Down Expand Up @@ -2886,6 +2917,8 @@ 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
68 changes: 67 additions & 1 deletion test/hls/hls_parser_unit.js
Expand Up @@ -2590,7 +2590,6 @@ describe('HlsParser', () => {
}
});


it('constructs DrmInfo for Widevine', async () => {
const master = [
'#EXTM3U\n',
Expand Down Expand Up @@ -2713,6 +2712,73 @@ describe('HlsParser', () => {
await testHlsParser(master, media, manifest);
});

it('constructs DrmInfo 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',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:6\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYFORMAT="identity",',
'URI="key.bin",\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4',
].join('');

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

await testHlsParser(master, media, manifest);
});

it('constructs DrmInfo 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',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:6\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,',
'URI="key.bin",\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4',
].join('');

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

await testHlsParser(master, media, manifest);
});

it('falls back to mp4 if HEAD request fails', async () => {
const master = [
'#EXTM3U\n',
Expand Down
10 changes: 10 additions & 0 deletions test/player_integration.js
Expand Up @@ -1152,4 +1152,14 @@ describe('Player', () => {
expect(chapter3.endTime).toBe(61.349);
});
}); // describe('addChaptersTrack')

it('requires a license server for HLS ClearKey content', async () => {
const expectedError = Util.jasmineError(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN,
'org.w3.clearkey'));
await expectAsync(player.load('test:sintel-hls-clearkey'))
.toBeRejectedWith(expectedError);
});
});
9 changes: 3 additions & 6 deletions test/test/util/manifest_generator.js
Expand Up @@ -393,8 +393,8 @@ shaka.test.ManifestGenerator.DrmInfo = class {
this.videoRobustness = '';
/** @type {Uint8Array} */
this.serverCertificate = null;
/** @type {Array.<shaka.extern.InitDataOverride>} */
this.initData = null;
/** @type {!Array.<shaka.extern.InitDataOverride>} */
this.initData = [];
/** @type {Set.<string>} */
this.keyIds = new Set();
/** @type {string} */
Expand Down Expand Up @@ -422,10 +422,7 @@ shaka.test.ManifestGenerator.DrmInfo = class {
* @param {!Uint8Array} buffer
*/
addInitData(type, buffer) {
if (!this.initData) {
this.initData = [];
}
this.initData.push({initData: buffer, initDataType: type});
this.initData.push({initData: buffer, initDataType: type, keyId: null});
}

/**
Expand Down
34 changes: 20 additions & 14 deletions test/test/util/test_scheme.js
Expand Up @@ -52,7 +52,8 @@ let ExtraMetadataType;
* duration: number,
* licenseServers: (!Object.<string, string>|undefined),
* licenseRequestHeaders: (!Object.<string, string>|undefined),
* sequenceMode: boolean
* customizeStream: (function(shaka.test.ManifestGenerator.Stream)|undefined),
* sequenceMode: (boolean|undefined)
* }}
*/
let MetadataType;
Expand Down Expand Up @@ -243,6 +244,10 @@ shaka.test.TestScheme = class {
});
}
}

if (data.customizeStream) {
data.customizeStream(stream);
}
}

/**
Expand Down Expand Up @@ -279,7 +284,7 @@ shaka.test.TestScheme = class {

const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.presentationTimeline.setDuration(data.duration);
manifest.sequenceMode = data.sequenceMode;
manifest.sequenceMode = data.sequenceMode || false;

const videoResolutions = data.videoResolutions || [undefined];
const audioLanguages = data.audioLanguages ||
Expand Down Expand Up @@ -513,7 +518,6 @@ shaka.test.TestScheme.DATA = {
audio: sintelAudioSegment,
text: vttSegment,
duration: 30,
sequenceMode: false,
},

// Like 'sintel', but flagged as sequence mode.
Expand All @@ -530,7 +534,6 @@ shaka.test.TestScheme.DATA = {
video: sintelVideoSegment,
audio: sintelAudioSegment,
duration: 300,
sequenceMode: false,
},

// Like 'sintel' above, but with languages and delayed setup.
Expand All @@ -547,7 +550,6 @@ shaka.test.TestScheme.DATA = {
language: 'fa', // Necessary to repro #1696
}),
duration: 30,
sequenceMode: false,
},

'sintel_multi_lingual_multi_res': {
Expand All @@ -561,20 +563,17 @@ shaka.test.TestScheme.DATA = {
audioLanguages: ['en', 'es'],
textLanguages: ['zh', 'fr'],
duration: 30,
sequenceMode: false,
},

'sintel_audio_only': {
audio: sintelAudioSegment,
duration: 30,
sequenceMode: false,
},

'sintel_no_text': {
video: sintelVideoSegment,
audio: sintelAudioSegment,
duration: 30,
sequenceMode: false,
},

// https://github.com/shaka-project/shaka-player/issues/2553
Expand All @@ -583,7 +582,6 @@ shaka.test.TestScheme.DATA = {
text: vttSegment,
textLanguages: ['de', 'de'], // one of these is the "forced subs" track
duration: 30,
sequenceMode: false,
},

'sintel-enc': {
Expand All @@ -592,7 +590,19 @@ shaka.test.TestScheme.DATA = {
text: vttSegment,
licenseServers: widevineDrmServers,
duration: 30,
sequenceMode: false,
},

// Equivalent to what you get with HLS METHOD=SAMPLE-AES, KEYFORMAT=identity.
// Requires explicit clear keys or license server configuration.
'sintel-hls-clearkey': {
video: sintelEncryptedVideo,
audio: sintelEncryptedAudio,
duration: 30,
sequenceMode: true,
customizeStream: (stream) => {
stream.encrypted = true;
stream.addDrmInfo('org.w3.clearkey');
},
},

'multidrm': {
Expand All @@ -602,7 +612,6 @@ shaka.test.TestScheme.DATA = {
licenseServers: axinomDrmServers,
licenseRequestHeaders: axinomDrmHeaders,
duration: 30,
sequenceMode: false,
},

'multidrm_no_init_data': {
Expand All @@ -615,7 +624,6 @@ shaka.test.TestScheme.DATA = {
licenseServers: axinomDrmServers,
licenseRequestHeaders: axinomDrmHeaders,
duration: 30,
sequenceMode: false,
},

'cea-708_ts': {
Expand All @@ -629,7 +637,6 @@ shaka.test.TestScheme.DATA = {
mimeType: 'application/cea-608',
},
duration: 30,
sequenceMode: false,
},

'cea-708_mp4': {
Expand All @@ -644,7 +651,6 @@ shaka.test.TestScheme.DATA = {
closedCaptions: new Map([['CC1', 'en']]),
},
duration: 30,
sequenceMode: false,
},
};

Expand Down

0 comments on commit b1e81a6

Please sign in to comment.