diff --git a/externs/msf_catalog.js b/externs/msf_catalog.js index d04290e4b..85aca47c9 100644 --- a/externs/msf_catalog.js +++ b/externs/msf_catalog.js @@ -14,6 +14,49 @@ var msfCatalog = {}; +/** + * Represents a URL object used in DRM system fields such as laURL, certURL + * and authzURL, as defined in the CMSF Content Protection section. + * + * @typedef {{ + * url: string, + * type: (string|undefined), + * }} + */ +msfCatalog.DrmUrl; + + +/** + * Describes a single DRM system configuration within a Content Protection + * entry, as defined in the CMSF Content Protection section (drmSystem field). + * + * @typedef {{ + * systemID: string, + * laURL: (!msfCatalog.DrmUrl|undefined), + * certURL: (!msfCatalog.DrmUrl|undefined), + * authzURL: (!msfCatalog.DrmUrl|undefined), + * pssh: (string|undefined), + * robustness: (string|undefined), + * }} + */ +msfCatalog.DrmSystem; + + +/** + * Describes a single content protection entry at the root level of the + * catalog, as defined in the CMSF Content Protection section. + * Tracks reference these entries via contentProtectionRefIDs. + * + * @typedef {{ + * refID: string, + * defaultKID: !Array, + * scheme: string, + * drmSystem: !msfCatalog.DrmSystem, + * }} + */ +msfCatalog.ContentProtection; + + /** * @typedef {{ * namespace: (string|undefined), @@ -44,6 +87,7 @@ var msfCatalog = {}; * trackDuration: (number|undefined), * eventType: (string|undefined), * parentName: (string|undefined), + * contentProtectionRefIDs: (Array|undefined), * }} */ msfCatalog.Track; @@ -55,6 +99,7 @@ msfCatalog.Track; * generatedAt: (number|undefined), * isComplete: (boolean|undefined), * deltaUpdate: (boolean|undefined), + * contentProtections: (Array|undefined), * addTracks: (Array|undefined), * removeTracks: (Array|undefined), * cloneTracks: (Array|undefined), diff --git a/lib/drm/drm_utils.js b/lib/drm/drm_utils.js index 866432fd0..12bf7bacc 100644 --- a/lib/drm/drm_utils.js +++ b/lib/drm/drm_utils.js @@ -277,6 +277,48 @@ shaka.drm.DrmUtils = class { static isMediaKeysPolyfilled(polyfillType) { return polyfillType === window.shakaMediaKeysPolyfill; } + + /** + * Returns a mapping between DRM system UUIDs and their corresponding + * EME key system identifiers used. + * + * @param {boolean=} withoutDashes + * @param {boolean=} useUrnFormat + * @return {!Object} + */ + static getUuidMap(withoutDashes = false, useUrnFormat = false) { + const baseMap = { + '1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', + 'e2719d58-a985-b3c9-781a-b030af78d30e': 'org.w3.clearkey', + 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', + '9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', + '79f0049a-4098-8642-ab92-e65be0885f95': 'com.microsoft.playready', + '94ce86fb-07ff-4f43-adb8-93d2fa968ca2': 'com.apple.fps', + '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c': 'com.huawei.wiseplay', + }; + + if (!withoutDashes && !useUrnFormat) { + return baseMap; + } + + /** @type {!Object} */ + const result = {}; + + for (const key in baseMap) { + const value = baseMap[key]; + + let finalKey = key; + if (useUrnFormat) { + finalKey = 'urn:uuid:' + key; + } else if (withoutDashes) { + finalKey = key.replace(/-/g, ''); + } + + result[finalKey] = value; + } + + return result; + } }; diff --git a/lib/media/segment_utils.js b/lib/media/segment_utils.js index 81ff34168..ac65596ed 100644 --- a/lib/media/segment_utils.js +++ b/lib/media/segment_utils.js @@ -307,6 +307,8 @@ shaka.media.SegmentUtils = class { } }; + let uuidMap; + new Mp4Parser() .box('moof', shaka.util.Mp4Parser.children) .box('moov', Mp4Parser.children) @@ -569,15 +571,9 @@ shaka.media.SegmentUtils = class { /* clone= */ false); const systemId = shaka.util.Uint8ArrayUtils.toHex(systemIdData); - const uuidMap = { - '1077efecc0b24d02ace33c1e52e2fb4b': 'org.w3.clearkey', - 'e2719d58a985b3c9781ab030af78d30e': 'org.w3.clearkey', - 'edef8ba979d64acea3c827dcd51d21ed': 'com.widevine.alpha', - '9a04f07998404286ab92e65be0885f95': 'com.microsoft.playready', - '79f0049a40988642ab92e65be0885f95': 'com.microsoft.playready', - '94ce86fb07ff4f43adb893d2fa968ca2': 'com.apple.fps', - '3d5e6d359b9a41e8b843dd3c6e72c42c': 'com.huawei.wiseplay', - }; + if (!uuidMap) { + uuidMap = shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true); + } const keySystem = uuidMap[systemId.toLowerCase()]; if (!keySystem) { diff --git a/lib/msf/msf_parser.js b/lib/msf/msf_parser.js index eda6a3242..f95c04780 100644 --- a/lib/msf/msf_parser.js +++ b/lib/msf/msf_parser.js @@ -10,6 +10,7 @@ goog.provide('shaka.msf.MSFParser'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.drm.DrmUtils'); +goog.require('shaka.drm.PlayReady'); goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.ManifestParser'); goog.require('shaka.media.PresentationTimeline'); @@ -529,6 +530,7 @@ shaka.msf.MSFParser = class { let isLive = true; let duration = Infinity; let targetLatency = 0; + const contentProtections = this.getContentProtections_(catalog); for (const track of catalog.tracks) { if ('isLive' in track) { isLive = track.isLive; @@ -539,7 +541,7 @@ shaka.msf.MSFParser = class { if (track.trackDuration) { duration = Math.min(duration, track.trackDuration); } - promises.push(this.processTrack_(track)); + promises.push(this.processTrack_(track, contentProtections)); } if (targetLatency) { /** @type {shaka.extern.ServiceDescription} */ @@ -560,10 +562,72 @@ shaka.msf.MSFParser = class { } /** - * @param {msfCatalog.Track} track + * @param {msfCatalog.Catalog} catalog + * @return {!Map} * @private */ - processTrack_(track) { + getContentProtections_(catalog) { + const uuidMap = shaka.drm.DrmUtils.getUuidMap(); + + /** @type {!Map} */ + const mapContentProtections = new Map(); + const contentProtections = catalog.contentProtections || []; + for (const contentProtection of contentProtections) { + const refId = contentProtection.refID; + const drmSystem = contentProtection.drmSystem; + + if (!drmSystem) { + continue; + } + const keySystem = uuidMap[drmSystem.systemID.toLowerCase()]; + if (!keySystem) { + continue; + } + const encryptionScheme = contentProtection.scheme || 'cenc'; + + let initData = null; + if (drmSystem.pssh) { + initData = [{ + initDataType: 'cenc', + initData: shaka.util.Uint8ArrayUtils.fromBase64(drmSystem.pssh), + }]; + } + + const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( + keySystem, encryptionScheme, initData); + + if (drmSystem.laURL?.url) { + drmInfo.licenseServerUri = drmSystem.laURL.url; + } else if (initData && + shaka.drm.DrmUtils.isPlayReadyKeySystem(keySystem)) { + drmInfo.licenseServerUri = + shaka.drm.PlayReady.getLicenseUrlFromPssh(initData[0].initData); + } + if (drmSystem.certURL?.url) { + drmInfo.serverCertificateUri = drmSystem.certURL.url; + } + if (drmSystem.robustness) { + drmInfo.videoRobustness = drmSystem.robustness; + drmInfo.audioRobustness = drmSystem.robustness; + } + if (contentProtection.defaultKID) { + for (const kid of contentProtection.defaultKID) { + const normalizedKid = kid.replace(/-/g, '').toLowerCase(); + drmInfo.keyIds.add(normalizedKid); + } + } + + mapContentProtections.set(refId, drmInfo); + } + return mapContentProtections; + } + + /** + * @param {msfCatalog.Track} track + * @param {!Map} contentProtections + * @private + */ + processTrack_(track, contentProtections) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const ManifestParserUtils = shaka.util.ManifestParserUtils; @@ -653,6 +717,22 @@ shaka.msf.MSFParser = class { } } + let drmInfos = []; + const contentProtectionRefIDs = track.contentProtectionRefIDs; + if (contentProtectionRefIDs) { + for (const refId of contentProtectionRefIDs) { + const drmInfo = contentProtections.get(refId); + if (drmInfo) { + drmInfos.push(drmInfo); + } else { + shaka.log.alwaysWarn('Unrecognized contentProtectionRefID', refId); + return; + } + } + } else { + drmInfos = basicInfo.drmInfos; + } + /** @type {shaka.extern.Stream} */ const stream = { id: this.globalId_++, @@ -665,7 +745,7 @@ shaka.msf.MSFParser = class { supplementalCodecs: '', kind, encrypted: false, - drmInfos: basicInfo.drmInfos, + drmInfos, keyIds: new Set(), language: shaka.util.LanguageUtils.normalize(language || 'und'), originalLanguage: language || null, diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index ea44eb047..88674e23b 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -138,22 +138,8 @@ shaka.util.PlayerConfiguration = class { ignoreSuggestedPresentationDelay: false, ignoreEmptyAdaptationSet: false, ignoreMaxSegmentDuration: false, - keySystemsByURI: { - 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': - 'org.w3.clearkey', - 'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e': - '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:94ce86fb-07ff-4f43-adb8-93d2fa968ca2': - 'com.apple.fps', - 'urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c': - 'com.huawei.wiseplay', - }, + keySystemsByURI: shaka.drm.DrmUtils.getUuidMap( + /* withoutDashes= */ false, /* useUrnFormat= */ true), manifestPreprocessorTXml: shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml, sequenceMode: false, diff --git a/test/drm/drm_utils_unit.js b/test/drm/drm_utils_unit.js index 128a75a15..a57ff29c5 100644 --- a/test/drm/drm_utils_unit.js +++ b/test/drm/drm_utils_unit.js @@ -254,4 +254,116 @@ describe('DrmUtils', () => { expect(result).toBe(false); }); }); + + describe('getUuidMap', () => { + it('returns UUIDs with dashes by default', () => { + const map = shaka.drm.DrmUtils.getUuidMap(); + + expect(map['edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBe('com.widevine.alpha'); + + expect(map['9a04f079-9840-4286-ab92-e65be0885f95']) + .toBe('com.microsoft.playready'); + }); + + it('returns UUIDs without dashes when requested', () => { + const map = shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true); + + expect(map['edef8ba979d64acea3c827dcd51d21ed']) + .toBe('com.widevine.alpha'); + + expect(map['9a04f07998404286ab92e65be0885f95']) + .toBe('com.microsoft.playready'); + }); + + it('does not include dashed UUIDs when withoutDashes is true', () => { + const map = shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true); + + expect(map['edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBeUndefined(); + }); + + it('returns consistent number of entries in both modes', () => { + const withDashes = shaka.drm.DrmUtils.getUuidMap(); + const withoutDashes = + shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true); + + expect(Object.keys(withDashes).length) + .toBe(Object.keys(withoutDashes).length); + }); + + it('correctly normalizes UUIDs by removing all dashes', () => { + const withDashes = shaka.drm.DrmUtils.getUuidMap(); + const withoutDashes = + shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true); + + for (const key in withDashes) { + const normalized = key.replace(/-/g, ''); + expect(withoutDashes[normalized]).toBe(withDashes[key]); + } + }); + + it('maps multiple UUIDs to the same key system (ClearKey)', () => { + const map = shaka.drm.DrmUtils.getUuidMap(); + + expect(map['1077efec-c0b2-4d02-ace3-3c1e52e2fb4b']) + .toBe('org.w3.clearkey'); + + expect(map['e2719d58-a985-b3c9-781a-b030af78d30e']) + .toBe('org.w3.clearkey'); + }); + + it('maps multiple UUIDs to the same key system (PlayReady)', () => { + const map = shaka.drm.DrmUtils.getUuidMap(); + + expect(map['9a04f079-9840-4286-ab92-e65be0885f95']) + .toBe('com.microsoft.playready'); + + expect(map['79f0049a-4098-8642-ab92-e65be0885f95']) + .toBe('com.microsoft.playready'); + }); + + it('returns UUIDs in URN format when requested', () => { + const map = shaka.drm.DrmUtils.getUuidMap( + /* withoutDashes= */ false, + /* useUrnFormat= */ true); + + expect(map['urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBe('com.widevine.alpha'); + + expect(map['urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95']) + .toBe('com.microsoft.playready'); + }); + + it('does not include non-URN UUIDs when useUrnFormat is true', () => { + const map = shaka.drm.DrmUtils.getUuidMap(false, true); + + expect(map['edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBeUndefined(); + }); + + it('useUrnFormat takes precedence over withoutDashes', () => { + const map = shaka.drm.DrmUtils.getUuidMap( + /* withoutDashes= */ true, + /* useUrnFormat= */ true); + + expect(map['urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBe('com.widevine.alpha'); + + expect(map['urn:uuid:edef8ba979d64acea3c827dcd51d21ed']) + .toBeUndefined(); + }); + + it('useUrnFormat takes precedence over withoutDashes', () => { + const map = shaka.drm.DrmUtils.getUuidMap( + /* withoutDashes= */ true, + /* useUrnFormat= */ true); + + expect(map['urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed']) + .toBe('com.widevine.alpha'); + + expect(map['urn:uuid:edef8ba979d64acea3c827dcd51d21ed']) + .toBeUndefined(); + }); + }); });