iOS 26 can decrypt scrubbed frames while continuous AVPlayer playback never starts

GitHub source

Inferred. This page is generated from GitHub Issue #14; GitHub remains the canonical authoring and discussion source.

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.rateDidChangeNotification
  • timeControlStatus
  • reasonForWaitingToPlay
  • AVPlayerItem.status
  • isPlaybackBufferEmpty
  • isPlaybackLikelyToKeepUp
  • AVPlayerItem.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:

  1. Keep FairPlay delegate/session lifetime stable across the entire player-item lifetime.
  2. Log the exact transition of timeControlStatus, reasonForWaitingToPlay, and rate around play().
  3. Avoid treating rate == 0 alone as a buffering diagnosis.
  4. Compare play() with playImmediately(atRate:) only as a diagnostic discriminator; do not use it to mask a persistent platform failure.
  5. Capture CoreMedia HTTP Live Streaming / Media Playback diagnostics and sysdiagnose for the affected iOS 26 build.
  6. 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#