Skip to content

FairPlay DRM fails with MEDIA_ERR_SRC_NOT_SUPPORTED in src= mode on modern Safari #9794

Description

@josephnef

Have you read the FAQ and checked for duplicate open issues?

Yes. Found related closed issues (#4967, #3073, #3506) but none address this specific deadlock on modern Safari with native EME + src= mode.

What version of Shaka Player are you using?

5.0.4 (also reproduced on 5.0.1)

Can you reproduce the issue with our latest release version?

Yes — 5.0.4 is the latest.

What browser and OS are you using?

Safari 18.3 on macOS 15.3 (Sequoia). Also reproducible on Safari 17.x.

What are the steps to reproduce the issue?

Configure Shaka Player for FairPlay DRM with useNativeHlsForFairPlay: true (default) on modern Safari and call player.load() with a FairPlay-encrypted HLS stream:

const player = new shaka.Player();
await player.attach(video);

player.configure({
  drm: {
    servers: { 'com.apple.fps': LICENSE_URL },
    advanced: { 'com.apple.fps': { serverCertificateUri: CERT_URL } },
    initDataTransform: (initData, initDataType, drmInfo) => {
      if (initDataType !== 'skd') return initData;
      const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
      const contentId = skdUri.split('skd://').pop();
      const cert = drmInfo.serverCertificate;
      return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert);
    },
  },
});

await player.load(FAIRPLAY_HLS_STREAM);  // Fails immediately

Key environment detail: Modern Safari (17+) has both native EME (navigator.requestMediaKeySystemAccess) and the legacy prefixed API (WebKitMediaKeys). The same stream plays correctly using manual WebKitMediaKeys — the issue is specific to Shaka's managed EME pipeline.

Minimal reproduction page

<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/shaka-player@5.0.4/dist/shaka-player.compiled.min.js"></script>
</head>
<body>
<video id="video" controls></video>
<pre id="log"></pre>
<script>
const log = msg => {
  document.getElementById('log').textContent += msg + '\n';
  console.log(msg);
};

async function play() {
  const video = document.getElementById('video');
  // Replace with your FairPlay HLS stream and license server
  const STREAM = 'YOUR_FAIRPLAY_HLS.m3u8';
  const LICENSE_URL = 'YOUR_SERVER/license/fairplay';
  const CERT_URL = 'YOUR_SERVER/license/fairplay/cert';

  shaka.polyfill.installAll();
  const player = new shaka.Player();
  await player.attach(video, false);

  player.configure({
    drm: {
      servers: { 'com.apple.fps': LICENSE_URL },
      advanced: { 'com.apple.fps': { serverCertificateUri: CERT_URL } },
      initDataTransform: (initData, initDataType, drmInfo) => {
        if (initDataType !== 'skd') return initData;
        const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
        const contentId = skdUri.split('skd://').pop();
        log('initDataTransform: contentId=' + contentId);
        const cert = drmInfo.serverCertificate;
        if (!cert) throw new Error('No FairPlay server certificate');
        return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert);
      },
    },
  });

  player.addEventListener('error', e =>
    log('ERROR: code=' + e.detail.code + ' data=' + JSON.stringify(e.detail.data)));
  video.addEventListener('encrypted', e => log('encrypted event: ' + e.initDataType));
  video.addEventListener('webkitneedkey', () => log('webkitneedkey event'));
  video.addEventListener('error', () =>
    log('video error: code=' + video.error.code + ' readyState=' + video.readyState));

  log('Loading...');
  try {
    await player.load(STREAM);
    log('Playback started');
  } catch (e) {
    log('FAILED: code=' + e.code + ' — ' + e.message);
  }
}

// Verify legacy WebKitMediaKeys works (proves stream/server are fine)
if (window.WebKitMediaKeys) {
  log('WebKitMediaKeys.isTypeSupported("com.apple.fps.1_0", "video/mp4") = ' +
      WebKitMediaKeys.isTypeSupported('com.apple.fps.1_0', 'video/mp4'));
  log('Manual WebKitMediaKeys works — issue is specific to Shaka managed EME');
  log('');
}
play();
</script>
</body>
</html>

What is the expected behaviour?

Shaka should manage the full FairPlay EME lifecycle: fetch certificate, attach MediaKeys, set video.src, handle encrypted event, run initDataTransform, exchange license, start playback — the same configure → load pattern that works for Widevine.

What is the actual behaviour?

player.load() fails immediately with error 3016 (VIDEO_ERROR, MEDIA_ERR_SRC_NOT_SUPPORTED). No encrypted event fires. initDataTransform is never called.

Root cause analysis

I traced through the Shaka 5.0.4 source and identified three interrelated bugs in the FairPlay src= code path:

Bug 1: needWaitForEncryptedEvent deadlock (drm_engine.js + abstract_device.js)

In DrmEngine.attach() (line ~608):

const needWaitForEncryptedEvent = device.needWaitForEncryptedEvent(keySystem);
// Returns TRUE for 'com.apple.fps' (abstract_device.js:350)

if (!needWaitForEncryptedEvent && (this.manifestInitData_ || ...)) {
  await this.attachMediaKeys_();  // SKIPPED for FairPlay
}
// Instead, registers listener for 'encrypted' event

Safari's requirement: WebKitMediaKeys must be attached to the video element before video.src is set for FairPlay HLS. Without keys attached, Safari's native HLS parser encounters #EXT-X-KEY and immediately errors with MEDIA_ERR_SRC_NOT_SUPPORTED.

