VTDecompressionSessionInvalidate hangs on iOS 11

Hi,


I've got a major issue with my code when running it on iOS 11 (beta 5 to GM). For some reason the exact same code on iOS 10 works perfectly well, but hangs during deinitialization on iOS 11.

I've tracked down the bug and am able to reproduce it in a very simple sample project (see below). What happens is:


When using the VTDecompressionSession API, I can successfully decode compressed video frames. I then invalidate the session using VTDecompressionSessionInvalidate. However on iOS 11, the invalidation step hangs indefinitely. The problem also arises if I call VTDecompressionSessionWaitForAsynchronousFrames (which I don't need, as I don't decode frames asynchronously).


The bug is 100% reproducible on an iPhone 7 running iOS 11. There is no issue on an iPhone 6s running iOS 10.3.


To solve the problem, I tried using a VTDecompressionOutputCallbackRecord instead of specifying the callback in VTDecompressionSessionDecodeFrameWithOutputHandler. I also tried the various flags: asynchronous decompression, real time playback, temporal processing. Finally, I also tried dispatching invalidation to another queue. This did not change anything.


import AVFoundation
import ************
class VT {
    private let asset: AVAsset
    private let track: AVAssetTrack
    private let reader: AVAssetReader
    private let output: AVAssetReaderOutput
    init() {
        let url = Bundle.main.url(forResource: "video", withExtension: "mp4")!
        self.asset = AVAsset(url: url)
        self.track = self.asset.tracks(withMediaType: .video).first!
        self.reader = try! AVAssetReader(asset: self.asset)
        self.output = AVAssetReaderTrackOutput(track: self.track, outputSettings: nil)
        self.reader.add(self.output)
    }
    func startReading() {
        self.reader.startReading()
        let formatDescription = self.track.formatDescriptions.first! as! CMVideoFormatDescription
        var decompressionSession: VTDecompressionSession?
        VTDecompressionSessionCreate(nil, formatDescription, nil, nil, nil, &decompressionSession)
        print("Reading buffers...")
        var i = 0
        while let buffer = self.output.copyNextSampleBuffer() {
            i += 1
            VTDecompressionSessionDecodeFrameWithOutputHandler(decompressionSession!, buffer, [], nil) {
                (status, _, imageBuffer, _, _) in
                if (status != noErr) { print("Error") }
                if (imageBuffer == nil) { print("No image in this buffer.") }
            }
        }
        print("Read \(i) buffers.")
        print("Invalidating the session...")
        // This hangs on iOS 11.
        VTDecompressionSessionInvalidate(decompressionSession!)
        // This would also hang.
//        VTDecompressionSessionWaitForAsynchronousFrames(decompressionSession!)
        // Hence this is printed only on iOS 10.
        print("Done.")
    }
}


To reproduce the problem, run the sample project on an iPhone 7 running iOS 11 GM, and observe that VTDecompressionSessionInvalidate hangs the execution of the program by pausing the debugger. You can also simply see that "Done.", supposed to be printed after the session invalidation, is never written to the debugger console.

The video I use is encoded on iOS in H264 using AVFoundation (and SDAVAssetExportSession). As it happens on all of the videos I tried (encoded in the same way), it shouldn't be too hard to produce a similar video for testing. However I can provide one if needed.


Is this a known issue? Has there been an API change? Using this somewhat low-level API, I don't understand how come nobody else has had the problem up to now?


Cheers,


Flo

Accepted Reply

OK so, a bit of good news on my side, and some more information for everyone: I found one of the reasons why WaitForAsynchronousFrames won't return.

It turns out that if DecodeFrameWithOutputHandler (or DecodeFrame) returns an error, the buffer will not be considered as "processed", and it looks like this is what WaitForAsynchronousFrames is waiting for indefinitely. More specifically, I was feeding buffers that sometimes did not contain any frames to decompress. While this is harmless on iOS 10, on iOS 11 it seems to be the cause of the deadlock later on when releasing the decompression session.

Hence in my case the workaround is simply to check that the number of samples in the buffer is non-zero before feeding it to the decompression session, which is probably good practice anyways.


Doing this, I'm not experiencing deadlocks anymore, and although I use several decompression sessions at the same time, the workaround provided by ceagleston does not seem to be necessary.

Replies

I encountered the same problem too. I have been using the VTDecompressionSessionInvalidate API of ************ in my App. Before iOS10, it works well. But it will hang(stuck) in iOS11 so that the App was killed by system. Some user of Our Apps gave us the feedback that our app will hang while stopping broadcasting after they update the system to iOS11. Luckily, I got the stack and hope that Apple could fix the bug of VTDecompressionSessionInvalidate API. Here is the stack:

0 libsystem_kernel.dylib 0x0000000184905150 __psynch_cvwait + 8

1 libsystem_pthread.dylib 0x0000000184a1ad40 _pthread_cond_wait$VARIANT$mp + 640

2 CoreMedia 0x0000000187ec44f8 WaitOnCondition + 16

3 CoreMedia 0x0000000187ec4440 FigSemaphoreWaitRelative + 168

4 ************ 0x000000018840b0d8 VTDecompressionSessionRemote_WaitForAsynchronousFrames + 120

5 ************ 0x000000018840aeb8 VTDecompressionSessionRemote_Invalidate + 88

6 ************ 0x00000001883b1e44 VTDecompressionSessionInvalidate + 52

