AVCam Example: Can I use a Actor instead of a DispatchQueue for capture session activity?

I've been studying the AVCam example and notice that everything pertaining to state transitions for the capture session is performed on a dedicated DispatchQueue. My question is this: Can I use an actor instead?

Answered by DTS Engineer in 788923022
Your answer would seem to imply that this is not possible

It depends on the nature of the delegate callbacks. If the delegate callbacks are all notifications, you can get away with just an actor. OTOH, if a delegate callback requires you to return a value, and that value depend on the state of your actor, then an actor that wraps a Dispatch queue is definitely the best option.

I’m not an expert on AVFoundation, so I’m going to cons up a standalone example. Imagine you have this old school type that uses Dispatch queues and delegates:

class Varnisher {

    init(queue: DispatchSerialQueue) {
        self.queue = queue
    }
    
    let queue: DispatchSerialQueue
    
    weak var delegate: Delegate?
    
    protocol Delegate: AnyObject {
        func varnisherShouldUseGloss(_ varnisher: Varnisher) -> Bool
    }
    
    … the actual implementation …
}

It guarantees that all the delegate methods will be run on queue. Now let’s say you want to wrangle this in an actor. Here’s how to do that:

actor VarnishCentral: Varnisher.Delegate {

    init() {
        let queue = DispatchSerialQueue(label: "VarnishCentral.queue")
        self.queue = queue
        self.varnisher = Varnisher(queue: queue)
        self.useGlossForNextWaffle = true
        
        self.varnisher.delegate = self
    }

    private let queue: DispatchSerialQueue
    private let varnisher: Varnisher
    private var useGlossForNextWaffle: Bool

    nonisolated var unownedExecutor: UnownedSerialExecutor {
        self.queue.asUnownedSerialExecutor()
    }

    nonisolated func varnisherShouldUseGloss(_ varnisher: Varnisher) -> Bool {
        self.assumeIsolated { a in
            a.shouldUseGloss()
        }
    }
    
    private func shouldUseGloss() -> Bool {
        defer { self.useGlossForNextWaffle.toggle() }
        return self.useGlossForNextWaffle
    }
}

Note the following:

  • The initialiser creates a Dispatch serial queue for use by both the actor and varnisher.

  • The unownedExecutor property relies on that queue being a valid serial executor.

  • The varnisherShouldUseGloss(_:) delegate method is marked as nonisolated.

  • It uses assumeIsolated(_:) to tell the compiler that it actually is isolated. This will trap is varnisher calls a delegate method on some other queue.

  • With an actor in hand, the delegate bounces to shouldUseGloss(), which is a standard actor-isolated method.

  • As an actor-isolated method, shouldUseGloss() can access and modify actor state directly.

  • And this chain allows for the function result to go all the way back to the varnisher.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The general technique would be to create your own actor backed with serial executor backed with a dispatch queue that you create, can access, and can then pass to AVCapture APIs that expect queue… then all code will be executed on that queue and can coexist with the code on actor.

so yes, you can do it, but I am exactly not sure whether it will build cleanly and work without issues on 5.10, should be possible easily from swift 6.

@enodev Thank you for your response. However, the answer seems to skirt the question. I don't want to use both Actor and DispatchQueue, perhaps in some sort of wrapped or nested form. I want to use one in place of the other. Your answer would seem to imply that this is not possible and that using an Actor with AVFoundation APIs that must run on something other than main, for blocking reasons, would still need a DispatchQueue.

Accepted Answer
Your answer would seem to imply that this is not possible

It depends on the nature of the delegate callbacks. If the delegate callbacks are all notifications, you can get away with just an actor. OTOH, if a delegate callback requires you to return a value, and that value depend on the state of your actor, then an actor that wraps a Dispatch queue is definitely the best option.

I’m not an expert on AVFoundation, so I’m going to cons up a standalone example. Imagine you have this old school type that uses Dispatch queues and delegates:

class Varnisher {

    init(queue: DispatchSerialQueue) {
        self.queue = queue
    }
    
    let queue: DispatchSerialQueue
    
    weak var delegate: Delegate?
    
    protocol Delegate: AnyObject {
        func varnisherShouldUseGloss(_ varnisher: Varnisher) -> Bool
    }
    
    … the actual implementation …
}

It guarantees that all the delegate methods will be run on queue. Now let’s say you want to wrangle this in an actor. Here’s how to do that:

actor VarnishCentral: Varnisher.Delegate {

    init() {
        let queue = DispatchSerialQueue(label: "VarnishCentral.queue")
        self.queue = queue
        self.varnisher = Varnisher(queue: queue)
        self.useGlossForNextWaffle = true
        
        self.varnisher.delegate = self
    }

    private let queue: DispatchSerialQueue
    private let varnisher: Varnisher
    private var useGlossForNextWaffle: Bool

    nonisolated var unownedExecutor: UnownedSerialExecutor {
        self.queue.asUnownedSerialExecutor()
    }

    nonisolated func varnisherShouldUseGloss(_ varnisher: Varnisher) -> Bool {
        self.assumeIsolated { a in
            a.shouldUseGloss()
        }
    }
    
    private func shouldUseGloss() -> Bool {
        defer { self.useGlossForNextWaffle.toggle() }
        return self.useGlossForNextWaffle
    }
}

Note the following:

  • The initialiser creates a Dispatch serial queue for use by both the actor and varnisher.

  • The unownedExecutor property relies on that queue being a valid serial executor.

  • The varnisherShouldUseGloss(_:) delegate method is marked as nonisolated.

  • It uses assumeIsolated(_:) to tell the compiler that it actually is isolated. This will trap is varnisher calls a delegate method on some other queue.

  • With an actor in hand, the delegate bounces to shouldUseGloss(), which is a standard actor-isolated method.

  • As an actor-isolated method, shouldUseGloss() can access and modify actor state directly.

  • And this chain allows for the function result to go all the way back to the varnisher.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

AVCam Example: Can I use a Actor instead of a DispatchQueue for capture session activity?
 
 
Q