Skip to content

iOS 26: ~20s main-thread hang on every play of a live (ICY/SHOUTcast) stream #105

Description

@pke

iOS 26: ~20s main-thread hang on every play of a live (ICY/SHOUTcast) stream

Summary

On iOS 26, starting playback of a live HTTP stream (SHOUTcast/Icecast, indefinite
duration) freezes the entire app for ~20 seconds: the UI stops responding,
touches are queued and delivered late, and audio only starts once the freeze
resolves. Backgrounding the app during the freeze gets it killed by the watchdog
(0x8BADF00D, "failed to terminate gracefully").

Older iOS versions are not affected — the same app + library were fine for years.
The regression is triggered by an iOS-side behavior change, but the root cause is
in SwiftAudioEx (bundled by react-native-track-player 4.x).

Root cause

AVPlayerWrapper's load completion inspects the asset's chapters/metadata with
synchronous AVAsset property getters, executed on the main thread:

if pendingAsset.availableChapterLocales.count > 0 {
    for locale in pendingAsset.availableChapterLocales { ... }
} else {
    for format in pendingAsset.availableMetadataFormats {
        let timeRange = CMTimeRange(start: ..., end: pendingAsset.duration)
        ...
    }
}

None of these keys (availableChapterLocales, availableMetadataFormats,
duration) are included in the preceding loadValuesAsynchronously(forKeys:)
call — only "playable" is — so each getter resolves synchronously via an XPC
round-trip to mediaserverd. For a live stream with indefinite duration,
iOS 26's media daemon only answers that query after an internal ~20s timeout.
The main thread sits in the XPC reply wait the whole time.

Watchdog backtrace captured mid-hang (iPhone 13 Pro, iOS 26.5):

Thread 0 (main, triggered):
  libsystem_kernel  mach_msg2_trap
  libdispatch       _dispatch_mach_send_and_wait_for_reply
  libxpc            xpc_connection_send_message_with_reply_sync
  CoreMedia         FigXPCConnectionSendSyncMessageCreatingReply
  MediaToolbox      remoteXPCAsset_CopyPropertyAndBlockageWarning
  AVFCore           -[AVFigAssetInspector _valueAsCFTypeForProperty:]
  AVFCore           -[AVAsset(AVAssetChapterInspection) _loadChapterInfo]
  AVFCore           -[AVAsset(AVAssetChapterInspection) availableChapterLocales]
  (app)

Instruments' Hangs instrument records it as a single "Severe Hang", 20.11 s,
starting the moment the item is prepared.

Reproduction

  • Any app using react-native-track-player 4.x (or SwiftAudioEx ≥ 1.0.0-rc-era)
    on iOS 26, playing a live SHOUTcast/Icecast URL. We reproduced with
    http://hi5.streamingsoundtracks.com/; (ICY 200 OK live MP3).
  • Every transition into play (initial autoplay and every stopped→playing)
    hangs the main thread ~20s. The iOS Simulator does not reproduce (its
    media daemon answers the chapter query immediately).

Suggested fix

Load the inspected keys asynchronously before reading them, e.g. include
"availableChapterLocales", "availableMetadataFormats" and "duration" in
the loadValuesAsynchronously(forKeys:) request and only touch them in its
completion — and/or skip chapter inspection entirely when
pendingAsset.duration.isIndefinite (live streams have no chapters).

Either variant removes the synchronous main-thread XPC fetch without changing
the public API. Happy to send a PR.

Workaround for affected apps (CocoaPods)

Patch the block out in a post_install hook (live radio has no chapters; ICY
timed metadata still arrives via the player-item observer):

['Sources/SwiftAudioEx', 'SwiftAudioEx/Classes'].each do |base|
  wrapper = File.join(__dir__, 'Pods', 'SwiftAudioEx', base,
                      'AVPlayerWrapper', 'AVPlayerWrapper.swift')
  next unless File.exist?(wrapper)
  contents = File.read(wrapper)
  fixed = contents.gsub(
    /^ +if pendingAsset\.availableChapterLocales\.count > 0 \{.*?\n +\}\n/m,
    "// patched: sync chapter/metadata inspection removed (20s main-thread hang on live streams under iOS 26)\n"
  )
  if contents != fixed
    File.chmod(0644, wrapper)
    File.write(wrapper, fixed)
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions