AVCaptureSession and concurrency

I'm having trouble using AVCaptureSession with SwiftUI and complete concurrency checks enabled.

My Camera class is MainActor isolated for simplicity, this alone eliminates a lot of concurrency warnings in the code. However, I'm getting run-time warnings on AVCaptureSession.startRunning() that it should be called on a background thread. Previously I would use DispatchQueue with a background priority queue but with Swift 5.10 that gives a "non-sendable type AVCaptureSession in asynchronous access" warning:

@MainActor
class Camera {
	private let captureSession = AVCaptureSession()
	// ...

	func start() async {
		// ...
		DispatchQueue.global(qos: .background).async {
			self.captureSession.startRunning()
		}
	}
}

On the other hand, the following compiles but still gives a run-time warning:

@MainActor
class Camera {
	private let captureSession = AVCaptureSession()
	// ...

	func start() async {
		// ...
		Task(priority: .background) {
			captureSession.startRunning()
		}
	}
}

What is the proper way of resolving this?

Also, shouldn't there be a more modern camera API already?

I suggest use actor with custom executor to wrap camera behaviors, and executor would be a dispatch queue you create for camera. In that way you can set session delegate queue to this background queue as well, so everything is executed on that queue. That’s probably the most convenient way as for now to connect old-style dispatch queue APIs like camera has to new concurrency.

I'm not entirely sure that your solution would eliminate the "non-sendable AVCaptureSession" warnings, because inevitably my captureSession property will end up in a non-MainActor context. Why MainActor: most of the camera callbacks and delegate methods are required to be on MainActor since the API is quite old.

Could you show a piece of code?

It actually required to be on the queue you set delegate to. It can be main queue/actor, though it’s not recommended practice.

By capture session and all other related parties being isolated to an actor, your accesses to it would be ensured to happen only on that actor and never leave it, which will eliminate sendability warnings and give you compile time safety for concurrency.

Roughly, that would be

actor Camera { 
    // don’t remember exactly naming for a custom executor, so that can be a little incorrect naming
    nonisolated var unownedExecutor: UnownedSerialExecutor { cameraQueue }
    // queue conformance to executor protocol available from iOS 17 IIRC, before that you need to implement it by yourself, but that’s easy
    private var cameraQueue = DispatchSerialQueue(…)
    
    // isolated to actor and ensure being called from cameraQueue 
    private let session = AVCaptureSession()
}
3 Likes

While @vns's solution seems to make more sense, I've found a quicker fix for now, by adding preconcurrency attribute and using Task.detached:

@preconcurrency import AVFoundation

@MainActor
class Camera {
	private let captureSession = AVCaptureSession()
	// ...

	func start() async {
		// ...
		Task.detached {
			captureSession.startRunning()
		}
	}
}

Seems to work OK without any compile-time or run-time warnings.