Skip to content

Commit 7ae6fc7

Browse files
committed
Fix FairPlay encrypted event handling.
The 'webkitkeyneeded' and 'encrypted' events send similar data, but they were incompatible with each other and our transform handling. This makes our polyfill produce the same format as the browser for cases where the browser may only fire the old event. This also makes our utilities work with the new format. The 'webkitkeyneeded' event was a length-prefixed UTF-16 string while the 'encrypted' event was just a UTF-8 string. This also makes a breaking change in the transform callback to pass the init data type. This shouldn't break anyone that only uses the first argument; the second argument was mainly added so we could have the default transform work without knowing anything. This change could also break people who use custom transform functions. The init data format is changing, which could break people who read it directly. If they follow the tutorial and use our utilities, it shouldn't break. This also updates the tutorial to match the new format and be more clear about the format. Fixes #2214 Change-Id: I006382028e828e31e20e085114fd7fd85c0e1eaa
1 parent f757690 commit 7ae6fc7

File tree

5 files changed

+45
-26
lines changed

5 files changed

+45
-26
lines changed

docs/tutorials/fairplay.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ is used by the browser to generate the license request. If you don't use the
2525
default content ID derivation, you need to specify a custom init data transform:
2626

2727
```js
28-
player.configure('drm.initDataTransform', (initData) => {
29-
const contentId = getMyContentId(initData);
28+
player.configure('drm.initDataTransform', (initData, initDataType) => {
29+
if (initDataType != 'skd')
30+
return initData;
31+
32+
// 'initData' is a buffer containing an 'skd://' URL as a UTF-8 string.
33+
const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
34+
const contentId = getMyContentId(sdkUri);
3035
const cert = player.drmInfo().serverCertificate;
3136
return shaka.util.FairPlayUtils.initDataTransform(initData, contentId, cert);
3237
});

externs/shaka/player.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,8 @@ shaka.extern.AdvancedDrmConfiguration;
520520
* delayLicenseRequestUntilPlayed: boolean,
521521
* advanced: Object.<string, shaka.extern.AdvancedDrmConfiguration>,
522522
* initDataTransform:
523-
* ((function(!Uint8Array, ?shaka.extern.DrmInfo):!Uint8Array)|undefined),
523+
* ((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)|
524+
* undefined),
524525
* logLicenseExchange: boolean
525526
* }}
526527
*
@@ -543,7 +544,8 @@ shaka.extern.AdvancedDrmConfiguration;
543544
* A dictionary which maps key system IDs to advanced DRM configuration for
544545
* those key systems.
545546
* @property
546-
* {((function(!Uint8Array, ?shaka.extern.DrmInfo):!Uint8Array)|undefined)}
547+
* {((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)|
548+
* undefined)}
547549
* initDataTransform
548550
* <i>Optional.</i><br>
549551
* If given, this function is called with the init data from the

lib/media/drm_engine.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,8 @@ shaka.media.DrmEngine = class {
10161016
this.activeSessions_.set(session, metadata);
10171017

10181018
try {
1019-
initData = this.config_.initDataTransform(initData, this.currentDrmInfo_);
1019+
initData = this.config_.initDataTransform(
1020+
initData, initDataType, this.currentDrmInfo_);
10201021
} catch (error) {
10211022
let shakaError = error;
10221023
if (!(error instanceof shaka.util.Error)) {
@@ -1061,11 +1062,12 @@ shaka.media.DrmEngine = class {
10611062

10621063
/**
10631064
* @param {!Uint8Array} initData
1065+
* @param {string} initDataType
10641066
* @param {?shaka.extern.DrmInfo} drmInfo
10651067
* @return {!Uint8Array}
10661068
*/
1067-
static defaultInitDataTransform(initData, drmInfo) {
1068-
if (shaka.media.DrmEngine.keySystem(drmInfo).startsWith('com.apple.fps')) {
1069+
static defaultInitDataTransform(initData, initDataType, drmInfo) {
1070+
if (initDataType == 'skd') {
10691071
const cert = drmInfo.serverCertificate;
10701072
const contentId =
10711073
shaka.util.FairPlayUtils.defaultGetContentId(initData);

lib/polyfill/patchedmediakeys_apple.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,26 @@ shaka.polyfill.PatchedMediaKeysApple = class {
150150

151151
goog.asserts.assert(event.initData != null, 'missing init data!');
152152

153+
// Convert the prefixed init data to match the native 'encrypted' event.
154+
const uint8 = shaka.util.BufferUtils.toUint8(event.initData);
155+
const dataview = shaka.util.BufferUtils.toDataView(uint8);
156+
// The first part is a 4 byte little-endian int, which is the length of
157+
// the second part.
158+
const length = dataview.getUint32(
159+
/* position= */ 0, /* littleEndian= */ true);
160+
if (length + 4 != uint8.byteLength) {
161+
throw new RangeError('Malformed FairPlay init data');
162+
}
163+
// The remainder is a UTF-16 skd URL. Convert this to UTF-8 and pass on.
164+
const str = shaka.util.StringUtils.fromUTF16(
165+
uint8.subarray(4), /* littleEndian= */ true);
166+
const initData = shaka.util.StringUtils.toUTF8(str);
167+
153168
// NOTE: Because "this" is a real EventTarget, the event we dispatch here
154169
// must also be a real Event.
155170
const event2 = new Event('encrypted');
156-
// TODO: validate this initDataType against the unprefixed version
157-
event2.initDataType = 'cenc';
158-
event2.initData = shaka.util.BufferUtils.toArrayBuffer(event.initData);
171+
event2.initDataType = 'skd';
172+
event2.initData = shaka.util.BufferUtils.toArrayBuffer(initData);
159173

160174
this.dispatchEvent(event2);
161175
}

lib/util/fairplay_utils.js

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
goog.provide('shaka.util.FairPlayUtils');
77

88
goog.require('goog.Uri');
9+
goog.require('goog.asserts');
910
goog.require('shaka.util.BufferUtils');
1011

1112

@@ -23,19 +24,7 @@ shaka.util.FairPlayUtils = class {
2324
* @export
2425
*/
2526
static defaultGetContentId(initData) {
26-
const uint8 = shaka.util.BufferUtils.toUint8(initData);
27-
const dataview = shaka.util.BufferUtils.toDataView(uint8);
28-
// The first part is a 4 byte little-endian int, which is the length of
29-
// the second part.
30-
const length = dataview.getUint32(
31-
/* position= */ 0, /* littleEndian= */ true);
32-
if (length + 4 != uint8.byteLength) {
33-
throw new RangeError('Malformed FairPlay init data');
34-
}
35-
36-
// The second part is a UTF-16 LE URI from the manifest.
37-
const uriString = shaka.util.StringUtils.fromUTF16(
38-
uint8.subarray(4), /* littleEndian= */ true);
27+
const uriString = shaka.util.StringUtils.fromBytesAutoDetect(initData);
3928

4029
// The domain of that URI is the content ID according to Apple's FPS
4130
// sample.
@@ -71,7 +60,7 @@ shaka.util.FairPlayUtils = class {
7160
}
7261

7362
// From that, we build a new init data to use in the session. This is
74-
// composed of several parts. First, the raw init data we already got.
63+
// composed of several parts. First, the init data as a UTF-16 sdk:// URL.
7564
// Second, a 4-byte LE length followed by the content ID in UTF-16-LE.
7665
// Third, a 4-byte LE length followed by the certificate.
7766
/** @type {BufferSource} */
@@ -83,8 +72,13 @@ shaka.util.FairPlayUtils = class {
8372
contentIdArray = contentId;
8473
}
8574

75+
// The init data we get is a UTF-8 string; convert that to a UTF-16 string.
76+
const sdkUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
77+
const utf16 =
78+
shaka.util.StringUtils.toUTF16(sdkUri, /* littleEndian= */ true);
79+
8680
const rebuiltInitData = new Uint8Array(
87-
8 + initData.byteLength + contentIdArray.byteLength + cert.byteLength);
81+
12 + utf16.byteLength + contentIdArray.byteLength + cert.byteLength);
8882

8983
let offset = 0;
9084
/** @param {BufferSource} array */
@@ -101,10 +95,12 @@ shaka.util.FairPlayUtils = class {
10195
append(array);
10296
};
10397

104-
append(initData);
98+
appendWithLength(utf16);
10599
appendWithLength(contentIdArray);
106100
appendWithLength(cert);
107101

102+
goog.asserts.assert(
103+
offset == rebuiltInitData.length, 'Inconsistent init data length');
108104
return rebuiltInitData;
109105
}
110106
};

0 commit comments

Comments
 (0)