Source#
Apple Developer Forums — “Playing FairPlay encrypted content works fine on ios17 but won't play on ios26” and related iOS 26 FairPlay reports.
Problem#
A FairPlay-protected HLS asset plays on iOS 17, but after updating the same device/app/content to iOS 26, tapping play does not advance playback. The unusual discriminator is that seeking/scrubbing forward can still display decrypted frames.
This means the investigation should not collapse all FairPlay failures into “license acquisition failed.”
seek / scrub
→ frame becomes available
→ protected sample can apparently be resolved/decrypted
play()
→ continuous time does not advance
Evidence level#
Apple Developer Forums report + inferred execution boundary.
No public Apple statement currently confirms the internal root cause. The fact that frames can be produced after a seek strongly weakens, but does not completely eliminate, a blanket “no usable FairPlay key” hypothesis.
Key distinction#
Treat these as separate states:
SPC/CKC exchange succeeds
↓
AVContentKey becomes usable
↓
protected sample can be decrypted
↓
AVPlayer transport accepts non-zero rate
↓
player enters .playing
↓
timebase advances continuously
↓
A/V render continuously
A successful seek-to-frame only proves progress through part of this chain. It does not prove that the player’s transport state machine is willing to run continuously.
First checks#
Instrument the transition around play() rather than only logging the FairPlay delegate:
player.play()
print("rate", player.rate)
print("timeControlStatus", player.timeControlStatus.rawValue)
print("reasonForWaiting", String(describing: player.reasonForWaitingToPlay))
print("itemStatus", player.currentItem?.status.rawValue ?? -1)
print("error", String(describing: player.currentItem?.error))Also observe:
AVPlayer.rateDidChangeNotificationtimeControlStatusreasonForWaitingToPlayAVPlayerItem.statusisPlaybackBufferEmptyisPlaybackLikelyToKeepUpAVPlayerItem.errorLog()AVPlayerItem.accessLog()- content-key request lifecycle and key identifier
- periodic player time
The decisive distinction is:
play() requested
+ rate requested > 0
+ timeControlStatus = waiting
→ investigate the wait reason / readiness gate
versus:
play() requested
+ timeControlStatus remains paused
→ investigate transport rejection / rate reset / policy gate
versus:
timeControlStatus = playing
+ currentTime does not advance
→ investigate timebase / decode pipeline / renderer progression
Why scrubbed frames matter#
A seek and continuous playback do not necessarily exercise the same runtime path.
A useful model is:
seek(target)
→ resolve target media
→ obtain/decrypt samples near target
→ decode enough data for a presentation frame
→ present frame
while normal playback requires:
play()
→ select requested rate
→ satisfy startup gates
→ start item timebase
→ continuously schedule encrypted samples
→ maintain key/decrypt state
→ decode
→ render
Therefore:
“I can see a frame after +10 seconds” is evidence that some protected media can be resolved and rendered; it is not evidence that every prerequisite for continuous playback is satisfied.
A/B verification matrix#
Use the exact same asset and key service:
A. iOS 17 + FairPlay HLS
B. iOS 26 + FairPlay HLS
C. iOS 26 + identity/AES test version of same authored content
D. iOS 26 + comparable clear HLS
For A/B capture the state immediately before play(), immediately after, and after 100 ms / 1 s / 5 s:
rate
timeControlStatus
reasonForWaitingToPlay
currentTime
AVPlayerItem.status
buffer flags
selected variant
key request lifecycle
error log
access log
Interpretation:
A works, B fails
→ OS-version-specific behavior is established
B fails, C works
→ FairPlay-specific path becomes primary
B and C fail, D works
→ protected/authoring path remains suspect, but not necessarily KSM
B reports waitingToPlayAtSpecifiedRate
→ follow reasonForWaitingToPlay and startup readiness
B remains paused after play()
→ investigate rate/transport policy before blaming buffering
Apple TN2454’s KEYFORMAT="identity" diagnostic remains useful for separating FairPlay key delivery from content-authoring/playback behavior. It must only be used for controlled testing, never production.
Important anti-pattern#
Do not infer:
FairPlay content does not play
→ CKC is wrong
when the evidence already shows a decrypted frame can be produced.
Instead ask where the first divergence occurs:
key request
→ key usable
→ protected sample resolved
→ frame decode
→ play() request
→ transport state
→ timebase progression
→ continuous render
Client-side mitigation / diagnostics#
Until the root cause is confirmed:
- Keep FairPlay delegate/session lifetime stable across the entire player-item lifetime.
- Log the exact transition of
timeControlStatus,reasonForWaitingToPlay, andratearoundplay(). - Avoid treating
rate == 0alone as a buffering diagnosis. - Compare
play()withplayImmediately(atRate:)only as a diagnostic discriminator; do not use it to mask a persistent platform failure. - Capture CoreMedia HTTP Live Streaming / Media Playback diagnostics and sysdiagnose for the affected iOS 26 build.
- Reproduce with the smallest FairPlay asset and the same KSM on an older working OS and the affected OS.
Engineering rule#
DRM key usability, one-frame decode, transport authorization/readiness, and continuous timebase progression are different states. A FairPlay investigation should locate the first transition that fails instead of using “DRM playback failed” as the root cause.
References#
- Apple Developer Forums — FairPlay content works on iOS 17 but not iOS 26
- Apple AVPlayer documentation: https://developer.apple.com/documentation/avfoundation/avplayer
- Apple
reasonForWaitingToPlay: https://developer.apple.com/documentation/avfoundation/avplayer/reasonforwaitingtoplay - Apple TN2454 — Debugging FairPlay Streaming: https://developer.apple.com/library/archive/technotes/tn2454/_index.html