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.
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 callplayer.load()with a FairPlay-encrypted HLS stream:Key environment detail: Modern Safari (17+) has both native EME (
navigator.requestMediaKeySystemAccess) and the legacy prefixed API (WebKitMediaKeys). The same stream plays correctly using manualWebKitMediaKeys— the issue is specific to Shaka's managed EME pipeline.Minimal reproduction page
What is the expected behaviour?
Shaka should manage the full FairPlay EME lifecycle: fetch certificate, attach MediaKeys, set
video.src, handleencryptedevent, runinitDataTransform, exchange license, start playback — the sameconfigure → loadpattern that works for Widevine.What is the actual behaviour?
player.load()fails immediately with error 3016 (VIDEO_ERROR,MEDIA_ERR_SRC_NOT_SUPPORTED). Noencryptedevent fires.initDataTransformis 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:
needWaitForEncryptedEventdeadlock (drm_engine.js+abstract_device.js)In
DrmEngine.attach()(line ~608):Safari's requirement:
WebKitMediaKeysmust be attached to the video element beforevideo.srcis set for FairPlay HLS. Without keys attached, Safari's native HLS parser encounters#EXT-X-KEYand immediately errors withMEDIA_ERR_SRC_NOT_SUPPORTED.Shaka's assumption: For
com.apple.fps, delay MediaKeys attachment untilencryptedfires.Deadlock: Safari won't fire
encryptedwithout keys → Shaka won't attach keys withoutencrypted.Even with
needWaitForEncryptedEventpatched to returnfalse, there's a second guard on the sameifblock:this.currentDrmInfo_.keySystem !== 'com.apple.fps'evaluates tofalse, andmanifestInitData_is alwaysnullin src= mode (Safari's native HLS parses the manifest, not Shaka). SoattachMediaKeys_()is still skipped.Bug 2: Apple polyfill skipped on modern Safari (
patchedmediakeys_apple.js)defaultInstall()(line ~32) skips when native EME exists:But Safari's native
video.setMediaKeys()does not enable FairPlay decryption for native HLS playback — onlyvideo.webkitSetMediaKeys()does. The Apple polyfill is required to route the standardsetMediaKeys()throughwebkitSetMediaKeys(), 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()deferswebkitSetMediaKeys()untilHAVE_METADATA(line ~485):The video never reaches
HAVE_METADATAbecause Safari errors atreadyState=0when it can't process FairPlay HLS without keys.webkitSetMediaKeys()must be called immediately.b) Wrong key system string for
WebKitMediaKeys(line ~427):WebKitMediaKeysrequires'com.apple.fps.1_0'for FairPlay HLS decryption. WhileWebKitMediaKeys.isTypeSupported('com.apple.fps', 'video/mp4')returnstrue, 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 FairPlayif (!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
encryptedevents are always listened for with FairPlay (needed for the license flow):2.
lib/polyfill/patchedmediakeys_apple.js— Three fixesa) 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:c) Remap key system string:
Working flow after patches
I'm happy to submit a PR with these changes if the approach looks reasonable.