Files
shaka-player/docs/tutorials/fairplay.md
T
Vincent Valot 6d76a135e5 feat: add modern EME support for FairPlay (#3776)
Add support for HLS com.apple.streamingkeydelivery through MSE/EME implementation.

Close #3346

## Tests
Tested on:
- Mac 11.6 Safari 15.2
- iOS 15.2 Safari 15.2
- Mac 11.6 Chrome 96 (for potential regressions on Widevine keySystem)

| Mode | DRM API | TS | CMAF (mono-key and multi-keys)
|---|---|---|---|
| file | EME |   |   |
| file | Legacy-prefixed |    |    |
| media-source | EME | **mux-js**: `encrypted` never fired<br />**real MSE**: `encrypted` event received, but with incorrect `sinf` initData (*1)  |   |
| media-source | Legacy-prefixed | **mux-js**: `webkitneedkey` never fired<br/>**real MSE**: TBD  | 🔴 fails to append media segment to SourceBuffer (init segment ok) `(video:4) – "failed fetch and append: code=3015"` |

## Support table 
| Mode | DRM API | TS | CMAF (mono-key and multi-keys)
|---|---|---|---|
| file | EME |   |   |
| file | Legacy-prefixed |    |    |
| media-source | EME | 🚫 `4040: HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED`  |   |
| media-source | Legacy-prefixed | 🚫 `4041: HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED`  |🚫 `4041: HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED` |

⚠️ Use EME APIs with multi-keys CMAF makes the video stalling with the audio continuing alone after a short time (~3 minutes in the stream, could be shorter, could be longer). Didn't find an explanation to that yet. I've observed the same behaviour with hls.js code so I don't think this is a player issue.
2022-02-07 11:17:22 -08:00

2.8 KiB

FairPlay Support

We support FairPlay with EME on compatible environments or native src=. Adding FairPlay support involves a bit more work than other key systems.

Server certificate

All FairPlay content requires setting a server certificate. You can either provide it directly or set a serverCertificateUri for Shaka to fetch it for you.

const req = await fetch('https://example.com/cert.der');
const cert = await req.arrayBuffer();

player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificate',
                 new Uint8Array(cert));
player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri',
                 'https://example.com/cert.der');

Content ID

Note: This only applies when legacy Apple Media Keys is used.

Some FairPlay content use custom signaling for the content ID. The content ID is used by the browser to generate the license request. If you don't use the default content ID derivation, you need to specify a custom init data transform:

player.configure('drm.initDataTransform', (initData, initDataType) => {
  if (initDataType != 'skd')
    return initData;

  // 'initData' is a buffer containing an 'skd://' URL as a UTF-8 string.
  const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
  const contentId = getMyContentId(skdUri);
  const cert = player.drmInfo().serverCertificate;
  return shaka.util.FairPlayUtils.initDataTransform(initData, contentId, cert);
});

License wrapping

Some FairPlay servers need to accept the license request in a different format or give the response in a different format. For more info, see the general {@tutorial license-wrapping} tutorial:

player.getNetworkingEngine().registerRequestFilter((type, request) => {
  if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
    return;
  }

  const originalPayload = new Uint8Array(request.body);
  const base64Payload =
      shaka.util.Uint8ArrayUtils.toStandardBase64(originalPayload);
  const params = 'spc=' + base64Payload;
  request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  request.body = shaka.util.StringUtils.toUTF8(encodeURIComponent(params));
});

player.getNetworkingEngine().registerResponseFilter((type, response) => {
  if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
    return;
  }

  let responseText = shaka.util.StringUtils.fromUTF8(response.data);
  // 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);
  }

  // Decode the base64-encoded data into the format the browser expects.
  response.data = shaka.util.Uint8ArrayUtils.fromBase64(responseText).buffer;
});