All posts

AVPlayerLooper crashes if you replace it too early. Your code won't be in the stack.

One of our top iOS crashes had no frame of ours in the stack trace. AVPlayerLooper finishes its setup after init returns, and replacing it before that runs crashes the app. The fix is one line.

Lucas Firmo 2 min read

One of the top crashes in our iOS app looked like this in Crashlytics:

-[AVPlayerItem _nameForLogging]

No frame of ours. Nothing to click. Just AVFoundation, the main queue and a message:

An AVPlayerItem can occupy only one position in a player's queue at a time.

The app has a few screens with a looping video in the background, built on AVQueuePlayer and AVPlayerLooper. So the crash was ours somehow. The stack just didn’t say where.

What AVPlayerLooper does after init

This is the part I didn’t know. AVPlayerLooper(player:templateItem:) doesn’t fill the queue inside init. It schedules that work on the main queue and returns right away. The full stack shows it:

-[AVQueuePlayer insertItem:afterItem:]
-[AVPlayerLooper _setupLoopingReturningError:]
__78-[AVPlayerLooper initWithPlayer:templateItem:timeRange:existingItemsOrdering:]_block_invoke_2
_dispatch_call_block_and_release

That block_invoke is the setup, running later, on its own.

Now picture a view that loads a video, and then loads another one in the same run loop turn. A configuration called twice, a cell reused fast, a state update that lands right after the first one:

func load(_ url: URL) {
    player.removeAllItems()
    looper = AVPlayerLooper(player: player, templateItem: AVPlayerItem(url: url))
}

load(first)
load(second) // before the first looper's setup ran

The first looper is released, but its setup block is already on the main queue. It still runs, and it inserts items into a queue the second looper is filling too. AVFoundation throws, and since the throw happens inside that deferred block, nothing of yours is on the stack anymore.

The fix is one line

func load(_ url: URL) {
    looper?.disableLooping() // cancels the setup the old looper still has queued
    player.removeAllItems()
    looper = AVPlayerLooper(player: player, templateItem: AVPlayerItem(url: url))
}

disableLooping() stops the old looper, including the setup it hasn’t done yet.

Proving it without a device

The nice surprise: this reproduces on macOS, in a command line tool of about 40 lines. No simulator, no app. Each case ran five times on Xcode 27:

CaseResult
replace the looper before its setup runscrashed 5 of 5
same, with disableLooping() firstsurvived 5 of 5
replace it half a second latersurvived 5 of 5

The code is here if you want to try it: github.com/lucasdomene/avplayerlooper-crash.

What I took from it

When a crash has no frame of yours, look for work the framework deferred. Something you called finished later, on a queue, after your code had moved on. The stack shows where it blew up, not who caused it.

And if you replace an AVPlayerLooper, disable the old one first.

Have you hit a crash where your own code wasn’t in the stack at all? What was it?

Get the next post by email

One email when something new is out. No spam.