Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MSS): Fix MSS PlayReady support #5486

Merged
merged 4 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions externs/isoboxer.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
});
});