Offline HLS `.variants` can fall back to the original master URL from a downloaded `.movpkg`

GitHub source

Evidence not classified. This page is generated from GitHub Issue #13; GitHub remains the canonical authoring and discussion source.

Source#

Apple Developer Forums: https://developer.apple.com/forums/thread/786659

Resolution updated: 2026-09-10

Problem#

A FairPlay-protected HLS asset is downloaded as a local .movpkg. asset.assetCache.isPlayableOffline == true, but the first offline call to:

try await asset.load(.variants)

can intermittently fail with NSURLErrorDomain -1009 and:

AVErrorFailedDependenciesKey = (
  "assetProperty_HLSAlternates"
)

The outer error reports the local .movpkg URL. Recreating the asset/player may then succeed under the same offline conditions.

Resolution status#

Confirmed mechanism; field trigger still unresolved.

The confirmed mechanism is that load(.variants) requests CoreMedia's HLSAlternates property. For a downloaded HLS package, that property is normally built from the package's local master-playlist mapping. If the local master DataItem cannot be used, the streaming resolver falls back to the original master URL retained in the package metadata.

When that fallback occurs offline, the request fails with -1009 (-1004 when the interface is up but the origin is unreachable).

The remaining unresolved question is why an apparently intact field .movpkg can transiently fail to expose/use its local master mapping on the first attempt.

Key distinction#

assetCache.isPlayableOffline == true
        ≠
complete HLS variant graph is guaranteed to resolve locally

isPlayableOffline asks whether at least one locally cached presentation can play offline. .variants is a broader metadata operation that materializes the HLS alternate/variant graph.

Therefore this failure is a metadata-resolution failure, not proof that the downloaded presentation is unplayable.

Confirmed execution path#

AVURLAsset.load(.variants)
  → request HLSAlternates
  → initialize streaming property loader
  → translate local .movpkg
  → resolve boot.xml Master DataItem
  → resolve local master-playlist DataPath
  → parse multivariant playlist
  → obtain alternate array

The failure branch is:

local Master DataItem unavailable/unreadable
  → local multivariant state cannot satisfy HLSAlternates
  → resolver follows retained MasterPlaylist.NetworkURL
  → real HTTP GET to original master URL
  → network unavailable
  → NSURLErrorDomain -1009 / -1004
  → HLSAlternates marked failed
  → load(.variants) throws

Evidence#

Public contract#

  • AVAssetCache.isPlayableOffline does not guarantee that all associated media-selection options or all higher-level asset metadata are available offline.
  • AVAssetVariant represents variants derived from HLS multivariant playlist state.

Healthy downloaded package#

Using Apple's public BipBop HLS sample:

  • online master exposed 24 variants;
  • AVAssetDownloadURLSession produced a completed .movpkg;
  • the package reported isPlayableOffline == true;
  • boot.xml mapped DataItem(Role=Master) to a local master playlist under the package Data directory;
  • that local master still contained all 24 variant declarations even though only a subset of streams was downloaded;
  • with network denied, .variants still returned all 24 variants and direct playback succeeded.

This proves a healthy package can satisfy HLSAlternates entirely from local master metadata.

Fault-injection control#

A controlled localhost HLS origin was used with a downloaded .movpkg.

Results:

intact package + origin down
→ .variants resolves locally
→ 0 failures in 200 fresh-process attempts
→ 0 origin requests

Master DataItem removed + origin down
→ HLSAlternates fails
→ NSURLErrorDomain -1004
→ outer failing URL is local .movpkg

Master DataItem zero-byte + origin down
→ same failure

same broken package + origin up
→ .variants succeeds
→ exactly one GET /master.m3u8 observed per load

This proves the remote-fallback edge is a real network request, not a synthesized offline error.

Why the error shows the .movpkg URL#

The outer AVFoundation error identifies the asset whose property failed:

NSErrorFailingURLKey
→ local .movpkg creation URL

AVErrorFailedDependenciesKey
→ HLSAlternates

That does not mean the nested resolver requested the .movpkg over the network. The actual remote resource must be established from the underlying error chain or CoreMedia/CFNetwork/network trace.

FairPlay relationship#

FairPlay is correlated with the reported assets but is not a direct prerequisite of HLSAlternates.

The relevant paths are separate:

variant metadata:
.movpkg → master playlist → HLSAlternates → AVAssetVariant

content protection:
content-key specifier → AVContentKeySession → persisted key → decrypt → render

Unless a content-key callback precedes and blocks the HLSAlternates failure, do not classify this as a FairPlay license/key failure.

Field trigger — unresolved#

The controlled tests explain exactly what happens when local master state is unavailable, but they do not yet explain why the original intact field package transiently enters that branch on the first attempt.

Still-open hypotheses:

  1. transient download-cache translator / playlist-state publication race;
  2. terminal per-AVURLAsset async-property state combined with a transient condition;
  3. shared MediaToolbox/CoreMedia state becoming ready between attempts;
  4. the reported retry actually bypasses .variants and goes directly to player preparation;
  5. package/file coordination temporarily makes the master mapping unavailable.

Do not describe retry success as cache warming until a concrete state/cache object is identified before and after the failed attempt and the retry is shown consuming that exact state.

Client fix#

Remove .variants from the offline playback critical path.

let asset = AVURLAsset(url: packageURL)

guard asset.assetCache?.isPlayableOffline == true else {
    throw OfflinePlaybackError.packageNotPlayable
}

// Configure persisted FairPlay-key handling independently.
// Do not gate playback on asset.load(.variants).
player.replaceCurrentItem(with: AVPlayerItem(asset: asset))

For offline audio/subtitle choices, use the locally cached media-selection options exposed by AVAssetCache.

If product logic needs bitrate/codec/variant descriptors while offline, persist the required descriptors when the download is configured/completed. Treat a later .variants request as optional inspection rather than a playback prerequisite.

A bounded asset recreation may be a pragmatic workaround for a transient OS issue, but it is not a root-cause fix.

Minimal verification for the remaining field trigger#

Run the same immutable package under identical conditions:

A. asset A → load(.variants) → retry load(.variants) on asset A
B. asset C → load(.variants) → new asset D → load(.variants)
C. asset E → AVPlayerItem directly, without .variants

Capture:

process/run UUID
monotonic timestamp
AVURLAsset object identity
package path/file identity
isPlayableOffline
property status before/after
full NSError + NSUnderlyingError chain
actual consulted resource URL
AVPlayerItem readiness
AVContentKeySession callback timestamps
CoreMedia / AVFoundation / CFNetwork events

The target is the first state difference that makes the intact local Master DataItem unavailable on attempt 1 and available/bypassed on attempt 2.

Engineering rule#

Offline playable does not mean offline introspectable.

For downloaded HLS, keep these contracts separate:

local presentation playable
        ≠
all media selections cached
        ≠
complete multivariant graph locally resolvable
        ≠
all optional AVURLAsset properties network-independent

References#