Skip to content

Commit

Permalink
feat(MSS): Fix MSS PlayReady support (shaka-project#5486)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Aug 17, 2023
1 parent aee1142 commit 1dd9809
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 5 deletions.
2 changes: 2 additions & 0 deletions externs/isoboxer.js
Expand Up @@ -45,6 +45,7 @@ var ISOBoxerUtils;

/**
* @typedef {{
* _parsing: boolean,
* type: string,
* size: number,
* _parent: ISOBox,
Expand Down Expand Up @@ -123,6 +124,7 @@ var ISOBoxerUtils;
* sample_info_size: Array.<number>,
* data_offset: number
* }}
* @property {boolean} _parsing
* @property {string} type
* @property {number} size
* @property {ISOBox} _parent
Expand Down
14 changes: 12 additions & 2 deletions lib/mss/content_protection.js
Expand Up @@ -175,7 +175,17 @@ shaka.mss.ContentProtection = class {
for (const elem of xml.getElementsByTagName('DATA')) {
const kid = shaka.util.XmlUtils.findChild(elem, 'KID');
if (kid) {
return kid.textContent;
// GUID: [DWORD, WORD, WORD, 8-BYTE]
const guidBytes =
shaka.util.Uint8ArrayUtils.fromBase64(kid.textContent);
// Reverse byte order from little-endian to big-endian
const kidBytes = new Uint8Array([
guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0],
guidBytes[5], guidBytes[4],
guidBytes[7], guidBytes[6],
...guidBytes.slice(8),
]);
return shaka.util.Uint8ArrayUtils.toHex(kidBytes);
}
}

Expand Down Expand Up @@ -274,7 +284,7 @@ shaka.mss.ContentProtection = class {

for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const systemID = element.getAttribute('SystemID');
const systemID = element.getAttribute('SystemID').toLowerCase();
const keySystem = keySystemsBySystemId[systemID];
if (keySystem) {
const KID = ContentProtection.getPlayReadyKID_(element);
Expand Down
38 changes: 36 additions & 2 deletions lib/transmuxer/mss_transmuxer.js
Expand Up @@ -98,14 +98,16 @@ shaka.transmuxer.MssTransmuxer = class {
}
});
// eslint-disable-next-line no-restricted-syntax
this.isoBoxer_.addBoxProcessor('senc', function() {
const sencProcessor = function() {
// eslint-disable-next-line no-invalid-this
const box = /** @type {!ISOBox} */(this);
box._procFullBox();
box._procField('sample_count', 'uint', 32);
if (box.flags & 1) {
box._procField('AlgorithmID', 'uint', 24);
box._procField('IV_size', 'uint', 8);
box._procFieldArray('KID', 16, 'uint', 8);
}
box._procField('sample_count', 'uint', 32);
// eslint-disable-next-line no-restricted-syntax
box._procEntries('entry', box.sample_count, function(entry) {
// eslint-disable-next-line no-invalid-this
Expand All @@ -125,6 +127,30 @@ shaka.transmuxer.MssTransmuxer = class {
});
}
});
};
this.isoBoxer_.addBoxProcessor('senc', sencProcessor);
// eslint-disable-next-line no-restricted-syntax
this.isoBoxer_.addBoxProcessor('uuid', function() {
const MssTransmuxer = shaka.transmuxer.MssTransmuxer;
// eslint-disable-next-line no-invalid-this
const box = /** @type {!ISOBox} */(this);
let isSENC = true;
for (let i = 0; i < 16; i++) {
if (box.usertype[i] !== MssTransmuxer.UUID_SENC_[i]) {
isSENC = false;
}
// Add support for other user types here
}

if (isSENC) {
if (box._parsing) {
// Convert this box to sepiff for later processing.
// See processMediaSegment_ function.
box.type = 'sepiff';
}
// eslint-disable-next-line no-restricted-syntax, no-invalid-this
sencProcessor.call(/** @type {!ISOBox} */(this));
}
});
}

Expand Down Expand Up @@ -345,6 +371,14 @@ shaka.transmuxer.MssTransmuxer = class {
}
};

/**
* @private {!Uint8Array}
*/
shaka.transmuxer.MssTransmuxer.UUID_SENC_ = new Uint8Array([
0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14,
0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4,
]);

shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
'mss/audio/mp4',
() => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'),
Expand Down
54 changes: 53 additions & 1 deletion test/mss/mss_parser_unit.js
Expand Up @@ -24,6 +24,27 @@ describe('MssParser Manifest', () => {

const aacCodecPrivateData = '1210';

// From https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264PR/S
// uperSpeedway_720.ism/Manifest
const protectionHeader = 'jAMAAAEAAQCCAzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0A' +
'bABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzA' +
'G8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQ' +
'BhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4' +
'AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZ' +
'AEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQ' +
'wBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AE' +
'sASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA' +
'9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEA' +
'WQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABDAFUAUwBUAE8ATQBBAFQAVABSA' +
'EkAQgBVAFQARQBTAD4APABJAEkAUwBfAEQAUgBNAF8AVgBFAFIAUwBJAE8ATgA+ADcALg' +
'AxAC4AMQAwADYANAAuADAAPAAvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4' +
'APAAvAEMAVQBTAFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AEwAQQBfAFUAUgBM' +
'AD4AaAB0AHQAcAA6AC8ALwBwAGwAYQB5AHIAZQBhAGQAeQAuAGQAaQByAGUAYwB0AHQAY' +
'QBwAHMALgBuAGUAdAAvAHAAcgAvAHMAdgBjAC8AcgBpAGcAaAB0AHMAbQBhAG4AYQBnAG' +
'UAcgAuAGEAcwBtAHgAPAAvAEwAQQBfAFUAUgBMAD4APABEAFMAXwBJAEQAPgBBAEgAKwA' +
'wADMAagB1AEsAYgBVAEcAYgBIAGwAMQBWAC8AUQBJAHcAUgBBAD0APQA8AC8ARABTAF8A' +
'SQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=';

/** @param {!shaka.extern.Manifest} manifest */
async function loadAllStreamsFor(manifest) {
const promises = [];
Expand Down Expand Up @@ -113,7 +134,7 @@ describe('MssParser Manifest', () => {
await Mss.testFails(source, error);
});

it('ive content ', async () => {
it('live content ', async () => {
const source = [
'<SmoothStreamingMedia Duration="1209510000" IsLive="true">',
' <StreamIndex Name="audio" Type="audio" Url="uri">',
Expand Down Expand Up @@ -417,4 +438,35 @@ describe('MssParser Manifest', () => {
expect(variant.audio).toBeTruthy();
expect(variant.video).toBeTruthy();
});

it('recognizes PlayReady System ID with mixed cases', async () => {
const manifestText = [
'<SmoothStreamingMedia Duration="1209510000">',
' <StreamIndex Type="video" Url="uri">',
' <QualityLevel Bitrate="2962000" CodecPrivateData="',
h264CodecPrivateData,
'" FourCC="H264" MaxHeight="720" MaxWidth="1280"/>',
' <c d="20020000"/>',
' </StreamIndex>',
' <Protection>',
' <ProtectionHeader SystemID="9a04F079-9840-4286-aB92-e65BE0885f95">',
protectionHeader,
' </ProtectionHeader>',
' </Protection>',
'</SmoothStreamingMedia>',
].join('\n');

fakeNetEngine.setResponseText('dummy://foo', manifestText);

/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const variant = manifest.variants[0];
expect(variant.video.drmInfos.length).toBe(1);
expect(variant.video.drmInfos[0].keySystem).toBe('com.microsoft.playready');
// Also able to parse KID correctly
expect(variant.video.drmInfos[0].keyIds.size).toBe(1);
// Expected KID: https://testweb.playready.microsoft.com/Content/Content2X
expect([...(variant.video.drmInfos[0].keyIds.values())][0]).toBe(
'09E367028F33436CA5DD60FFE6671E70'.toLowerCase());
});
});
46 changes: 46 additions & 0 deletions test/mss/mss_player_integration.js
Expand Up @@ -25,6 +25,12 @@ describe('MSS Player', () => {
// eslint-disable-next-line max-len
const url = 'https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest';

// eslint-disable-next-line max-len
const playreadyUrl = 'https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest';

// eslint-disable-next-line max-len
const playreadyLicenseUrl = 'https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:150)';

beforeAll(async () => {
video = shaka.test.UiUtils.createVideoElement();
document.body.appendChild(video);
Expand Down Expand Up @@ -81,4 +87,44 @@ describe('MSS Player', () => {

await player.unload();
});

it('MSS VoD PlayReady', async () => {
const support = await shaka.media.DrmEngine.probeSupport();
if (!support['com.microsoft.playready']) {
return;
}
// Make sure we are playing the lowest res available to avoid test flake
// based on network issues. Note that disabling ABR and setting a low
// abr.defaultBandwidthEstimate would not be sufficient, because it
// would only affect the choice of track on the first period. When we
// cross a period boundary, the default bandwidth estimate will no
// longer be in effect, and AbrManager may choose higher res tracks for
// the new period. Using abr.restrictions.maxHeight will let us force
// AbrManager to the lowest resolution, which is its fallback when these
// soft restrictions cannot be met.
player.configure('abr.restrictions.maxHeight', 1);

player.configure({
drm: {
servers: {
'com.microsoft.playready': playreadyLicenseUrl,
},
},
});

await player.load(playreadyUrl, /* startTime= */ null,
/* mimeType= */ 'application/vnd.ms-sstr+xml');
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 5 seconds, but stop early if the video ends. If it takes
// longer than 10 seconds, fail the test.
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 5, 10);

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

0 comments on commit 1dd9809

Please sign in to comment.