1// Copyright 2013 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#import "media/video/capture/mac/video_capture_device_avfoundation_mac.h" 6 7#import <CoreVideo/CoreVideo.h> 8 9#include "base/logging.h" 10#include "base/mac/foundation_util.h" 11#include "media/video/capture/mac/video_capture_device_mac.h" 12#include "ui/gfx/size.h" 13 14@implementation VideoCaptureDeviceAVFoundation 15 16#pragma mark Class methods 17 18+ (void)getDeviceNames:(NSMutableDictionary*)deviceNames { 19 // At this stage we already know that AVFoundation is supported and the whole 20 // library is loaded and initialised, by the device monitoring. 21 NSArray* devices = [AVCaptureDeviceGlue devices]; 22 for (CrAVCaptureDevice* device in devices) { 23 if (([device hasMediaType:AVFoundationGlue::AVMediaTypeVideo()] || 24 [device hasMediaType:AVFoundationGlue::AVMediaTypeMuxed()]) && 25 ![device isSuspended]) { 26 DeviceNameAndTransportType* nameAndTransportType = 27 [[[DeviceNameAndTransportType alloc] 28 initWithName:[device localizedName] 29 transportType:[device transportType]] autorelease]; 30 [deviceNames setObject:nameAndTransportType 31 forKey:[device uniqueID]]; 32 } 33 } 34} 35 36+ (NSDictionary*)deviceNames { 37 NSMutableDictionary* deviceNames = 38 [[[NSMutableDictionary alloc] init] autorelease]; 39 // The device name retrieval is not going to happen in the main thread, and 40 // this might cause instabilities (it did in QTKit), so keep an eye here. 41 [self getDeviceNames:deviceNames]; 42 return deviceNames; 43} 44 45+ (void)getDevice:(const media::VideoCaptureDevice::Name&)name 46 supportedFormats:(media::VideoCaptureFormats*)formats{ 47 NSArray* devices = [AVCaptureDeviceGlue devices]; 48 CrAVCaptureDevice* device = nil; 49 for (device in devices) { 50 if ([[device uniqueID] UTF8String] == name.id()) 51 break; 52 } 53 if (device == nil) 54 return; 55 for (CrAVCaptureDeviceFormat* format in device.formats) { 56 // MediaSubType is a CMPixelFormatType but can be used as CVPixelFormatType 57 // as well according to CMFormatDescription.h 58 media::VideoPixelFormat pixelFormat = media::PIXEL_FORMAT_UNKNOWN; 59 switch (CoreMediaGlue::CMFormatDescriptionGetMediaSubType( 60 [format formatDescription])) { 61 case kCVPixelFormatType_422YpCbCr8: // Typical. 62 pixelFormat = media::PIXEL_FORMAT_UYVY; 63 break; 64 case CoreMediaGlue::kCMPixelFormat_422YpCbCr8_yuvs: 65 pixelFormat = media::PIXEL_FORMAT_YUY2; 66 break; 67 case CoreMediaGlue::kCMVideoCodecType_JPEG_OpenDML: 68 pixelFormat = media::PIXEL_FORMAT_MJPEG; 69 default: 70 break; 71 } 72 73 CoreMediaGlue::CMVideoDimensions dimensions = 74 CoreMediaGlue::CMVideoFormatDescriptionGetDimensions( 75 [format formatDescription]); 76 77 for (CrAVFrameRateRange* frameRate in 78 [format videoSupportedFrameRateRanges]) { 79 media::VideoCaptureFormat format( 80 gfx::Size(dimensions.width, dimensions.height), 81 frameRate.maxFrameRate, 82 pixelFormat); 83 formats->push_back(format); 84 DVLOG(2) << name.name() << " " << format.ToString(); 85 } 86 } 87 88} 89 90#pragma mark Public methods 91 92- (id)initWithFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver { 93 if ((self = [super init])) { 94 DCHECK(main_thread_checker_.CalledOnValidThread()); 95 DCHECK(frameReceiver); 96 [self setFrameReceiver:frameReceiver]; 97 captureSession_.reset( 98 [[AVFoundationGlue::AVCaptureSessionClass() alloc] init]); 99 } 100 return self; 101} 102 103- (void)dealloc { 104 [self stopCapture]; 105 [super dealloc]; 106} 107 108- (void)setFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver { 109 base::AutoLock lock(lock_); 110 frameReceiver_ = frameReceiver; 111} 112 113- (BOOL)setCaptureDevice:(NSString*)deviceId { 114 DCHECK(captureSession_); 115 DCHECK(main_thread_checker_.CalledOnValidThread()); 116 117 if (!deviceId) { 118 // First stop the capture session, if it's running. 119 [self stopCapture]; 120 // Now remove the input and output from the capture session. 121 [captureSession_ removeOutput:captureVideoDataOutput_]; 122 if (captureDeviceInput_) { 123 [captureSession_ removeInput:captureDeviceInput_]; 124 // No need to release |captureDeviceInput_|, is owned by the session. 125 captureDeviceInput_ = nil; 126 } 127 return YES; 128 } 129 130 // Look for input device with requested name. 131 captureDevice_ = [AVCaptureDeviceGlue deviceWithUniqueID:deviceId]; 132 if (!captureDevice_) { 133 [self sendErrorString:[NSString 134 stringWithUTF8String:"Could not open video capture device."]]; 135 return NO; 136 } 137 138 // Create the capture input associated with the device. Easy peasy. 139 NSError* error = nil; 140 captureDeviceInput_ = [AVCaptureDeviceInputGlue 141 deviceInputWithDevice:captureDevice_ 142 error:&error]; 143 if (!captureDeviceInput_) { 144 captureDevice_ = nil; 145 [self sendErrorString:[NSString 146 stringWithFormat:@"Could not create video capture input (%@): %@", 147 [error localizedDescription], 148 [error localizedFailureReason]]]; 149 return NO; 150 } 151 [captureSession_ addInput:captureDeviceInput_]; 152 153 // Create a new data output for video. The data output is configured to 154 // discard late frames by default. 155 captureVideoDataOutput_.reset( 156 [[AVFoundationGlue::AVCaptureVideoDataOutputClass() alloc] init]); 157 if (!captureVideoDataOutput_) { 158 [captureSession_ removeInput:captureDeviceInput_]; 159 [self sendErrorString:[NSString 160 stringWithUTF8String:"Could not create video data output."]]; 161 return NO; 162 } 163 [captureVideoDataOutput_ 164 setSampleBufferDelegate:self 165 queue:dispatch_get_global_queue( 166 DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; 167 [captureSession_ addOutput:captureVideoDataOutput_]; 168 return YES; 169} 170 171- (BOOL)setCaptureHeight:(int)height 172 width:(int)width 173 frameRate:(float)frameRate { 174 // Check if either of VideoCaptureDeviceMac::AllocateAndStart() or 175 // VideoCaptureDeviceMac::ReceiveFrame() is calling here, depending on the 176 // running state. VCDM::ReceiveFrame() calls here to change aspect ratio. 177 DCHECK((![captureSession_ isRunning] && 178 main_thread_checker_.CalledOnValidThread()) || 179 callback_thread_checker_.CalledOnValidThread()); 180 181 frameWidth_ = width; 182 frameHeight_ = height; 183 frameRate_ = frameRate; 184 185 // The capture output has to be configured, despite Mac documentation 186 // detailing that setting the sessionPreset would be enough. The reason for 187 // this mismatch is probably because most of the AVFoundation docs are written 188 // for iOS and not for MacOsX. AVVideoScalingModeKey() refers to letterboxing 189 // yes/no and preserve aspect ratio yes/no when scaling. Currently we set 190 // cropping and preservation. 191 NSDictionary* videoSettingsDictionary = @{ 192 (id)kCVPixelBufferWidthKey : @(width), 193 (id)kCVPixelBufferHeightKey : @(height), 194 (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_422YpCbCr8), 195 AVFoundationGlue::AVVideoScalingModeKey() : 196 AVFoundationGlue::AVVideoScalingModeResizeAspectFill() 197 }; 198 [captureVideoDataOutput_ setVideoSettings:videoSettingsDictionary]; 199 200 CrAVCaptureConnection* captureConnection = [captureVideoDataOutput_ 201 connectionWithMediaType:AVFoundationGlue::AVMediaTypeVideo()]; 202 // Check selector existence, related to bugs http://crbug.com/327532 and 203 // http://crbug.com/328096. 204 // CMTimeMake accepts integer argumenst but |frameRate| is float, round it. 205 if ([captureConnection 206 respondsToSelector:@selector(isVideoMinFrameDurationSupported)] && 207 [captureConnection isVideoMinFrameDurationSupported]) { 208 [captureConnection setVideoMinFrameDuration: 209 CoreMediaGlue::CMTimeMake(media::kFrameRatePrecision, 210 (int)(frameRate * media::kFrameRatePrecision))]; 211 } 212 if ([captureConnection 213 respondsToSelector:@selector(isVideoMaxFrameDurationSupported)] && 214 [captureConnection isVideoMaxFrameDurationSupported]) { 215 [captureConnection setVideoMaxFrameDuration: 216 CoreMediaGlue::CMTimeMake(media::kFrameRatePrecision, 217 (int)(frameRate * media::kFrameRatePrecision))]; 218 } 219 return YES; 220} 221 222- (BOOL)startCapture { 223 DCHECK(main_thread_checker_.CalledOnValidThread()); 224 if (!captureSession_) { 225 DLOG(ERROR) << "Video capture session not initialized."; 226 return NO; 227 } 228 // Connect the notifications. 229 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 230 [nc addObserver:self 231 selector:@selector(onVideoError:) 232 name:AVFoundationGlue::AVCaptureSessionRuntimeErrorNotification() 233 object:captureSession_]; 234 [captureSession_ startRunning]; 235 return YES; 236} 237 238- (void)stopCapture { 239 DCHECK(main_thread_checker_.CalledOnValidThread()); 240 if ([captureSession_ isRunning]) 241 [captureSession_ stopRunning]; // Synchronous. 242 [[NSNotificationCenter defaultCenter] removeObserver:self]; 243} 244 245#pragma mark Private methods 246 247// |captureOutput| is called by the capture device to deliver a new frame. 248- (void)captureOutput:(CrAVCaptureOutput*)captureOutput 249 didOutputSampleBuffer:(CoreMediaGlue::CMSampleBufferRef)sampleBuffer 250 fromConnection:(CrAVCaptureConnection*)connection { 251 // AVFoundation calls from a number of threads, depending on, at least, if 252 // Chrome is on foreground or background. Sample the actual thread here. 253 callback_thread_checker_.DetachFromThread(); 254 callback_thread_checker_.CalledOnValidThread(); 255 CVImageBufferRef videoFrame = 256 CoreMediaGlue::CMSampleBufferGetImageBuffer(sampleBuffer); 257 // Lock the frame and calculate frame size. 258 const int kLockFlags = 0; 259 if (CVPixelBufferLockBaseAddress(videoFrame, kLockFlags) == 260 kCVReturnSuccess) { 261 void* baseAddress = CVPixelBufferGetBaseAddress(videoFrame); 262 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(videoFrame); 263 size_t frameWidth = CVPixelBufferGetWidth(videoFrame); 264 size_t frameHeight = CVPixelBufferGetHeight(videoFrame); 265 size_t frameSize = bytesPerRow * frameHeight; 266 UInt8* addressToPass = reinterpret_cast<UInt8*>(baseAddress); 267 268 media::VideoCaptureFormat captureFormat( 269 gfx::Size(frameWidth, frameHeight), 270 frameRate_, 271 media::PIXEL_FORMAT_UYVY); 272 base::AutoLock lock(lock_); 273 if (!frameReceiver_) 274 return; 275 frameReceiver_->ReceiveFrame(addressToPass, frameSize, captureFormat, 0, 0); 276 CVPixelBufferUnlockBaseAddress(videoFrame, kLockFlags); 277 } 278} 279 280- (void)onVideoError:(NSNotification*)errorNotification { 281 NSError* error = base::mac::ObjCCast<NSError>([[errorNotification userInfo] 282 objectForKey:AVFoundationGlue::AVCaptureSessionErrorKey()]); 283 [self sendErrorString:[NSString 284 stringWithFormat:@"%@: %@", 285 [error localizedDescription], 286 [error localizedFailureReason]]]; 287} 288 289- (void)sendErrorString:(NSString*)error { 290 DLOG(ERROR) << [error UTF8String]; 291 base::AutoLock lock(lock_); 292 if (frameReceiver_) 293 frameReceiver_->ReceiveError([error UTF8String]); 294} 295 296@end 297