Shaka's assumption: For com.apple.fps, delay MediaKeys attachment until encrypted fires.

Deadlock: Safari won't fire encrypted without keys → Shaka won't attach keys without encrypted.

Even with needWaitForEncryptedEvent patched to return false, there's a second guard on the same if block: this.currentDrmInfo_.keySystem !== 'com.apple.fps' evaluates to false, and manifestInitData_ is always null in src= mode (Safari's native HLS parses the manifest, not Shaka). So attachMediaKeys_() is still skipped.

Bug 2: Apple polyfill skipped on modern Safari (patchedmediakeys_apple.js)

defaultInstall() (line ~32) skips when native EME exists:

if (navigator.requestMediaKeySystemAccess &&
     MediaKeySystemAccess.prototype.getConfiguration) {
  return;  // Skips on modern Safari
}

But Safari's native video.setMediaKeys() does not enable FairPlay decryption for native HLS playback — only video.webkitSetMediaKeys() does. The Apple polyfill is required to route the standard setMediaKeys() through webkitSetMediaKeys(), even on modern Safari.

Bug 3: Apple polyfill timing + key system string (patchedmediakeys_apple.js)

When the polyfill IS installed (force or otherwise), two more issues surface:

a) setMedia() defers webkitSetMediaKeys() until HAVE_METADATA (line ~485):

shaka.util.MediaReadyState.waitForReadyState(media,
    HTMLMediaElement.HAVE_METADATA, this.eventManager_, () => {
      media.webkitSetMediaKeys(this.nativeMediaKeys_);
    });

The video never reaches HAVE_METADATA because Safari errors at readyState=0 when it can't process FairPlay HLS without keys. webkitSetMediaKeys() must be called immediately.

b) Wrong key system string for WebKitMediaKeys (line ~427):

this.nativeMediaKeys_ = new WebKitMediaKeys(keySystem);
// keySystem = 'com.apple.fps' (from Shaka's config)

WebKitMediaKeys requires 'com.apple.fps.1_0' for FairPlay HLS decryption. While WebKitMediaKeys.isTypeSupported('com.apple.fps', 'video/mp4') returns true, creating keys with 'com.apple.fps' doesn't actually enable decryption — only 'com.apple.fps.1_0' does.

Proposed fix

I built Shaka 5.0.4 from source with these three patches and confirmed FairPlay playback works end-to-end (tested with both local and remote FairPlay HLS streams).

1. lib/drm/drm_engine.js — Force early MediaKeys attachment for FairPlay

     if (!needWaitForEncryptedEvent &&
         (this.manifestInitData_ ||
         this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
         this.storedPersistentSessions_.size)) {
       await this.attachMediaKeys_();
+    } else if (this.currentDrmInfo_.keySystem === 'com.apple.fps') {
+      // Safari requires MediaKeys attached BEFORE video.src for FairPlay HLS
+      await this.attachMediaKeys_();
     }

Also ensure encrypted events are always listened for with FairPlay (needed for the license flow):

-    if (needWaitForEncryptedEvent ||
+    if (needWaitForEncryptedEvent ||
+        this.currentDrmInfo_.keySystem === 'com.apple.fps' ||
         (!this.manifestInitData_ && !this.storedPersistentSessions_.size &&

2. lib/polyfill/patchedmediakeys_apple.js — Three fixes

a) Install on modern Safari (or provide a way to force it):

   static defaultInstall() {
     if (!window.HTMLVideoElement || !window.WebKitMediaKeys) {
       return;
     }
-    if (navigator.requestMediaKeySystemAccess &&
-         MediaKeySystemAccess.prototype.getConfiguration) {
-      return;
-    }
     shaka.polyfill.PatchedMediaKeysApple.install();
   }

b) Call webkitSetMediaKeys() immediately:

-    shaka.util.MediaReadyState.waitForReadyState(media,
-        HTMLMediaElement.HAVE_METADATA,
-        this.eventManager_, () => {
-          media.webkitSetMediaKeys(this.nativeMediaKeys_);
-        });
+    media.webkitSetMediaKeys(this.nativeMediaKeys_);

c) Remap key system string:

-    this.nativeMediaKeys_ = new WebKitMediaKeys(keySystem);
+    const nativeKeySystem = keySystem === 'com.apple.fps' ?
+        'com.apple.fps.1_0' : keySystem;
+    this.nativeMediaKeys_ = new WebKitMediaKeys(nativeKeySystem);

Working flow after patches

setMediaKeys(polyfillMK)
  → polyfill calls webkitSetMediaKeys(WebKitMediaKeys('com.apple.fps.1_0'))
  → readyState=0, src=(none) → succeeds, webkitKeys=set

video.src = stream.m3u8
  → webkitKeys=set, mediaKeys=set

Safari HLS parser hits #EXT-X-KEY
  → webkitneedkey fires → polyfill translates to 'encrypted' (initDataType='skd')

Shaka onEncryptedEvent_
  → initDataTransform extracts contentId
  → generateRequest creates SPC
  → license request via NetworkingEngine (filters fire) → 200 OK
  → session.update(CKC) → decryption active → playback starts

I'm happy to submit a PR with these changes if the approach looks reasonable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    browser: SafariIssues affecting Safari or WebKit derivativescomponent: EMEThe issue involves the Encrypted Media Extensions web APIcomponent: FairPlayThe issue involves the FairPlay DRMplatform: iOSIssues affecting iOSplatform: macOSIssues affecting macOSpriority: P1Big impact or workaround impractical; resolve before feature releasetype: bugSomething isn't working correctly

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions