blob: b9ee118490303bfea22f744f6dccdb79f56cc720 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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 <set>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/task_runner_util.h"
#include "base/threading/thread_checker.h"
namespace {
// This class is used to keep track of system devices names and their types.
class DeviceInfo {
public:
enum DeviceType { kAudio, kVideo, kMuxed, kUnknown, kInvalid };
DeviceInfo(const std::string& unique_id, DeviceType type)
: unique_id_(unique_id), type_(type) {}
// Operator== is needed here to use this class in a std::find. A given
// |unique_id_| always has the same |type_| so for comparison purposes the
// latter can be safely ignored.
bool operator==(const DeviceInfo& device) const {
return unique_id_ == device.unique_id_;
}
const std::string& unique_id() const { return unique_id_; }
DeviceType type() const { return type_; }
private:
std::string unique_id_;
DeviceType type_;
// Allow generated copy constructor and assignment.
};
// Base abstract class used by DeviceMonitorMac.
class DeviceMonitorMacImpl {
public:
explicit DeviceMonitorMacImpl(media::DeviceMonitorMac* monitor)
: monitor_(monitor),
cached_devices_(),
device_arrival_(nil),
device_removal_(nil) {
DCHECK(monitor);
// Initialise the devices_cache_ with a not-valid entry. For the case in
// which there is one single device in the system and we get notified when
// it gets removed, this will prevent the system from thinking that no
// devices were added nor removed and not notifying the |monitor_|.
cached_devices_.push_back(DeviceInfo("invalid", DeviceInfo::kInvalid));
}
virtual ~DeviceMonitorMacImpl() {}
virtual void OnDeviceChanged() = 0;
// Method called by the default notification center when a device is removed
// or added to the system. It will compare the |cached_devices_| with the
// current situation, update it, and, if there's an update, signal to
// |monitor_| with the appropriate device type.
void ConsolidateDevicesListAndNotify(
const std::vector<DeviceInfo>& snapshot_devices);
protected:
media::DeviceMonitorMac* monitor_;
std::vector<DeviceInfo> cached_devices_;
// Handles to NSNotificationCenter block observers.
id device_arrival_;
id device_removal_;
private:
DISALLOW_COPY_AND_ASSIGN(DeviceMonitorMacImpl);
};
void DeviceMonitorMacImpl::ConsolidateDevicesListAndNotify(
const std::vector<DeviceInfo>& snapshot_devices) {
bool video_device_added = false;
bool video_device_removed = false;
// Compare the current system devices snapshot with the ones cached to detect
// additions, present in the former but not in the latter. If we find a device
// in snapshot_devices entry also present in cached_devices, we remove it from
// the latter vector.
std::vector<DeviceInfo>::const_iterator it;
for (it = snapshot_devices.begin(); it != snapshot_devices.end(); ++it) {
std::vector<DeviceInfo>::iterator cached_devices_iterator =
std::find(cached_devices_.begin(), cached_devices_.end(), *it);
if (cached_devices_iterator == cached_devices_.end()) {
video_device_added |= ((it->type() == DeviceInfo::kVideo) ||
(it->type() == DeviceInfo::kMuxed));
DVLOG(1) << "Video device has been added, id: " << it->unique_id();
} else {
cached_devices_.erase(cached_devices_iterator);
}
}
// All the remaining entries in cached_devices are removed devices.
for (it = cached_devices_.begin(); it != cached_devices_.end(); ++it) {
video_device_removed |= ((it->type() == DeviceInfo::kVideo) ||
(it->type() == DeviceInfo::kMuxed) ||
(it->type() == DeviceInfo::kInvalid));
DVLOG(1) << "Video device has been removed, id: " << it->unique_id();
}
// Update the cached devices with the current system snapshot.
cached_devices_ = snapshot_devices;
if (video_device_added || video_device_removed)
monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE);
}
// 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::Closure _onDeviceChangedCallback;
// Member to keep track of the devices we are already monitoring.
std::set<base::scoped_nsobject<AVCaptureDevice>> _monitoredDevices;
// Pegged to the "main" thread -- usually content::BrowserThread::UI.
base::ThreadChecker _mainThreadChecker;
}
- (id)initWithOnChangedCallback:(const base::Closure&)callback;
- (void)startObserving:(base::scoped_nsobject<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);
// Enumerate devices in |device_thread| and run the bottom half in
// DoOnDeviceChange(). |suspend_observer_| calls back here on suspend event,
// and our parent AVFoundationMonitorImpl calls on connect/disconnect device.
void OnDeviceChanged(
const scoped_refptr<base::SingleThreadTaskRunner>& device_thread);
// 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* devices);
// Bottom half of OnDeviceChanged(), starts |suspend_observer_| for current
// devices and composes a snapshot of them to send it to
// |avfoundation_monitor_impl_|. Assumes that |devices| has been retained
// prior to being called, and releases it internally.
void DoOnDeviceChanged(NSArray* devices);
base::scoped_nsobject<CrAVFoundationDeviceObserver> suspend_observer_;
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::Closure on_device_changed_callback = base::Bind(
&SuspendObserverDelegate::OnDeviceChanged, this, device_thread);
suspend_observer_.reset([[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 devices array is retained in |device_thread| and
// released in DoStartObserver().
base::PostTaskAndReplyWithResult(
device_thread.get(), FROM_HERE, base::BindOnce(base::RetainBlock(^{
return [[AVCaptureDevice devices] retain];
})),
base::BindOnce(&SuspendObserverDelegate::DoStartObserver, this));
}
void SuspendObserverDelegate::OnDeviceChanged(
const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) {
DCHECK(main_thread_checker_.CalledOnValidThread());
// Enumerate the devices in Device thread and post the consolidation of the
// new devices and the old ones to be done on main thread. The devices array
// is retained in |device_thread| and released in DoOnDeviceChanged().
PostTaskAndReplyWithResult(
device_thread.get(), FROM_HERE, base::BindOnce(base::RetainBlock(^{
return [[AVCaptureDevice devices] retain];
})),
base::BindOnce(&SuspendObserverDelegate::DoOnDeviceChanged, this));
}
void SuspendObserverDelegate::ResetDeviceMonitor() {
DCHECK(main_thread_checker_.CalledOnValidThread());
avfoundation_monitor_impl_ = NULL;
[suspend_observer_ clearOnDeviceChangedCallback];
}
SuspendObserverDelegate::~SuspendObserverDelegate() {
DCHECK(main_thread_checker_.CalledOnValidThread());
}
void SuspendObserverDelegate::DoStartObserver(NSArray* devices) {
DCHECK(main_thread_checker_.CalledOnValidThread());
base::scoped_nsobject<NSArray> auto_release(devices);
for (AVCaptureDevice* device in devices) {
base::scoped_nsobject<AVCaptureDevice> device_ptr([device retain]);
[suspend_observer_ startObserving:device_ptr];
}
}
void SuspendObserverDelegate::DoOnDeviceChanged(NSArray* devices) {
DCHECK(main_thread_checker_.CalledOnValidThread());
base::scoped_nsobject<NSArray> auto_release(devices);
std::vector<DeviceInfo> snapshot_devices;
for (AVCaptureDevice* device in devices) {
base::scoped_nsobject<AVCaptureDevice> device_ptr([device retain]);
[suspend_observer_ startObserving:device_ptr];
BOOL suspended = [device respondsToSelector:@selector(isSuspended)] &&
[device isSuspended];
DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown;
if ([device hasMediaType:AVMediaTypeVideo]) {
if (suspended)
continue;
device_type = DeviceInfo::kVideo;
} else if ([device hasMediaType:AVMediaTypeMuxed]) {
device_type = suspended ? DeviceInfo::kAudio : DeviceInfo::kMuxed;
} else if ([device hasMediaType:AVMediaTypeAudio]) {
device_type = DeviceInfo::kAudio;
}
snapshot_devices.push_back(
DeviceInfo([[device uniqueID] UTF8String], device_type));
}
// Make sure no references are held to |devices| when
// ConsolidateDevicesListAndNotify is called since the VideoCaptureManager
// and AudioCaptureManagers also enumerates the available devices but on
// another thread.
auto_release.reset();
// |avfoundation_monitor_impl_| might have been NULLed asynchronously before
// arriving at this line.
if (avfoundation_monitor_impl_) {
avfoundation_monitor_impl_->ConsolidateDevicesListAndNotify(
snapshot_devices);
}
}
// 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() override;
void OnDeviceChanged() override;
private:
// {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_;
DISALLOW_COPY_AND_ASSIGN(AVFoundationMonitorImpl);
};
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) {
OnDeviceChanged();
}];
device_removal_ =
[nc addObserverForName:AVCaptureDeviceWasDisconnectedNotification
object:nil
queue:nil
usingBlock:^(NSNotification* notification) {
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(device_task_runner_);
}
} // namespace
@implementation CrAVFoundationDeviceObserver
- (id)initWithOnChangedCallback:(const base::Closure&)callback {
DCHECK(_mainThreadChecker.CalledOnValidThread());
if ((self = [super init])) {
DCHECK(!callback.is_null());
_onDeviceChangedCallback = callback;
}
return self;
}
- (void)dealloc {
DCHECK(_mainThreadChecker.CalledOnValidThread());
std::set<base::scoped_nsobject<AVCaptureDevice>>::iterator it =
_monitoredDevices.begin();
while (it != _monitoredDevices.end())
[self removeObservers:*(it++)];
[super dealloc];
}
- (void)startObserving:(base::scoped_nsobject<AVCaptureDevice>)device {
DCHECK(_mainThreadChecker.CalledOnValidThread());
DCHECK(device != nil);
// Skip this device if there are already observers connected to it.
if (std::find(_monitoredDevices.begin(), _monitoredDevices.end(), device) !=
_monitoredDevices.end()) {
return;
}
[device addObserver:self
forKeyPath:@"suspended"
options:0
context:device.get()];
[device addObserver:self
forKeyPath:@"connected"
options:0
context:device.get()];
_monitoredDevices.insert(device);
}
- (void)stopObserving:(AVCaptureDevice*)device {
DCHECK(_mainThreadChecker.CalledOnValidThread());
DCHECK(device != nil);
std::set<base::scoped_nsobject<AVCaptureDevice>>::iterator found =
std::find(_monitoredDevices.begin(), _monitoredDevices.end(), 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:static_cast<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() {}
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(
base::SystemMonitor::DeviceType type) {
DCHECK(thread_checker_.CalledOnValidThread());
// TODO(xians): Remove the global variable for SystemMonitor.
base::SystemMonitor::Get()->ProcessDevicesChanged(type);
}
} // namespace media