How to observe AVCaptureDevice.DiscoverySession devices property?

At Apple Developer documentation, https://developer.apple.com/documentation/avfoundation/avcapturedevice/discoverysession you can find the sentence

You can also key-value observe this property to monitor changes to the list of available devices.

But how to use it? I tried it with the code above and tested on my MacBook with EarPods.

When I disconnect the EarPods, nothing was happened.

  • MacBook Air M2
  • macOS Sequoia 15.0.1
  • Xcode 16.0
import Foundation
import AVFoundation

let discovery_session = AVCaptureDevice.DiscoverySession.init(deviceTypes: [.microphone], mediaType: .audio, position: .unspecified)
let devices = discovery_session.devices
for device in devices {
    print(device.localizedName)
}
let device = devices[0]

let observer = Observer()
discovery_session.addObserver(observer, forKeyPath: "devices", options: [.new, .old], context: nil)

let input = try! AVCaptureDeviceInput(device: device)
let queue = DispatchQueue(label: "queue")
var output = AVCaptureAudioDataOutput()
let delegate = OutputDelegate()
output.setSampleBufferDelegate(delegate, queue: queue)

var session = AVCaptureSession()

session.beginConfiguration()
session.addInput(input)
session.addOutput(output)
session.commitConfiguration()

session.startRunning()

let group = DispatchGroup()
let q = DispatchQueue(label: "", attributes: .concurrent)

q.async(group: group, execute: DispatchWorkItem() {
    sleep(10)

    session.stopRunning()
})

_ = group.wait(timeout: .distantFuture)

class Observer: NSObject {
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("Change")
        if keyPath == "devices" {
            if let newDevices = change?[.newKey] as? [AVCaptureDevice] {
                print("New devices: \(newDevices.map { $0.localizedName })")
            }
            if let oldDevices = change?[.oldKey] as? [AVCaptureDevice] {
                print("Old devices: \(oldDevices.map { $0.localizedName })")
            }
        }
    }
}

class OutputDelegate : NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        print("Output")
    }
}

Hi @Kusaanko , really interesting question.

Try to use this options [.initial, .new] in your addObserver options method: https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/1413959-initial

From the docs it seems that the .initial option behaves las follow:

When this option is used withaddObserver(_:forKeyPath:options:context:) a notification will be sent for each indexed object to which the observer is being added.

Bye Rob

Hi, I tried it and I got the context below when initializing.

Change
New devices: [ .... ]

When I disconnect AirPods, the log Output won't be shown anymore but I got nothing more. When I disconnect the AirPods, it was not shown on Settings app.

I tried this code

for i in 0..<60 {
    sleep(1)
    for device in discovery_session.devices {
        print(device.localizedName)
    }
}

session.stopRunning()

Then it seems that the devices list is not updated. This code prints the same content every time while the app is runnning.

I don't have EarPods now, so I will try it later.

Hello @Kusaanko, thank you for your post. If you are trying to detect when your AirPods get connected or disconnected, you can also listen for the wasConnectedNotification and the wasDisconnectedNotification.

On iOS there is the routeChangeNotification from AVAudioSession, please see Responding to audio route changes for more information.

On macOS, you can also use Core Audio's kAudioHardwarePropertyDevices. Below is a minimal example in C++:

struct AudioDeviceManager {
    AudioDeviceManager() {
        AudioObjectPropertyAddress address = {kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
        OSStatus error = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &address, &propertyListenerCallback, this);
        // handle error
    }
    
    ~AudioDeviceManager() {
        AudioObjectPropertyAddress address = {kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
        OSStatus error = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &address, &propertyListenerCallback, this);
        // handle error
    }
    
    void propertyChanged(AudioObjectPropertySelector selector) {
        if (selector == kAudioHardwarePropertyDevices) {
            AudioObjectPropertyAddress address = {kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
            UInt32 size;
            OSStatus error = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address, 0, nullptr, &size);
            // handle error
            
            std::vector<AudioDeviceID> devices(size / sizeof(AudioDeviceID));
            error = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, nullptr, &size, devices.data());
            // handle error
        }
    }
    
    static OSStatus
    propertyListenerCallback(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inClientData) {
        if (auto *manager = static_cast<AudioDeviceManager *>(inClientData))
            manager->propertyChanged(inAddresses->mSelector);
        
        return noErr;
    }
};

Hello, @Engineer, thank you for your reply.

I still cannot receive any notification using this code on macOS with AirPods or EarPods.

NotificationCenter.default.addObserver(forName: AVCaptureDevice.wasDisconnectedNotification, object: nil, queue: nil) { notification in
    print("disconnected \(notification)")
}

I found a similar issue on StackOverflow. https://stackoverflow.com/questions/30789622/avcapturedevice-deviceswithmediatype-does-not-update-after-added-removed-camera

I tried the soluon( use AVCaptureDevice.devices() instead of AVCaptureDevice.DiscoverySession.devices() ) in the page, but it doesn't work.

Thanks for your question. On iOS, AVAudioSession controls the routing policy of mics and speakers on the device. The AVCaptureDevice representing audio in AVCaptureDiscoverySession, is a shell representing the current route. It can change depending on what was plugged in last. So when you have no EarPods in, the AVCaptureDevice will be fixed to the built in audio microphone, and when you put EarPods in, it will change to the EarPods. You can detect the change by key value observing the localizedName of the audio capture device.

I forgot to say, the target OS is macOS.

Could anyone write a sample Swift code to detect a microphone disconnect using AVCapture for macOS?

Is there someone who can run my example to detect device changes?

Or this is a bug?

How to observe AVCaptureDevice.DiscoverySession devices property?
 
 
Q