feat(MSF): Add CMSF contentProtection signaling support (#9972)

See https://github.com/moq-wg/cmsf/pull/18
This commit is contained in:
Álvaro Velad Galván
2026-04-14 17:43:45 +02:00
committed by GitHub
parent f48bd96197
commit aa2dfaec3f
6 changed files with 290 additions and 29 deletions
+45
View File
@@ -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<string>,
* 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<string>|undefined),
* }}
*/
msfCatalog.Track;
@@ -55,6 +99,7 @@ msfCatalog.Track;
* generatedAt: (number|undefined),
* isComplete: (boolean|undefined),
* deltaUpdate: (boolean|undefined),
* contentProtections: (Array<!msfCatalog.ContentProtection>|undefined),
* addTracks: (Array<!msfCatalog.Track>|undefined),
* removeTracks: (Array<!msfCatalog.Track>|undefined),
* cloneTracks: (Array<!msfCatalog.Track>|undefined),
+42
View File
@@ -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<string, string>}
*/
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<string, string>} */
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;
}
};
+5 -9
View File
@@ -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) {
+84 -4
View File
@@ -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<string, !shaka.extern.DrmInfo>}
* @private
*/
processTrack_(track) {
getContentProtections_(catalog) {
const uuidMap = shaka.drm.DrmUtils.getUuidMap();
/** @type {!Map<string, !shaka.extern.DrmInfo>} */
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<string, !shaka.extern.DrmInfo>} 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,
+2 -16
View File
@@ -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,
+112
View File
@@ -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();
});
});
});