diff --git a/lib/util/fairplay_utils.js b/lib/util/fairplay_utils.js index 08cc4fe351..323a181697 100644 --- a/lib/util/fairplay_utils.js +++ b/lib/util/fairplay_utils.js @@ -8,9 +8,11 @@ goog.provide('shaka.util.FairPlayUtils'); goog.require('goog.Uri'); goog.require('goog.asserts'); +goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.Uint8ArrayUtils'); /** @@ -18,6 +20,29 @@ goog.require('shaka.util.StringUtils'); * @export */ shaka.util.FairPlayUtils = class { + /** + * Check if FairPlay is supported. + * + * @return {!Promise.} + * @export + */ + static async isFairPlaySupported() { + const config = { + initDataTypes: ['cenc', 'sinf', 'skd'], + videoCapabilities: [ + { + contentType: 'video/mp4; codecs="avc1.42E01E"', + }, + ], + }; + try { + await navigator.requestMediaKeySystemAccess('com.apple.fps', [config]); + return true; + } catch (err) { + return false; + } + } + /** * Using the default method, extract a content ID from the init data. This is * based on the FairPlay example documentation. @@ -106,4 +131,88 @@ shaka.util.FairPlayUtils = class { offset == rebuiltInitData.length, 'Inconsistent init data length'); return rebuiltInitData; } + + /** + * SPC FairPlay request. + * + * @param {shaka.net.NetworkingEngine.RequestType} type + * @param {shaka.extern.Request} request + * @export + */ + static spcFairPlayRequest(type, request) { + if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { + return; + } + const body = /** @type {!(ArrayBuffer|ArrayBufferView)} */(request.body); + const originalPayload = shaka.util.BufferUtils.toUint8(body); + const base64Payload = shaka.util.Uint8ArrayUtils.toBase64(originalPayload); + request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload); + } + + /** + * Common FairPlay response transform for some DRMs providers. + * + * @param {shaka.net.NetworkingEngine.RequestType} type + * @param {shaka.extern.Response} response + * @export + */ + static commonFairPlayResponse(type, response) { + if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { + return; + } + + // In Apple's docs, responses can be of the form: + // '\nbase64encoded\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. + + 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; + } + + let licenseProcessing = false; + + // Trim whitespace. + responseText = responseText.trim(); + + // Look for wrapper and remove it. + if (responseText.substr(0, 5) === '' && + responseText.substr(-6) === '') { + responseText = responseText.slice(5, -6); + licenseProcessing = true; + } + + // Look for a JSON wrapper and remove it. + try { + const responseObject = /** @type {!Object} */(JSON.parse(responseText)); + if (responseObject['ckc']) { + responseText = responseObject['ckc']; + licenseProcessing = true; + } + if (responseObject['CkcMessage']) { + responseText = responseObject['CkcMessage']; + licenseProcessing = true; + } + if (responseObject['License']) { + responseText = responseObject['License']; + licenseProcessing = true; + } + } catch (err) { + // It wasn't JSON. Fall through with other transformations. + } + + if (licenseProcessing) { + // 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.BufferUtils.toArrayBuffer( + shaka.util.Uint8ArrayUtils.fromBase64(responseText)); + } + } };