Skip to content

Commit

Permalink
Add FairPlay EME polyfill and DrmEngine support
Browse files Browse the repository at this point in the history
This adds a polyfill for Apple's prefixed EME implementation.  This
will be used on all macOS versions prior to 10.14 (Mojave) and on
Safari versions prior to 12.1.

This also adds support for FairPlay license protocol eccentricities
in DrmEngine, so that the proper formatting is used for requests and
responses.

Issue #382

Change-Id: If1274d2f018a475f56c09df97645694f13acbde9
  • Loading branch information
joeyparrish committed Apr 30, 2019
1 parent 8391fe1 commit a121733
Show file tree
Hide file tree
Showing 11 changed files with 1,098 additions and 1 deletion.
2 changes: 2 additions & 0 deletions build/conformance.textproto
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ requirement: {
error_message: 'Event is not constructable on IE. Instead, use '
'document.createEvent(\'CustomEvent\') to create an event and '
'event.initCustomEvent(\'my-event-type\') to initialize it.'
whitelist_regexp: 'lib/polyfill/patchedmediakeys_apple.js'
}


Expand All @@ -247,6 +248,7 @@ requirement: {
value: 'TypedArray.prototype.slice'
error_message: 'TypedArray.slice is not allowed because it '
'is not supported on IE11'
whitelist_regexp: 'lib/polyfill/patchedmediakeys_apple.js'
}
requirement: {
type: BANNED_NAME
Expand Down
1 change: 1 addition & 0 deletions build/types/polyfill
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
+../../lib/polyfill/input_event.js
+../../lib/polyfill/mathround.js
+../../lib/polyfill/mediasource.js
+../../lib/polyfill/patchedmediakeys_apple.js
+../../lib/polyfill/patchedmediakeys_ms.js
+../../lib/polyfill/patchedmediakeys_nop.js
+../../lib/polyfill/patchedmediakeys_webkit.js
Expand Down
1 change: 1 addition & 0 deletions demo/asset_section.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ shakaDemo.preparePlayer_ = function(asset) {
let commonDrmSystems = [
'com.widevine.alpha',
'com.microsoft.playready',
'com.apple.fps.1_0',
'com.adobe.primetime',
'org.w3.clearkey',
];
Expand Down
1 change: 1 addition & 0 deletions demo/common/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ shakaAssets.Source = {
/** @enum {string} */
shakaAssets.KeySystem = {
CLEAR_KEY: 'org.w3.clearkey',
FAIRPLAY: 'com.apple.fps.1_0',
PLAYREADY: 'com.microsoft.playready',
WIDEVINE: 'com.widevine.alpha',
};
Expand Down
125 changes: 125 additions & 0 deletions externs/webkitmediakeys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview Externs for prefixed EME v20140218 as supported by IE11/Edge
* (http://www.w3.org/TR/2014/WD-encrypted-media-20140218).
* @externs
*/


/**
* @constructor
* @param {string} keySystem
*/
function WebKitMediaKeys(keySystem) {}


/**
* @param {string} keySystem
* @param {string} contentType
* @return {boolean}
*/
WebKitMediaKeys.isTypeSupported = function(keySystem, contentType) {};


/**
* @param {string} contentType
* @param {Uint8Array} initData
* @return {!WebKitMediaKeySession}
*/
WebKitMediaKeys.prototype.createSession = function(contentType, initData) {};


/**
* @interface
* @extends {EventTarget}
*/
function WebKitMediaKeySession() {}


/**
* @param {Uint8Array} message
*/
WebKitMediaKeySession.prototype.update = function(message) {};


WebKitMediaKeySession.prototype.close = function() {};


/** @type {WebKitMediaKeyError} */
WebKitMediaKeySession.prototype.error;


/** @override */
WebKitMediaKeySession.prototype.addEventListener =
function(type, listener, useCapture) {};


/** @override */
WebKitMediaKeySession.prototype.removeEventListener =
function(type, listener, useCapture) {};


/** @override */
WebKitMediaKeySession.prototype.dispatchEvent = function(evt) {};


/**
* @param {WebKitMediaKeys} mediaKeys
*/
HTMLMediaElement.prototype.webkitSetMediaKeys = function(mediaKeys) {};


/** @type {WebKitMediaKeys} */
HTMLMediaElement.prototype.webkitKeys;


/** @constructor */
function WebKitMediaKeyError() {}


/** @type {number} */
WebKitMediaKeyError.prototype.code;


/** @type {number} */
WebKitMediaKeyError.prototype.systemCode;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_UNKNOWN;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_CLIENT;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_SERVICE;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_OUTPUT;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_HARDWARECHANGE;


/** @type {number} */
WebKitMediaKeyError.MEDIA_KEYERR_DOMAIN;
79 changes: 79 additions & 0 deletions lib/media/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ shaka.media.DrmEngine.prototype.attach = function(video) {
// warn when the stream is encrypted, even though the manifest does not know
// it.
// Don't complain about this twice, so just listenOnce().
// FIXME: This is ineffective when a prefixed event is translated by our
// polyfills, since those events are only caught and translated by a
// MediaKeys instance. With clear content and no polyfilled MediaKeys
// instance attached, you'll never see the 'encrypted' event on those
// platforms (IE 11 & Safari).
this.eventManager_.listenOnce(video, 'encrypted', (event) => {
this.onError_(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
Expand Down Expand Up @@ -1186,11 +1191,18 @@ shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) {
this.currentDrmInfo_.keySystem == 'com.chromecast.playready') {
this.unpackPlayReadyRequest_(request);
}
if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps')) {
this.formatFairPlayRequest_(request);
}

this.playerInterface_.netEngine.request(requestType, request).promise
.then(function(response) {
if (this.isDestroying_) { return Promise.reject(); }

if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps')) {
this.parseFairPlayResponse_(response);
}

// Request succeeded, now pass the response to the CDM.
return session.update(response.data).then(function() {
let event = new shaka.util.FakeEvent('drmsessionupdate');
Expand Down Expand Up @@ -1309,6 +1321,72 @@ shaka.media.DrmEngine.prototype.unpackPlayReadyRequest_ = function(request) {
};


/**
* Formats FairPlay license requests. Modifies the request object.
*
* @param {shaka.extern.Request} request
* @private
*/
shaka.media.DrmEngine.prototype.formatFairPlayRequest_ = function(request) {
// The standard format for FairPlay seems to be to place the request into a
// POST parameter (spc=).
const originalPayload = new Uint8Array(request.body);
const base64Payload = shaka.util.Uint8ArrayUtils.toBase64(originalPayload);
const params = 'spc=' + base64Payload;
request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
request.body = shaka.util.StringUtils.toUTF8(params);
};


/**
* Parse FairPlay license response format. Modifies the response object.
* This will run after any response filters, so application-specific formats
* can still be handled by the app.
*
* @param {shaka.extern.Response} response
* @private
*/
shaka.media.DrmEngine.prototype.parseFairPlayResponse_ = function(response) {
// In Apple's docs, responses can be of the form:
// '\n<ckc>base64encoded</ckc>\n' or 'base64encoded'
// We have also seen responses in JSON format from some of our partners.
// In all of these text-based formats, the CKC data is base64-encoded.

// This handles all of the above. Other formats should be handled via
// application-level response filters.

let responseText;
try {
// Convert it to text for further processing.
responseText = shaka.util.StringUtils.fromUTF8(response.data);
} catch (error) {
// Assume it's not a text format of any kind and leave it alone.
return;
}

// Trim whitespace.
responseText = responseText.trim();

// Look for <ckc> wrapper and remove it.
if (responseText.substr(0, 5) === '<ckc>' &&
responseText.substr(-6) === '</ckc>') {
responseText = responseText.slice(5, -6);
}

// Look for a JSON wrapper and remove it.
try {
const responseObject = JSON.parse(responseText);
responseText = responseObject['ckc'];
} catch (error) {
// It wasn't JSON. Fall through with other transformations.
}

// Decode the base64-encoded data into the format the browser expects.
// It's not clear why FairPlay license servers don't just serve this directly.
response.data = shaka.util.Uint8ArrayUtils.fromBase64(responseText).buffer;
};


/**
* @param {!Event} event
* @private
Expand Down Expand Up @@ -1477,6 +1555,7 @@ shaka.media.DrmEngine.probeSupport = function() {
'org.w3.clearkey',
'com.widevine.alpha',
'com.microsoft.playready',
'com.apple.fps.3_0',
'com.apple.fps.2_0',
'com.apple.fps.1_0',
'com.apple.fps',
Expand Down
6 changes: 5 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -4545,7 +4545,11 @@ shaka.Player.prototype.onVideoError_ = function(event) {
* @private
*/
shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
goog.asserts.assert(this.streamingEngine_, 'Should have been initialized.');
if (!this.streamingEngine_) {
// We can't use this info to manage restrictions in src= mode, so ignore it.
return;
}

const restrictedStatuses = shaka.Player.restrictedStatuses_;

/** @type {shaka.extern.Period} */
Expand Down
Loading

0 comments on commit a121733

Please sign in to comment.