// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/device_monitors/device_monitor_mac.h"
#include <AVFoundation/AVFoundation.h>
#include <CoreFoundation/CoreFoundation.h>
#include <set>
#include <string>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/sys_string_conversions.h"
#import "base/task/single_thread_task_runner.h"
#include "base/threading/thread_checker.h"
#include "media/base/mac/video_capture_device_avfoundation_helpers.h"
namespace {
// Base abstract class used by DeviceMonitorMac.
class DeviceMonitorMacImpl {
public:
explicit DeviceMonitorMacImpl(media::DeviceMonitorMac* monitor)
: monitor_(monitor) {
DCHECK(monitor);
}
DeviceMonitorMacImpl(const DeviceMonitorMacImpl&) = delete;
DeviceMonitorMacImpl& operator=(const DeviceMonitorMacImpl&) = delete;
virtual ~DeviceMonitorMacImpl() = default;
virtual void OnDeviceChanged() = 0;
media::DeviceMonitorMac* monitor() const { return monitor_; }
protected:
// Handles to NSNotificationCenter block observers.
id __strong device_arrival_ = nil;
id __strong device_removal_ = nil;
private:
raw_ptr<media::DeviceMonitorMac> monitor_;
};
// Forward declaration for use by CrAVFoundationDeviceObserver.
class SuspendObserverDelegate;
} // namespace
// This class is a Key-Value Observer (KVO) shim. It is needed because C++
// classes cannot observe Key-Values directly. Created, manipulated, and
// destroyed on the UI Thread by SuspendObserverDelegate.
@interface CrAVFoundationDeviceObserver : NSObject {
@private
// Callback for device changed, has to run on Device Thread.
base::RepeatingClosure _onDeviceChangedCallback;
// Member to keep track of the devices we are already monitoring.
std::set<AVCaptureDevice * __strong> _monitoredDevices;
// Pegged to the "main" thread -- usually content::BrowserThread::UI.
base::ThreadChecker _mainThreadChecker;
}
- (instancetype)initWithOnChangedCallback:
(const base::RepeatingClosure&)callback;
- (void)startObserving:(AVCaptureDevice*)device;
- (void)stopObserving:(AVCaptureDevice*)device;
- (void)clearOnDeviceChangedCallback;
@end
namespace {
// This class owns and manages the lifetime of a CrAVFoundationDeviceObserver.
// It is created and destroyed on AVFoundationMonitorImpl's main thread (usually
// browser's UI thread), and it operates on this thread except for the expensive
// device enumerations which are run on Device Thread.
class SuspendObserverDelegate
: public base::RefCountedThreadSafe<SuspendObserverDelegate> {
public:
explicit SuspendObserverDelegate(DeviceMonitorMacImpl* monitor);
// Create |suspend_observer_| for all devices and register OnDeviceChanged()
// as its change callback. Schedule bottom half in DoStartObserver().
void StartObserver(
const scoped_refptr<base::SingleThreadTaskRunner>& device_thread);
// Send a notification of devices being changed.
void OnDeviceChanged();
// Remove the device monitor's weak reference. Remove ourselves as suspend
// notification observer from |suspend_observer_|.
void ResetDeviceMonitor();
private:
friend class base::RefCountedThreadSafe<SuspendObserverDelegate>;
virtual ~SuspendObserverDelegate();
// Bottom half of StartObserver(), starts |suspend_observer_| for all devices.
// Assumes that |devices| has been retained prior to being called, and
// releases it internally.
void DoStartObserver(NSArray<AVCaptureDevice*>* devices);
CrAVFoundationDeviceObserver* __strong suspend_observer_;
raw_ptr<DeviceMonitorMacImpl> avfoundation_monitor_impl_;
// Pegged to the "main" thread -- usually content::BrowserThread::UI.
base::ThreadChecker main_thread_checker_;
};
SuspendObserverDelegate::SuspendObserverDelegate(DeviceMonitorMacImpl* monitor)
: avfoundation_monitor_impl_(monitor) {
DCHECK(main_thread_checker_.CalledOnValidThread());
}
void SuspendObserverDelegate::StartObserver(
const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) {
DCHECK(main_thread_checker_.CalledOnValidThread());
base::RepeatingClosure on_device_changed_callback =
base::BindRepeating(&SuspendObserverDelegate::OnDeviceChanged, this);
suspend_observer_ = [[CrAVFoundationDeviceObserver alloc]
initWithOnChangedCallback:on_device_changed_callback];
// Enumerate the devices in Device thread and post the observers start to be
// done on UI thread. The block is bound as a type returning a strong
// reference which keeps the devices array alive.
device_thread->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(^{
return media::GetVideoCaptureDevices();
}),
base::BindOnce(&SuspendObserverDelegate::DoStartObserver, this));
}
void SuspendObserverDelegate::OnDeviceChanged() {
DCHECK(main_thread_checker_.CalledOnValidThread());
// |avfoundation_monitor_impl_| might have been NULLed asynchronously before
// arriving at this line.
if (avfoundation_monitor_impl_) {
// Forward the event to MediaDevicesManager::OnDevicesChanged, which will
// enumerate the devices on its own.
avfoundation_monitor_impl_->monitor()->NotifyDeviceChanged();
}
}
void SuspendObserverDelegate::ResetDeviceMonitor() {
DCHECK(main_thread_checker_.CalledOnValidThread());
avfoundation_monitor_impl_ = nullptr;
[suspend_observer_ clearOnDeviceChangedCallback];
}
SuspendObserverDelegate::~SuspendObserverDelegate() {
DCHECK(main_thread_checker_.CalledOnValidThread());
}
void SuspendObserverDelegate::DoStartObserver(
NSArray<AVCaptureDevice*>* devices) {
DCHECK(main_thread_checker_.CalledOnValidThread());
for (AVCaptureDevice* device in devices) {
[suspend_observer_ startObserving:device];
}
}
// AVFoundation implementation of the Mac Device Monitor, registers as a global
// device connect/disconnect observer and plugs suspend/wake up device observers
// per device. This class is created and lives on the main Application thread
// (UI for content). Owns a SuspendObserverDelegate that notifies when a device
// is suspended/resumed.
class AVFoundationMonitorImpl : public DeviceMonitorMacImpl {
public:
AVFoundationMonitorImpl(
media::DeviceMonitorMac* monitor,
const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner);
AVFoundationMonitorImpl(const AVFoundationMonitorImpl&) = delete;
AVFoundationMonitorImpl& operator=(const AVFoundationMonitorImpl&) = delete;
~AVFoundationMonitorImpl() override;
void OnDeviceChanged() override;
private:
bool IsAudioDevice(AVCaptureDevice* device);
// {Video,AudioInput}DeviceManager's "Device" thread task runner used for
// posting tasks to |suspend_observer_delegate_|;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
// Pegged to the "main" thread -- usually content::BrowserThread::UI.
base::ThreadChecker main_thread_checker_;
scoped_refptr<SuspendObserverDelegate> suspend_observer_delegate_;
};
AVFoundationMonitorImpl::AVFoundationMonitorImpl(
media::DeviceMonitorMac* monitor,
const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner)
: DeviceMonitorMacImpl(monitor),
device_task_runner_(device_task_runner),
suspend_observer_delegate_(new SuspendObserverDelegate(this)) {
DCHECK(main_thread_checker_.CalledOnValidThread());
NSNotificationCenter* nc = NSNotificationCenter.defaultCenter;
device_arrival_ =
[nc addObserverForName:AVCaptureDeviceWasConnectedNotification
object:nil
queue:nil
usingBlock:^(NSNotification* notification) {
if (!IsAudioDevice(notification.object)) {
OnDeviceChanged();
}
}];
device_removal_ =
[nc addObserverForName:AVCaptureDeviceWasDisconnectedNotification
object:nil
queue:nil
usingBlock:^(NSNotification* notification) {
if (!IsAudioDevice(notification.object)) {
OnDeviceChanged();
}
}];
suspend_observer_delegate_->StartObserver(device_task_runner_);
}
AVFoundationMonitorImpl::~AVFoundationMonitorImpl() {
DCHECK(main_thread_checker_.CalledOnValidThread());
suspend_observer_delegate_->ResetDeviceMonitor();
NSNotificationCenter* nc = NSNotificationCenter.defaultCenter;
[nc removeObserver:device_arrival_];
[nc removeObserver:device_removal_];
}
void AVFoundationMonitorImpl::OnDeviceChanged() {
DCHECK(main_thread_checker_.CalledOnValidThread());
suspend_observer_delegate_->OnDeviceChanged();
}
bool AVFoundationMonitorImpl::IsAudioDevice(AVCaptureDevice* device) {
DCHECK(main_thread_checker_.CalledOnValidThread());
return [device hasMediaType:AVMediaTypeAudio];
}
} // namespace
@implementation CrAVFoundationDeviceObserver
- (instancetype)initWithOnChangedCallback:
(const base::RepeatingClosure&)callback {
DCHECK(_mainThreadChecker.CalledOnValidThread());
if ((self = [super init])) {
DCHECK(!callback.is_null());
_onDeviceChangedCallback = callback;
}
return self;
}
- (void)dealloc {
DCHECK(_mainThreadChecker.CalledOnValidThread());
auto it = _monitoredDevices.begin();
while (it != _monitoredDevices.end()) {
[self removeObservers:*(it++)];
}
}
- (void)startObserving:(AVCaptureDevice*)device {
DCHECK(_mainThreadChecker.CalledOnValidThread());
DCHECK(device != nil);
// Skip this device if there are already observers connected to it.
if (base::Contains(_monitoredDevices, device)) {
return;
}
// Pass a raw pointer to the device as the context. This is safe because the
// device is retained in _monitoredDevices for the duration of the
// observation.
[device addObserver:self
forKeyPath:@"suspended"
options:0
context:(__bridge void*)device];
[device addObserver:self
forKeyPath:@"connected"
options:0
context:(__bridge void*)device];
_monitoredDevices.insert(device);
}
- (void)stopObserving:(AVCaptureDevice*)device {
DCHECK(_mainThreadChecker.CalledOnValidThread());
DCHECK(device != nil);
auto found = base::ranges::find(_monitoredDevices, device);
DCHECK(found != _monitoredDevices.end());
[self removeObservers:*found];
_monitoredDevices.erase(found);
}
- (void)clearOnDeviceChangedCallback {
DCHECK(_mainThreadChecker.CalledOnValidThread());
_onDeviceChangedCallback.Reset();
}
- (void)removeObservers:(AVCaptureDevice*)device {
DCHECK(_mainThreadChecker.CalledOnValidThread());
// Check sanity of |device| via its -observationInfo. http://crbug.com/371271.
if (device.observationInfo) {
[device removeObserver:self forKeyPath:@"suspended"];
[device removeObserver:self forKeyPath:@"connected"];
}
}
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
DCHECK(_mainThreadChecker.CalledOnValidThread());
if ([keyPath isEqual:@"suspended"]) {
_onDeviceChangedCallback.Run();
}
if ([keyPath isEqual:@"connected"]) {
[self stopObserving:(__bridge AVCaptureDevice*)context];
}
}
@end // @implementation CrAVFoundationDeviceObserver
namespace media {
DeviceMonitorMac::DeviceMonitorMac(
scoped_refptr<base::SingleThreadTaskRunner> device_task_runner)
: device_task_runner_(std::move(device_task_runner)) {
// AVFoundation do not need to be fired up until the user exercises a
// GetUserMedia. Bringing up either library and enumerating the devices in the
// system is an operation taking in the range of hundred of ms, so it is
// triggered explicitly from MediaStreamManager::StartMonitoring().
}
DeviceMonitorMac::~DeviceMonitorMac() = default;
void DeviceMonitorMac::StartMonitoring() {
DCHECK(thread_checker_.CalledOnValidThread());
DVLOG(1) << "Monitoring via AVFoundation";
device_monitor_impl_ =
std::make_unique<AVFoundationMonitorImpl>(this, device_task_runner_);
}
void DeviceMonitorMac::NotifyDeviceChanged() {
DCHECK(thread_checker_.CalledOnValidThread());
base::SystemMonitor::Get()->ProcessDevicesChanged(
base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE);
}
} // namespace media