Skip to content

Commit

Permalink
feat(drm): make dash keySystems configurable (shaka-project#3276)
Browse files Browse the repository at this point in the history
Make the DASH keySystems configurable, so that any developer could chose to opt-in for recommendation based on DASH DRM UUID.

Example:
player.configure({
  dash: {
    keySystemsByURI: {
      'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
    }
  }
});
  • Loading branch information
valotvince committed Apr 3, 2021
1 parent 7c6ee2d commit b4595d5
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 55 deletions.
19 changes: 17 additions & 2 deletions docs/tutorials/drm-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,22 @@ particular key system at all, but instead state that any CENC system will do:
```

If this is the only `<ContentProtection>` element in the manifest, Shaka will
try all key systems it knows. (Based on
{@linksource shaka.dash.ContentProtection.defaultKeySystems_}.)
try all key systems it knows. (Based on keySystemsByURI in
{@linksource shaka.extern.DashManifestConfiguration}.)

Through `player.configure()`, you can change the dash key systems mapping by
scheme URI:
```js
player.configure({
dash: {
keySystemsByURI: {
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
}
}
});
```

If the browser supports it and you configured a license server URL for it, we'll
use it.

Expand Down Expand Up @@ -163,6 +177,7 @@ Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/sec

- `3000`
- `2000`
- `150`

`com.microsoft.playready` key system ignores given robustness and stays at a
`2000` decryption level.
Expand Down
6 changes: 5 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,8 @@ shaka.extern.DrmConfiguration;
* initialSegmentLimit: number,
* ignoreSuggestedPresentationDelay: boolean,
* ignoreEmptyAdaptationSet: boolean,
* ignoreMaxSegmentDuration: boolean
* ignoreMaxSegmentDuration: boolean,
* keySystemsByURI: !Object.<string, string>
* }}
*
* @property {string} clockSyncUri
Expand Down Expand Up @@ -671,6 +672,9 @@ shaka.extern.DrmConfiguration;
* If true will cause DASH parser to ignore
* <code>maxSegmentDuration</code> from manifest. Defaults to
* <code>false</code> if not provided.
* @property {Object.<string, string>} keySystemsByURI
* A map of scheme URI to key system name. Defaults to default key systems
* mapping handled by Shaka.
* @exportDoc
*/
shaka.extern.DashManifestConfiguration;
Expand Down
45 changes: 17 additions & 28 deletions lib/dash/content_protection.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ shaka.dash.ContentProtection = class {
*
* @param {!Array.<!Element>} elems
* @param {boolean} ignoreDrmInfo
* @param {!Object.<string, string>} keySystemsByURI
* @return {shaka.dash.ContentProtection.Context}
*/
static parseFromAdaptationSet(elems, ignoreDrmInfo) {
static parseFromAdaptationSet(elems, ignoreDrmInfo, keySystemsByURI) {
const ContentProtection = shaka.dash.ContentProtection;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const parsed = ContentProtection.parseElements_(elems);
Expand Down Expand Up @@ -67,7 +68,7 @@ shaka.dash.ContentProtection = class {

if (parsedNonCenc.length) {
drmInfos = ContentProtection.convertElements_(
defaultInit, parsedNonCenc);
defaultInit, parsedNonCenc, keySystemsByURI);

// If there are no drmInfos after parsing, then add a dummy entry.
// This may be removed in parseKeyIds.
Expand All @@ -82,8 +83,7 @@ shaka.dash.ContentProtection = class {
if (parsed.length && (ignoreDrmInfo || !parsedNonCenc.length)) {
drmInfos = [];

const keySystems = ContentProtection.defaultKeySystems_;
for (const keySystem of keySystems.values()) {
for (const keySystem of Object.values(keySystemsByURI)) {
// If the manifest doesn't specify any key systems, we shouldn't
// put clearkey in this list. Otherwise, it may be triggered when
// a real key system should be used instead.
Expand Down Expand Up @@ -121,12 +121,14 @@ shaka.dash.ContentProtection = class {
* @param {!Array.<!Element>} elems
* @param {shaka.dash.ContentProtection.Context} context
* @param {boolean} ignoreDrmInfo
* @param {!Object.<string, string>} keySystemsByURI
* @return {?string} The parsed key ID
*/
static parseFromRepresentation(elems, context, ignoreDrmInfo) {
static parseFromRepresentation(
elems, context, ignoreDrmInfo, keySystemsByURI) {
const ContentProtection = shaka.dash.ContentProtection;
const repContext = ContentProtection.parseFromAdaptationSet(
elems, ignoreDrmInfo);
elems, ignoreDrmInfo, keySystemsByURI);

if (context.firstRepresentation) {
const asUnknown = context.drmInfos.length == 1 &&
Expand Down Expand Up @@ -348,20 +350,20 @@ shaka.dash.ContentProtection = class {
*
* @param {Array.<shaka.extern.InitDataOverride>} defaultInit
* @param {!Array.<shaka.dash.ContentProtection.Element>} elements
* @param {!Object.<string, string>} keySystemsByURI
* @return {!Array.<shaka.extern.DrmInfo>}
* @private
*/
static convertElements_(defaultInit, elements) {
static convertElements_(defaultInit, elements, keySystemsByURI) {
const ContentProtection = shaka.dash.ContentProtection;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const defaultKeySystems = ContentProtection.defaultKeySystems_;
const licenseUrlParsers = ContentProtection.licenseUrlParsers_;

/** @type {!Array.<shaka.extern.DrmInfo>} */
const out = [];

for (const element of elements) {
const keySystem = defaultKeySystems.get(element.schemeUri);
const keySystem = keySystemsByURI[element.schemeUri];
if (keySystem) {
goog.asserts.assert(
!element.init || element.init.length,
Expand Down Expand Up @@ -544,25 +546,6 @@ shaka.dash.ContentProtection.Context;
*/
shaka.dash.ContentProtection.Element;


/**
* A map of scheme URI to key system name.
*
* @const {!Map.<string, string>}
* @private
*/
shaka.dash.ContentProtection.defaultKeySystems_ = new Map()
.set('urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b',
'org.w3.clearkey')
.set('urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
'com.widevine.alpha')
.set('urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95',
'com.microsoft.playready')
.set('urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95',
'com.microsoft.playready')
.set('urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb',
'com.adobe.primetime');

/**
* A map of key system name to license server url parser.
*
Expand All @@ -573,6 +556,12 @@ shaka.dash.ContentProtection.licenseUrlParsers_ = new Map()
.set('com.widevine.alpha',
shaka.dash.ContentProtection.getWidevineLicenseUrl)
.set('com.microsoft.playready',
shaka.dash.ContentProtection.getPlayReadyLicenseUrl)
.set('com.microsoft.playready.recommendation',
shaka.dash.ContentProtection.getPlayReadyLicenseUrl)
.set('com.microsoft.playready.software',
shaka.dash.ContentProtection.getPlayReadyLicenseUrl)
.set('com.microsoft.playready.hardware',
shaka.dash.ContentProtection.getPlayReadyLicenseUrl);

/**
Expand Down
7 changes: 5 additions & 2 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,9 @@ shaka.dash.DashParser = class {
const contentProtectionElems =
XmlUtils.findChildren(elem, 'ContentProtection');
const contentProtection = ContentProtection.parseFromAdaptationSet(
contentProtectionElems, this.config_.dash.ignoreDrmInfo);
contentProtectionElems,
this.config_.dash.ignoreDrmInfo,
this.config_.dash.keySystemsByURI);

const language =
shaka.util.LanguageUtils.normalize(elem.getAttribute('lang') || 'und');
Expand Down Expand Up @@ -1070,7 +1072,8 @@ shaka.dash.DashParser = class {
XmlUtils.findChildren(node, 'ContentProtection');
const keyId = shaka.dash.ContentProtection.parseFromRepresentation(
contentProtectionElems, contentProtection,
this.config_.dash.ignoreDrmInfo);
this.config_.dash.ignoreDrmInfo,
this.config_.dash.keySystemsByURI);
const keyIds = new Set(keyId ? [keyId] : []);

// Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling.
Expand Down
34 changes: 18 additions & 16 deletions lib/media/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,17 @@ shaka.media.DrmEngine = class {
return drmInfo ? drmInfo.keySystem : '';
}

/**
* @param {?string} keySystem
* @return {boolean} */
static isPlayReadyKeySystem(keySystem) {
if (keySystem) {
return !!keySystem.match(/^com\.(microsoft|chromecast)\.playready/);
}

return false;
}

/**
* Check if DrmEngine (as initialized) will likely be able to support the
* given content type.
Expand Down Expand Up @@ -1281,8 +1292,8 @@ shaka.media.DrmEngine = class {
request.sessionId = session.sessionId;
// NOTE: allowCrossSiteCredentials can be set in a request filter.

if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' ||
this.currentDrmInfo_.keySystem == 'com.chromecast.playready') {
if (shaka.media.DrmEngine.isPlayReadyKeySystem(
this.currentDrmInfo_.keySystem)) {
this.unpackPlayReadyRequest_(request);
}

Expand Down Expand Up @@ -1449,12 +1460,13 @@ shaka.media.DrmEngine = class {
// on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a
// Bug filed: https://bit.ly/2thuzXu

// NOTE that we skip this if byteLength != 16. This is used for the IE11
// and Edge 12 EME polyfill, which uses single-byte dummy key IDs.
// NOTE that we skip this if byteLength != 16. This is used for Edge
// which uses single-byte dummy key IDs.
// However, unlike Edge and Chromecast, Tizen doesn't have this problem.
if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
if (shaka.media.DrmEngine.isPlayReadyKeySystem(
this.currentDrmInfo_.keySystem) &&
keyId.byteLength == 16 &&
(shaka.util.Platform.isIE() || shaka.util.Platform.isEdge())) {
shaka.util.Platform.isEdge()) {
// Read out some fields in little-endian:
const dataView = shaka.util.BufferUtils.toDataView(keyId);
const part0 = dataView.getUint32(0, /* LE= */ true);
Expand All @@ -1466,16 +1478,6 @@ shaka.media.DrmEngine = class {
dataView.setUint16(6, part2, /* BE= */ false);
}

// Microsoft's implementation in IE11 seems to never set key status to
// 'usable'. It is stuck forever at 'status-pending'. In spite of this,
// the keys do seem to be usable and content plays correctly.
// Bug filed: https://bit.ly/2tpIU3n
// Microsoft has fixed the issue on Edge, but it remains in IE.
if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
status == 'status-pending') {
status = 'usable';
}

if (status != 'status-pending') {
found.loaded = true;
}
Expand Down
6 changes: 6 additions & 0 deletions lib/offline/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -1558,6 +1558,12 @@ shaka.offline.Storage.defaultSystemIds_ = new Map()
.set('org.w3.clearkey', '1077efecc0b24d02ace33c1e52e2fb4b')
.set('com.widevine.alpha', 'edef8ba979d64acea3c827dcd51d21ed')
.set('com.microsoft.playready', '9a04f07998404286ab92e65be0885f95')
.set('com.microsoft.playready.recommendation',
'9a04f07998404286ab92e65be0885f95')
.set('com.microsoft.playready.software',
'9a04f07998404286ab92e65be0885f95')
.set('com.microsoft.playready.hardware',
'9a04f07998404286ab92e65be0885f95')
.set('com.adobe.primetime', 'f239e769efa348509c16a903c6932efb');

shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);
12 changes: 12 additions & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ shaka.util.PlayerConfiguration = class {
ignoreSuggestedPresentationDelay: false,
ignoreEmptyAdaptationSet: false,
ignoreMaxSegmentDuration: false,
keySystemsByURI: {
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b':
'org.w3.clearkey',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
'com.widevine.alpha',
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95':
'com.microsoft.playready',
'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95':
'com.microsoft.playready',
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb':
'com.adobe.primetime',
},
},
hls: {
ignoreTextStreamFailures: false,
Expand Down
14 changes: 8 additions & 6 deletions test/demo/demo_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ describe('Demo', () => {
.add('preferredVariantRole')
.add('preferredTextRole')
.add('playRangeStart')
.add('playRangeEnd');
.add('playRangeEnd')
.add('manifest.dash.keySystemsByURI');

/**
* @param {!Object} section
Expand All @@ -108,13 +109,14 @@ describe('Demo', () => {
for (const key in section) {
const name = (accumulatedName) ? (accumulatedName + '.' + key) : key;
const value = section[key];
if (configPrimitives.has(typeof value)) {
if (!exceptions.has(name)) {

if (!exceptions.has(name)) {
if (configPrimitives.has(typeof value)) {
checkValueNameFn(name);
} else {
// It's a sub-section.
check(value, name);
}
} else {
// It's a sub-section.
check(value, name);
}
}
};
Expand Down
18 changes: 18 additions & 0 deletions test/media/drm_engine_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,24 @@ describe('DrmEngine', () => {
});
}); // describe('destroy')

describe('isPlayReadyKeySystem', () => {
it('should return true for MS & Chromecast PlayReady', () => {
expect(shaka.media.DrmEngine.isPlayReadyKeySystem(
'com.microsoft.playready')).toBe(true);
expect(shaka.media.DrmEngine.isPlayReadyKeySystem(
'com.microsoft.playready.anything')).toBe(true);
expect(shaka.media.DrmEngine.isPlayReadyKeySystem(
'com.chromecast.playready')).toBe(true);
});

it('should return false for non-PlayReady key systems', () => {
expect(shaka.media.DrmEngine.isPlayReadyKeySystem(
'com.widevine.alpha')).toBe(false);
expect(shaka.media.DrmEngine.isPlayReadyKeySystem(
'com.abc.playready')).toBe(false);
});
});

describe('getDrmInfo', () => {
it('includes correct info', async () => {
// Leave only one drmInfo
Expand Down

0 comments on commit b4595d5

Please sign in to comment.