AVAssetReaderOutput.copyNextSampleBuffer() sometimes hangs forever

I'm using AVAssetReaders with AVSampleBufferDisplayLayers to display multiple videos at once. I'm seeing this issue on iOS 13.1.3, 13.2b2, on various hardware like iPad 10.5 and iPad 12.9.


It works well for a while, then a random call to copyNextSampleBuffer never returns, blocking that thread indefinitely and eating up resources.

I have tried different threading approaches with no avail:

  • If copyNextSampleBuffer() and reader.cancelReading() are done on the same queue, then copyNextSampleBuffer() gets stuck and the cancelReading() never gets processed because the queue is blocked. If I manually (with the debugger) jump in on that blocked queue and execute cancelReading(), immediately an EXC_BREAKPOINT crashes the app
  • If copyNextSampleBuffer() and reader.cancelReading() are done on different queues, then copyNextSampleBuffer() crashes with EXC_BAD_ACCESS


Here's the stacktrace (same queue approach). I don't understand why it's stuck, my expectation is that copyNextSampleBuffer should always return (ie. with nil in error case).

  • VideoPlayerView: UIView with AVSampleBufferDisplayLayer
  • AVAssetFactory: Singleton with the queue that creates & manages all AVAssetReader / AVAsset* objects


* thread #22, queue = 'AVAssetFactory'
    frame #0: 0x00000001852355f4 libsystem_kernel.dylib`mach_msg_trap + 8
    frame #1: 0x0000000185234a60 libsystem_kernel.dylib`mach_msg + 72
    frame #2: 0x00000001853dc068 CoreFoundation`__CFRunLoopServiceMachPort + 216
    frame #3: 0x00000001853d7188 CoreFoundation`__CFRunLoopRun + 1444
    frame #4: 0x00000001853d68bc CoreFoundation`CFRunLoopRunSpecific + 464
    frame #5: 0x000000018f42b6ac AVFoundation`-[AVRunLoopCondition _waitInMode:untilDate:] + 400
    frame #6: 0x000000018f38f1dc AVFoundation`-[AVAssetReaderOutput copyNextSampleBuffer] + 148
    frame #7: 0x000000018f3900f0 AVFoundation`-[AVAssetReaderTrackOutput copyNextSampleBuffer] + 72
  * frame #8: 0x0000000103309d98 Photobooth`closure #1 in AVAssetFactory.nextSampleBuffer(reader=0x00000002814016f0, retval=(Swift.Optional<CoreMedia.CMSampleBuffer>, Swift.Optional<AVFoundation.AVAssetReader.Status>) @ 0x000000016dbd1cb8) at AVAssetFactory.swift:108:34
    frame #9: 0x0000000102f4f480 Photobooth`thunk for @callee_guaranteed () -> () at <compiler-generated>:0
    frame #10: 0x0000000102f4f4a4 Photobooth`thunk for @escaping @callee_guaranteed () -> () at <compiler-generated>:0
    frame #11: 0x000000010bfe6c04 libdispatch.dylib`_dispatch_client_callout + 16
    frame #12: 0x000000010bff5888 libdispatch.dylib`_dispatch_lane_barrier_sync_invoke_and_complete + 124
    frame #13: 0x0000000103309a5c Photobooth`AVAssetFactory.nextSampleBuffer(reader=0x00000002814016f0, self=0x0000000281984f60) at AVAssetFactory.swift:101:20
    frame #14: 0x00000001032ab690 Photobooth`closure #1 in VideoPlayerView.setRequestMediaLoop(self=0x000000014b8da1d0, handledCompletion=false) at VideoPlayerView.swift:254:70
    frame #15: 0x0000000102dce978 Photobooth`thunk for @escaping @callee_guaranteed () -> () at <compiler-generated>:0
    frame #16: 0x000000018f416848 AVFoundation`-[AVMediaDataRequester _requestMediaDataIfReady] + 80
    frame #17: 0x000000010bfe5828 libdispatch.dylib`_dispatch_call_block_and_release + 24
    frame #18: 0x000000010bfe6c04 libdispatch.dylib`_dispatch_client_callout + 16
    frame #19: 0x000000010bfedb74 libdispatch.dylib`_dispatch_lane_serial_drain + 744
    frame #20: 0x000000010bfee744 libdispatch.dylib`_dispatch_lane_invoke + 500
    frame #21: 0x000000010bff9ae4 libdispatch.dylib`_dispatch_workloop_worker_thread + 1324
    frame #22: 0x000000018517bfa4 libsystem_pthread.dylib`_pthread_wqthread + 276


I've tried all kinds of other things like making sure the AVAssets and all objects are made on one queue, and stopping the AVAssetReaders from ever deallocing to see if that helps. Nothing works. Any ideas?

Replies

Filed as FB7403546

Experiencing the same issue here on iOS 13.2.

Filed as FB7436253

I think I've found the proper usage, cancelReading() should be called in the same thread as copyNextSampleBuffer(), but only after copyNextSampleBuffer() is unblocked.

For instance, keep state to track cancellation, let copyNextSampleBuffer() finish returning, and cancel reading before the next iteration:

while !shouldCancel, let buffer = output.copyNextSampleBuffer() {
    ...
}
if shouldCancel {
    reader.cancelReading()
}