1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "content/browser/device_monitor_mac.h" 6 7 #import <QTKit/QTKit.h> 8 9 #include <set> 10 11 #include "base/bind_helpers.h" 12 #include "base/logging.h" 13 #include "base/mac/bind_objc_block.h" 14 #include "base/mac/scoped_nsobject.h" 15 #include "base/threading/thread_checker.h" 16 #include "content/public/browser/browser_thread.h" 17 #import "media/base/mac/avfoundation_glue.h" 18 19 using content::BrowserThread; 20 21 namespace { 22 23 // This class is used to keep track of system devices names and their types. 24 class DeviceInfo { 25 public: 26 enum DeviceType { 27 kAudio, 28 kVideo, 29 kMuxed, 30 kUnknown, 31 kInvalid 32 }; 33 34 DeviceInfo(std::string unique_id, DeviceType type) 35 : unique_id_(unique_id), type_(type) {} 36 37 // Operator== is needed here to use this class in a std::find. A given 38 // |unique_id_| always has the same |type_| so for comparison purposes the 39 // latter can be safely ignored. 40 bool operator==(const DeviceInfo& device) const { 41 return unique_id_ == device.unique_id_; 42 } 43 44 const std::string& unique_id() const { return unique_id_; } 45 DeviceType type() const { return type_; } 46 47 private: 48 std::string unique_id_; 49 DeviceType type_; 50 // Allow generated copy constructor and assignment. 51 }; 52 53 // Base abstract class used by DeviceMonitorMac to interact with either a QTKit 54 // or an AVFoundation implementation of events and notifications. 55 class DeviceMonitorMacImpl { 56 public: 57 explicit DeviceMonitorMacImpl(content::DeviceMonitorMac* monitor) 58 : monitor_(monitor), 59 cached_devices_(), 60 device_arrival_(nil), 61 device_removal_(nil) { 62 DCHECK(monitor); 63 // Initialise the devices_cache_ with a not-valid entry. For the case in 64 // which there is one single device in the system and we get notified when 65 // it gets removed, this will prevent the system from thinking that no 66 // devices were added nor removed and not notifying the |monitor_|. 67 cached_devices_.push_back(DeviceInfo("invalid", DeviceInfo::kInvalid)); 68 } 69 virtual ~DeviceMonitorMacImpl() {} 70 71 virtual void OnDeviceChanged() = 0; 72 73 // Method called by the default notification center when a device is removed 74 // or added to the system. It will compare the |cached_devices_| with the 75 // current situation, update it, and, if there's an update, signal to 76 // |monitor_| with the appropriate device type. 77 void ConsolidateDevicesListAndNotify( 78 const std::vector<DeviceInfo>& snapshot_devices); 79 80 protected: 81 content::DeviceMonitorMac* monitor_; 82 std::vector<DeviceInfo> cached_devices_; 83 84 // Handles to NSNotificationCenter block observers. 85 id device_arrival_; 86 id device_removal_; 87 88 private: 89 DISALLOW_COPY_AND_ASSIGN(DeviceMonitorMacImpl); 90 }; 91 92 void DeviceMonitorMacImpl::ConsolidateDevicesListAndNotify( 93 const std::vector<DeviceInfo>& snapshot_devices) { 94 bool video_device_added = false; 95 bool audio_device_added = false; 96 bool video_device_removed = false; 97 bool audio_device_removed = false; 98 99 // Compare the current system devices snapshot with the ones cached to detect 100 // additions, present in the former but not in the latter. If we find a device 101 // in snapshot_devices entry also present in cached_devices, we remove it from 102 // the latter vector. 103 std::vector<DeviceInfo>::const_iterator it; 104 for (it = snapshot_devices.begin(); it != snapshot_devices.end(); ++it) { 105 std::vector<DeviceInfo>::iterator cached_devices_iterator = 106 std::find(cached_devices_.begin(), cached_devices_.end(), *it); 107 if (cached_devices_iterator == cached_devices_.end()) { 108 video_device_added |= ((it->type() == DeviceInfo::kVideo) || 109 (it->type() == DeviceInfo::kMuxed)); 110 audio_device_added |= ((it->type() == DeviceInfo::kAudio) || 111 (it->type() == DeviceInfo::kMuxed)); 112 DVLOG(1) << "Device has been added, id: " << it->unique_id(); 113 } else { 114 cached_devices_.erase(cached_devices_iterator); 115 } 116 } 117 // All the remaining entries in cached_devices are removed devices. 118 for (it = cached_devices_.begin(); it != cached_devices_.end(); ++it) { 119 video_device_removed |= ((it->type() == DeviceInfo::kVideo) || 120 (it->type() == DeviceInfo::kMuxed) || 121 (it->type() == DeviceInfo::kInvalid)); 122 audio_device_removed |= ((it->type() == DeviceInfo::kAudio) || 123 (it->type() == DeviceInfo::kMuxed) || 124 (it->type() == DeviceInfo::kInvalid)); 125 DVLOG(1) << "Device has been removed, id: " << it->unique_id(); 126 } 127 // Update the cached devices with the current system snapshot. 128 cached_devices_ = snapshot_devices; 129 130 if (video_device_added || video_device_removed) 131 monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE); 132 if (audio_device_added || audio_device_removed) 133 monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE); 134 } 135 136 class QTKitMonitorImpl : public DeviceMonitorMacImpl { 137 public: 138 explicit QTKitMonitorImpl(content::DeviceMonitorMac* monitor); 139 virtual ~QTKitMonitorImpl(); 140 141 virtual void OnDeviceChanged() OVERRIDE; 142 private: 143 void CountDevices(); 144 void OnAttributeChanged(NSNotification* notification); 145 146 id device_change_; 147 }; 148 149 QTKitMonitorImpl::QTKitMonitorImpl(content::DeviceMonitorMac* monitor) 150 : DeviceMonitorMacImpl(monitor) { 151 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 152 device_arrival_ = 153 [nc addObserverForName:QTCaptureDeviceWasConnectedNotification 154 object:nil 155 queue:nil 156 usingBlock:^(NSNotification* notification) { 157 OnDeviceChanged();}]; 158 device_removal_ = 159 [nc addObserverForName:QTCaptureDeviceWasDisconnectedNotification 160 object:nil 161 queue:nil 162 usingBlock:^(NSNotification* notification) { 163 OnDeviceChanged();}]; 164 device_change_ = 165 [nc addObserverForName:QTCaptureDeviceAttributeDidChangeNotification 166 object:nil 167 queue:nil 168 usingBlock:^(NSNotification* notification) { 169 OnAttributeChanged(notification);}]; 170 } 171 172 QTKitMonitorImpl::~QTKitMonitorImpl() { 173 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 174 [nc removeObserver:device_arrival_]; 175 [nc removeObserver:device_removal_]; 176 [nc removeObserver:device_change_]; 177 } 178 179 void QTKitMonitorImpl::OnAttributeChanged( 180 NSNotification* notification) { 181 if ([[[notification userInfo] 182 objectForKey:QTCaptureDeviceChangedAttributeKey] 183 isEqualToString:QTCaptureDeviceSuspendedAttribute]) { 184 OnDeviceChanged(); 185 } 186 } 187 188 void QTKitMonitorImpl::OnDeviceChanged() { 189 std::vector<DeviceInfo> snapshot_devices; 190 191 NSArray* devices = [QTCaptureDevice inputDevices]; 192 for (QTCaptureDevice* device in devices) { 193 DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown; 194 // Act as if suspended video capture devices are not attached. For 195 // example, a laptop's internal webcam is suspended when the lid is closed. 196 if ([device hasMediaType:QTMediaTypeVideo] && 197 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute] 198 boolValue]) { 199 device_type = DeviceInfo::kVideo; 200 } else if ([device hasMediaType:QTMediaTypeMuxed] && 201 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute] 202 boolValue]) { 203 device_type = DeviceInfo::kMuxed; 204 } else if ([device hasMediaType:QTMediaTypeSound] && 205 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute] 206 boolValue]) { 207 device_type = DeviceInfo::kAudio; 208 } 209 snapshot_devices.push_back( 210 DeviceInfo([[device uniqueID] UTF8String], device_type)); 211 } 212 ConsolidateDevicesListAndNotify(snapshot_devices); 213 } 214 215 // Forward declaration for use by CrAVFoundationDeviceObserver. 216 class SuspendObserverDelegate; 217 218 } // namespace 219 220 // This class is a Key-Value Observer (KVO) shim. It is needed because C++ 221 // classes cannot observe Key-Values directly. Created, manipulated, and 222 // destroyed on the UI Thread by SuspendObserverDelegate. 223 @interface CrAVFoundationDeviceObserver : NSObject { 224 @private 225 // Callback for device changed, has to run on Device Thread. 226 base::Closure onDeviceChangedCallback_; 227 228 // Member to keep track of the devices we are already monitoring. 229 std::set<base::scoped_nsobject<CrAVCaptureDevice> > monitoredDevices_; 230 } 231 232 - (id)initWithOnChangedCallback:(const base::Closure&)callback; 233 - (void)startObserving:(base::scoped_nsobject<CrAVCaptureDevice>)device; 234 - (void)stopObserving:(CrAVCaptureDevice*)device; 235 - (void)clearOnDeviceChangedCallback; 236 237 @end 238 239 namespace { 240 241 // This class owns and manages the lifetime of a CrAVFoundationDeviceObserver. 242 // It is created and destroyed in UI thread by AVFoundationMonitorImpl, and it 243 // operates on this thread except for the expensive device enumerations which 244 // are run on Device Thread. 245 class SuspendObserverDelegate : 246 public base::RefCountedThreadSafe<SuspendObserverDelegate> { 247 public: 248 explicit SuspendObserverDelegate(DeviceMonitorMacImpl* monitor); 249 250 // Create |suspend_observer_| for all devices and register OnDeviceChanged() 251 // as its change callback. Schedule bottom half in DoStartObserver(). 252 void StartObserver( 253 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread); 254 // Enumerate devices in |device_thread| and run the bottom half in 255 // DoOnDeviceChange(). |suspend_observer_| calls back here on suspend event, 256 // and our parent AVFoundationMonitorImpl calls on connect/disconnect device. 257 void OnDeviceChanged( 258 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread); 259 // Remove the device monitor's weak reference. Remove ourselves as suspend 260 // notification observer from |suspend_observer_|. 261 void ResetDeviceMonitor(); 262 263 private: 264 friend class base::RefCountedThreadSafe<SuspendObserverDelegate>; 265 266 virtual ~SuspendObserverDelegate(); 267 268 // Bottom half of StartObserver(), starts |suspend_observer_| for all devices. 269 // Assumes that |devices| has been retained prior to being called, and 270 // releases it internally. 271 void DoStartObserver(NSArray* devices); 272 // Bottom half of OnDeviceChanged(), starts |suspend_observer_| for current 273 // devices and composes a snapshot of them to send it to 274 // |avfoundation_monitor_impl_|. Assumes that |devices| has been retained 275 // prior to being called, and releases it internally. 276 void DoOnDeviceChanged(NSArray* devices); 277 278 base::scoped_nsobject<CrAVFoundationDeviceObserver> suspend_observer_; 279 DeviceMonitorMacImpl* avfoundation_monitor_impl_; 280 }; 281 282 SuspendObserverDelegate::SuspendObserverDelegate(DeviceMonitorMacImpl* monitor) 283 : avfoundation_monitor_impl_(monitor) { 284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 285 } 286 287 void SuspendObserverDelegate::StartObserver( 288 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) { 289 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 290 291 base::Closure on_device_changed_callback = 292 base::Bind(&SuspendObserverDelegate::OnDeviceChanged, 293 this, device_thread); 294 suspend_observer_.reset([[CrAVFoundationDeviceObserver alloc] 295 initWithOnChangedCallback:on_device_changed_callback]); 296 297 // Enumerate the devices in Device thread and post the observers start to be 298 // done on UI thread. The devices array is retained in |device_thread| and 299 // released in DoStartObserver(). 300 base::PostTaskAndReplyWithResult( 301 device_thread.get(), 302 FROM_HERE, 303 base::BindBlock(^{ return [[AVCaptureDeviceGlue devices] retain]; }), 304 base::Bind(&SuspendObserverDelegate::DoStartObserver, this)); 305 } 306 307 void SuspendObserverDelegate::OnDeviceChanged( 308 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) { 309 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 310 // Enumerate the devices in Device thread and post the consolidation of the 311 // new devices and the old ones to be done on UI thread. The devices array 312 // is retained in |device_thread| and released in DoOnDeviceChanged(). 313 PostTaskAndReplyWithResult( 314 device_thread.get(), 315 FROM_HERE, 316 base::BindBlock(^{ return [[AVCaptureDeviceGlue devices] retain]; }), 317 base::Bind(&SuspendObserverDelegate::DoOnDeviceChanged, this)); 318 } 319 320 void SuspendObserverDelegate::ResetDeviceMonitor() { 321 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 322 avfoundation_monitor_impl_ = NULL; 323 [suspend_observer_ clearOnDeviceChangedCallback]; 324 } 325 326 SuspendObserverDelegate::~SuspendObserverDelegate() { 327 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 328 } 329 330 void SuspendObserverDelegate::DoStartObserver(NSArray* devices) { 331 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 332 base::scoped_nsobject<NSArray> auto_release(devices); 333 for (CrAVCaptureDevice* device in devices) { 334 base::scoped_nsobject<CrAVCaptureDevice> device_ptr([device retain]); 335 [suspend_observer_ startObserving:device_ptr]; 336 } 337 } 338 339 void SuspendObserverDelegate::DoOnDeviceChanged(NSArray* devices) { 340 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 341 base::scoped_nsobject<NSArray> auto_release(devices); 342 std::vector<DeviceInfo> snapshot_devices; 343 for (CrAVCaptureDevice* device in devices) { 344 base::scoped_nsobject<CrAVCaptureDevice> device_ptr([device retain]); 345 [suspend_observer_ startObserving:device_ptr]; 346 347 BOOL suspended = [device respondsToSelector:@selector(isSuspended)] && 348 [device isSuspended]; 349 DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown; 350 if ([device hasMediaType:AVFoundationGlue::AVMediaTypeVideo()]) { 351 if (suspended) 352 continue; 353 device_type = DeviceInfo::kVideo; 354 } else if ([device hasMediaType:AVFoundationGlue::AVMediaTypeMuxed()]) { 355 device_type = suspended ? DeviceInfo::kAudio : DeviceInfo::kMuxed; 356 } else if ([device hasMediaType:AVFoundationGlue::AVMediaTypeAudio()]) { 357 device_type = DeviceInfo::kAudio; 358 } 359 snapshot_devices.push_back(DeviceInfo([[device uniqueID] UTF8String], 360 device_type)); 361 } 362 363 // |avfoundation_monitor_impl_| might have been NULLed asynchronously before 364 // arriving at this line. 365 if (avfoundation_monitor_impl_) { 366 avfoundation_monitor_impl_->ConsolidateDevicesListAndNotify( 367 snapshot_devices); 368 } 369 } 370 371 // AVFoundation implementation of the Mac Device Monitor, registers as a global 372 // device connect/disconnect observer and plugs suspend/wake up device observers 373 // per device. This class is created and lives in UI thread. Owns a 374 // SuspendObserverDelegate that notifies when a device is suspended/resumed. 375 class AVFoundationMonitorImpl : public DeviceMonitorMacImpl { 376 public: 377 AVFoundationMonitorImpl( 378 content::DeviceMonitorMac* monitor, 379 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner); 380 virtual ~AVFoundationMonitorImpl(); 381 382 virtual void OnDeviceChanged() OVERRIDE; 383 384 private: 385 // {Video,AudioInput}DeviceManager's "Device" thread task runner used for 386 // posting tasks to |suspend_observer_delegate_|; valid after 387 // MediaStreamManager calls StartMonitoring(). 388 const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_; 389 390 scoped_refptr<SuspendObserverDelegate> suspend_observer_delegate_; 391 392 DISALLOW_COPY_AND_ASSIGN(AVFoundationMonitorImpl); 393 }; 394 395 AVFoundationMonitorImpl::AVFoundationMonitorImpl( 396 content::DeviceMonitorMac* monitor, 397 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner) 398 : DeviceMonitorMacImpl(monitor), 399 device_task_runner_(device_task_runner), 400 suspend_observer_delegate_(new SuspendObserverDelegate(this)) { 401 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 402 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 403 device_arrival_ = 404 [nc addObserverForName:AVFoundationGlue:: 405 AVCaptureDeviceWasConnectedNotification() 406 object:nil 407 queue:nil 408 usingBlock:^(NSNotification* notification) { 409 OnDeviceChanged();}]; 410 device_removal_ = 411 [nc addObserverForName:AVFoundationGlue:: 412 AVCaptureDeviceWasDisconnectedNotification() 413 object:nil 414 queue:nil 415 usingBlock:^(NSNotification* notification) { 416 OnDeviceChanged();}]; 417 suspend_observer_delegate_->StartObserver(device_task_runner_); 418 } 419 420 AVFoundationMonitorImpl::~AVFoundationMonitorImpl() { 421 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 422 suspend_observer_delegate_->ResetDeviceMonitor(); 423 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 424 [nc removeObserver:device_arrival_]; 425 [nc removeObserver:device_removal_]; 426 } 427 428 void AVFoundationMonitorImpl::OnDeviceChanged() { 429 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 430 suspend_observer_delegate_->OnDeviceChanged(device_task_runner_); 431 } 432 433 } // namespace 434 435 @implementation CrAVFoundationDeviceObserver 436 437 - (id)initWithOnChangedCallback:(const base::Closure&)callback { 438 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 439 if ((self = [super init])) { 440 DCHECK(!callback.is_null()); 441 onDeviceChangedCallback_ = callback; 442 } 443 return self; 444 } 445 446 - (void)dealloc { 447 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 448 std::set<base::scoped_nsobject<CrAVCaptureDevice> >::iterator it = 449 monitoredDevices_.begin(); 450 while (it != monitoredDevices_.end()) 451 [self removeObservers:*(it++)]; 452 [super dealloc]; 453 } 454 455 - (void)startObserving:(base::scoped_nsobject<CrAVCaptureDevice>)device { 456 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 457 DCHECK(device != nil); 458 // Skip this device if there are already observers connected to it. 459 if (std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device) != 460 monitoredDevices_.end()) { 461 return; 462 } 463 [device addObserver:self 464 forKeyPath:@"suspended" 465 options:0 466 context:device.get()]; 467 [device addObserver:self 468 forKeyPath:@"connected" 469 options:0 470 context:device.get()]; 471 monitoredDevices_.insert(device); 472 } 473 474 - (void)stopObserving:(CrAVCaptureDevice*)device { 475 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 476 DCHECK(device != nil); 477 478 std::set<base::scoped_nsobject<CrAVCaptureDevice> >::iterator found = 479 std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device); 480 DCHECK(found != monitoredDevices_.end()); 481 [self removeObservers:*found]; 482 monitoredDevices_.erase(found); 483 } 484 485 - (void)clearOnDeviceChangedCallback { 486 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 487 onDeviceChangedCallback_.Reset(); 488 } 489 490 - (void)removeObservers:(CrAVCaptureDevice*)device { 491 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 492 // Check sanity of |device| via its -observationInfo. http://crbug.com/371271. 493 if ([device observationInfo]) { 494 [device removeObserver:self 495 forKeyPath:@"suspended"]; 496 [device removeObserver:self 497 forKeyPath:@"connected"]; 498 } 499 } 500 501 - (void)observeValueForKeyPath:(NSString*)keyPath 502 ofObject:(id)object 503 change:(NSDictionary*)change 504 context:(void*)context { 505 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 506 if ([keyPath isEqual:@"suspended"]) 507 onDeviceChangedCallback_.Run(); 508 if ([keyPath isEqual:@"connected"]) 509 [self stopObserving:static_cast<CrAVCaptureDevice*>(context)]; 510 } 511 512 @end // @implementation CrAVFoundationDeviceObserver 513 514 namespace content { 515 516 DeviceMonitorMac::DeviceMonitorMac() { 517 // Both QTKit and AVFoundation do not need to be fired up until the user 518 // exercises a GetUserMedia. Bringing up either library and enumerating the 519 // devices in the system is an operation taking in the range of hundred of ms, 520 // so it is triggered explicitly from MediaStreamManager::StartMonitoring(). 521 } 522 523 DeviceMonitorMac::~DeviceMonitorMac() {} 524 525 void DeviceMonitorMac::StartMonitoring( 526 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner) { 527 DCHECK(thread_checker_.CalledOnValidThread()); 528 if (AVFoundationGlue::IsAVFoundationSupported()) { 529 DVLOG(1) << "Monitoring via AVFoundation"; 530 device_monitor_impl_.reset(new AVFoundationMonitorImpl(this, 531 device_task_runner)); 532 } else { 533 DVLOG(1) << "Monitoring via QTKit"; 534 device_monitor_impl_.reset(new QTKitMonitorImpl(this)); 535 } 536 } 537 538 void DeviceMonitorMac::NotifyDeviceChanged( 539 base::SystemMonitor::DeviceType type) { 540 DCHECK(thread_checker_.CalledOnValidThread()); 541 // TODO(xians): Remove the global variable for SystemMonitor. 542 base::SystemMonitor::Get()->ProcessDevicesChanged(type); 543 } 544 545 } // namespace content 546