7 XXXXX 0x0000000102a21094 -[hw264decoder releasedecoder] + 3641492 (XXXXXX.m:221)

Thank you for your reply rogerlinjw. I thought I was going mad being the only one experiencing the issue.

I have the exact same stack trace.

Unfortunately, it looks like the issue is still occuring on iOS 11 GM.

Hi flofromnull,


I'm also experiencing this issue in the iOS 11 GMs, and we've had scattered reports of the deadlock during the beta releases as well.


I work at Twilio, and we maintain a fork of Chromium WebRTC. What I found is that it is possible to workaround this issue assuming that you can meet the following API contract in your own code:


1. You will not be enqueuing any more frames to VTDecompressionSession after beginning the teardown process.

2. Your code is the sole owner of VTDecompressionSession, and thus you don't need the multi-owner invalidation semantics that it provides.


Luckily in my case the WebRTC implementation makes these two guarantees, so I proceeded with the following Objective-C++ implementation:


void H264VideoToolboxDecoder::DestroyDecompressionSession() {
    if (decompression_session_) {
        // Prevent deadlocks in VTDecompressionSessionInvalidate by waiting for the frames to complete manually.
        // WebRTC should already provide a guarantee that no more frames will be delivered while this method is being called
        // We are the single owner of the Session so we do not need the invalidation semantics that it provides.
        VTDecompressionSessionWaitForAsynchronousFrames(decompression_session_);
        CFRelease(decompression_session_);
        decompression_session_ = nullptr;
    }
}


From my reading of your example code using AVAssetWriter I would assume that you can make the same guarantees. When you got the deadlock were you calling both Invalidate and WaitForAsynchronousFrames on iOS 11?


I hope this was helpful. We are putting this change through testing, and so far it looks good. I have an integration test which reproduced the deadlock pretty frequently where 3 VTCompressionSessions and 9 VTDecompressionSessions were being used simultaneously.


Best,

Chris Eagleston

We encountered the same issue with our decoding process.

I reported a bug to Apple regarding the dead lock.

Thank you for the snippet, it seems to solve the issue.

Hey iOS Dev @ GIROPTIC,


I'm glad I could help. Since you've filed a bug with Apple would you mind posting it on OpenRadar? I haven't filed anything yet, and would prefer not to duplcate our efforts if possible.


I tried searching for:

http://www.openradar.me/search?query=VTDecompressionSession


But didn't see anything new.


Regards,

Chris

Hi ceagleston,


Thank you very much for your reponse.


Yes, it would seem that I have the same guarantees. However I am not sure how to solve the issue using your workaround. In my case, a simple call to VTDecompressionSessionWaitForAsynchronousFrames is enough to make the program hang, as stated in the sample code in the original post.

You also mention that you could reproduce the deadlock "pretty frequently", meaning that it didn't always occur? In the sample code I provided, the hang seems to be deterministic and happens on all runs.


To answer your question, calling only VTDecompressionSessionWaitForAsynchronousFrames or Invalidate is enough to get a deadlock.

Actually, switching to ObjC and simply calling CFRelease gives me the following stack (and deadlock):

frame #0: 0x0000000180edd150 libsystem_kernel.dylib`__psynch_cvwait + 8
frame #1: 0x0000000180ff2d30 libsystem_pthread.dylib`_pthread_cond_wait$VARIANT$mp + 640
frame #2: 0x00000001844bb258 CoreMedia`WaitOnCondition + 20
frame #3: 0x00000001844bb180 CoreMedia`FigSemaphoreWaitRelative + 168
frame #4: 0x0000000184b59368 ************`VTDecompressionSessionRemote_WaitForAsynchronousFrames + 312
frame #5: 0x0000000184b58f80 ************`VTDecompressionSessionRemote_Invalidate + 88
frame #6: 0x0000000184aed33c ************`VTDecompressionSessionInvalidate + 52
frame #7: 0x0000000184aed6f8 ************`vtDecompressionSessionFinalize + 212
frame #8: 0x000000018136faac CoreFoundation`_CFRelease + 224


It therefore seems that WaitForAsynchronousFrames always end up getting called and is the source of the problem, independently of whether it is called from Invalidate or not.


Did I miss something in how you solved this problem? Would you mind sharing how you use the VTDecompressionSession (i.e. asynchronously or not, using a CallbackRecord or OutputHandler)? If I remember correctly I had already fiddled with various configurations, but knowing yours might be helpful.

Simply getting the sample code of the original post to work would be awesome.


Thanks,


Flo

OK so, a bit of good news on my side, and some more information for everyone: I found one of the reasons why WaitForAsynchronousFrames won't return.

It turns out that if DecodeFrameWithOutputHandler (or DecodeFrame) returns an error, the buffer will not be considered as "processed", and it looks like this is what WaitForAsynchronousFrames is waiting for indefinitely. More specifically, I was feeding buffers that sometimes did not contain any frames to decompress. While this is harmless on iOS 10, on iOS 11 it seems to be the cause of the deadlock later on when releasing the decompression session.

Hence in my case the workaround is simply to check that the number of samples in the buffer is non-zero before feeding it to the decompression session, which is probably good practice anyways.


Doing this, I'm not experiencing deadlocks anymore, and although I use several decompression sessions at the same time, the workaround provided by ceagleston does not seem to be necessary.

Hello, can you post the modified code?