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.
5 #import "media/video/capture/mac/video_capture_device_qtkit_mac.h"
7 #import <QTKit/QTKit.h>
9 #include "base/debug/crash_logging.h"
10 #include "base/logging.h"
11 #include "base/mac/scoped_nsexception_enabler.h"
12 #include "media/video/capture/mac/video_capture_device_mac.h"
13 #include "media/video/capture/video_capture_device.h"
14 #include "media/video/capture/video_capture_types.h"
15 #include "ui/gfx/size.h"
17 @implementation VideoCaptureDeviceQTKit
19 #pragma mark Class methods
21 + (void)getDeviceNames:(NSMutableDictionary*)deviceNames {
22 // Third-party drivers often throw exceptions, which are fatal in
23 // Chromium (see comments in scoped_nsexception_enabler.h). The
24 // following catches any exceptions and continues in an orderly
25 // fashion with no devices detected.
26 NSArray* captureDevices =
27 base::mac::RunBlockIgnoringExceptions(^{
28 return [QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo];
31 for (QTCaptureDevice* device in captureDevices) {
32 if ([[device attributeForKey:QTCaptureDeviceSuspendedAttribute] boolValue])
34 DeviceNameAndTransportType* nameAndTransportType =
35 [[[DeviceNameAndTransportType alloc]
36 initWithName:[device localizedDisplayName]
37 transportType:media::kIOAudioDeviceTransportTypeUnknown]
39 [deviceNames setObject:nameAndTransportType
40 forKey:[device uniqueID]];
44 + (NSDictionary*)deviceNames {
45 NSMutableDictionary* deviceNames =
46 [[[NSMutableDictionary alloc] init] autorelease];
48 // TODO(shess): Post to the main thread to see if that helps
49 // http://crbug.com/139164
50 [self performSelectorOnMainThread:@selector(getDeviceNames:)
51 withObject:deviceNames
56 #pragma mark Public methods
58 - (id)initWithFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver {
61 frameReceiver_ = frameReceiver;
62 lock_ = [[NSLock alloc] init];
68 [captureSession_ release];
69 [captureDeviceInput_ release];
73 - (void)setFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver {
75 frameReceiver_ = frameReceiver;
79 - (BOOL)setCaptureDevice:(NSString*)deviceId {
81 // Set the capture device.
82 if (captureDeviceInput_) {
83 DLOG(ERROR) << "Video capture device already set.";
87 // TODO(mcasas): Consider using [QTCaptureDevice deviceWithUniqueID] instead
88 // of explicitly forcing reenumeration of devices.
89 NSArray *captureDevices =
90 [QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo];
91 NSArray *captureDevicesNames =
92 [captureDevices valueForKey:@"uniqueID"];
93 NSUInteger index = [captureDevicesNames indexOfObject:deviceId];
94 if (index == NSNotFound) {
95 [self sendErrorString:[NSString
96 stringWithUTF8String:"Video capture device not found."]];
99 QTCaptureDevice *device = [captureDevices objectAtIndex:index];
100 if ([[device attributeForKey:QTCaptureDeviceSuspendedAttribute]
102 [self sendErrorString:[NSString
103 stringWithUTF8String:"Cannot open suspended video capture device."]];
107 if (![device open:&error]) {
108 [self sendErrorString:[NSString
109 stringWithFormat:@"Could not open video capture device (%@): %@",
110 [error localizedDescription],
111 [error localizedFailureReason]]];
114 captureDeviceInput_ = [[QTCaptureDeviceInput alloc] initWithDevice:device];
115 captureSession_ = [[QTCaptureSession alloc] init];
117 QTCaptureDecompressedVideoOutput *captureDecompressedOutput =
118 [[[QTCaptureDecompressedVideoOutput alloc] init] autorelease];
119 [captureDecompressedOutput setDelegate:self];
120 if (![captureSession_ addOutput:captureDecompressedOutput error:&error]) {
121 [self sendErrorString:[NSString
122 stringWithFormat:@"Could not connect video capture output (%@): %@",
123 [error localizedDescription],
124 [error localizedFailureReason]]];
128 // This key can be used to check if video capture code was related to a
130 base::debug::SetCrashKeyValue("VideoCaptureDeviceQTKit", "OpenedDevice");
132 // Set the video pixel format to 2VUY (a.k.a UYVY, packed 4:2:2).
133 NSDictionary *captureDictionary = [NSDictionary
134 dictionaryWithObject:
135 [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8]
136 forKey:(id)kCVPixelBufferPixelFormatTypeKey];
137 [captureDecompressedOutput setPixelBufferAttributes:captureDictionary];
141 // Remove the previously set capture device.
142 if (!captureDeviceInput_) {
143 [self sendErrorString:[NSString
144 stringWithUTF8String:"No video capture device set, on removal."]];
147 if ([[captureSession_ inputs] count] > 0) {
148 // The device is still running.
151 if ([[captureSession_ outputs] count] > 0) {
152 // Only one output is set for |captureSession_|.
153 DCHECK_EQ([[captureSession_ outputs] count], 1u);
154 id output = [[captureSession_ outputs] objectAtIndex:0];
155 [output setDelegate:nil];
157 // TODO(shess): QTKit achieves thread safety by posting messages to the
158 // main thread. As part of -addOutput:, it posts a message to the main
159 // thread which in turn posts a notification which will run in a future
160 // spin after the original method returns. -removeOutput: can post a
161 // main-thread message in between while holding a lock which the
162 // notification handler will need. Posting either -addOutput: or
163 // -removeOutput: to the main thread should fix it, remove is likely
164 // safer. http://crbug.com/152757
165 [captureSession_ performSelectorOnMainThread:@selector(removeOutput:)
169 [captureSession_ release];
170 captureSession_ = nil;
171 [captureDeviceInput_ release];
172 captureDeviceInput_ = nil;
177 - (BOOL)setCaptureHeight:(int)height
179 frameRate:(float)frameRate {
180 if (!captureDeviceInput_) {
181 [self sendErrorString:[NSString
182 stringWithUTF8String:"No video capture device set."]];
185 if ([[captureSession_ outputs] count] != 1) {
186 [self sendErrorString:[NSString
187 stringWithUTF8String:"Video capture capabilities already set."]];
190 if (frameRate <= 0.0f) {
191 [self sendErrorString:[NSString stringWithUTF8String: "Wrong frame rate."]];
195 frameRate_ = frameRate;
197 QTCaptureDecompressedVideoOutput *output =
198 [[captureSession_ outputs] objectAtIndex:0];
200 // Set up desired output properties. The old capture dictionary is used to
201 // retrieve the initial pixel format, which must be maintained.
202 NSDictionary* videoSettingsDictionary = @{
203 (id)kCVPixelBufferWidthKey : @(width),
204 (id)kCVPixelBufferHeightKey : @(height),
205 (id)kCVPixelBufferPixelFormatTypeKey : [[output pixelBufferAttributes]
206 valueForKey:(id)kCVPixelBufferPixelFormatTypeKey]
208 [output setPixelBufferAttributes:videoSettingsDictionary];
210 [output setMinimumVideoFrameInterval:(NSTimeInterval)1/frameRate];
214 - (BOOL)startCapture {
215 if ([[captureSession_ outputs] count] == 0) {
216 // Capture properties not set.
217 [self sendErrorString:[NSString
218 stringWithUTF8String:"Video capture device not initialized."]];
221 if ([[captureSession_ inputs] count] == 0) {
223 if (![captureSession_ addInput:captureDeviceInput_ error:&error]) {
224 [self sendErrorString:[NSString
225 stringWithFormat:@"Could not connect video capture device (%@): %@",
226 [error localizedDescription],
227 [error localizedFailureReason]]];
231 NSNotificationCenter * notificationCenter =
232 [NSNotificationCenter defaultCenter];
233 [notificationCenter addObserver:self
234 selector:@selector(handleNotification:)
235 name:QTCaptureSessionRuntimeErrorNotification
236 object:captureSession_];
237 [captureSession_ startRunning];
242 - (void)stopCapture {
243 if ([[captureSession_ inputs] count] == 1) {
244 [captureSession_ removeInput:captureDeviceInput_];
245 [captureSession_ stopRunning];
248 [[NSNotificationCenter defaultCenter] removeObserver:self];
251 // |captureOutput| is called by the capture device to deliver a new frame.
252 - (void)captureOutput:(QTCaptureOutput*)captureOutput
253 didOutputVideoFrame:(CVImageBufferRef)videoFrame
254 withSampleBuffer:(QTSampleBuffer*)sampleBuffer
255 fromConnection:(QTCaptureConnection*)connection {
257 if(!frameReceiver_) {
262 // Lock the frame and calculate frame size.
263 const int kLockFlags = 0;
264 if (CVPixelBufferLockBaseAddress(videoFrame, kLockFlags)
265 == kCVReturnSuccess) {
266 void *baseAddress = CVPixelBufferGetBaseAddress(videoFrame);
267 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(videoFrame);
268 size_t frameWidth = CVPixelBufferGetWidth(videoFrame);
269 size_t frameHeight = CVPixelBufferGetHeight(videoFrame);
270 size_t frameSize = bytesPerRow * frameHeight;
272 // TODO(shess): bytesPerRow may not correspond to frameWidth_*2,
273 // but VideoCaptureController::OnIncomingCapturedData() requires
274 // it to do so. Plumbing things through is intrusive, for now
275 // just deliver an adjusted buffer.
276 // TODO(nick): This workaround could probably be eliminated by using
277 // VideoCaptureController::OnIncomingCapturedVideoFrame, which supports
279 UInt8* addressToPass = static_cast<UInt8*>(baseAddress);
280 // UYVY is 2 bytes per pixel.
281 size_t expectedBytesPerRow = frameWidth * 2;
282 if (bytesPerRow > expectedBytesPerRow) {
283 // TODO(shess): frameHeight and frameHeight_ are not the same,
284 // try to do what the surrounding code seems to assume.
285 // Ironically, captureCapability and frameSize are ignored
287 adjustedFrame_.resize(expectedBytesPerRow * frameHeight);
288 // std::vector is contiguous according to standard.
289 UInt8* adjustedAddress = &adjustedFrame_[0];
291 for (size_t y = 0; y < frameHeight; ++y) {
292 memcpy(adjustedAddress + y * expectedBytesPerRow,
293 addressToPass + y * bytesPerRow,
294 expectedBytesPerRow);
297 addressToPass = adjustedAddress;
298 frameSize = frameHeight * expectedBytesPerRow;
301 media::VideoCaptureFormat captureFormat(gfx::Size(frameWidth, frameHeight),
303 media::PIXEL_FORMAT_UYVY);
305 // The aspect ratio dictionary is often missing, in which case we report
306 // a pixel aspect ratio of 0:0.
307 int aspectNumerator = 0, aspectDenominator = 0;
308 CFDictionaryRef aspectRatioDict = (CFDictionaryRef)CVBufferGetAttachment(
309 videoFrame, kCVImageBufferPixelAspectRatioKey, NULL);
310 if (aspectRatioDict) {
311 CFNumberRef aspectNumeratorRef = (CFNumberRef)CFDictionaryGetValue(
312 aspectRatioDict, kCVImageBufferPixelAspectRatioHorizontalSpacingKey);
313 CFNumberRef aspectDenominatorRef = (CFNumberRef)CFDictionaryGetValue(
314 aspectRatioDict, kCVImageBufferPixelAspectRatioVerticalSpacingKey);
315 DCHECK(aspectNumeratorRef && aspectDenominatorRef) <<
316 "Aspect Ratio dictionary missing its entries.";
317 CFNumberGetValue(aspectNumeratorRef, kCFNumberIntType, &aspectNumerator);
319 aspectDenominatorRef, kCFNumberIntType, &aspectDenominator);
322 // Deliver the captured video frame.
323 frameReceiver_->ReceiveFrame(addressToPass, frameSize, captureFormat,
324 aspectNumerator, aspectDenominator);
326 CVPixelBufferUnlockBaseAddress(videoFrame, kLockFlags);
331 - (void)handleNotification:(NSNotification*)errorNotification {
332 NSError * error = (NSError*)[[errorNotification userInfo]
333 objectForKey:QTCaptureSessionErrorKey];
334 [self sendErrorString:[NSString
335 stringWithFormat:@"%@: %@",
336 [error localizedDescription],
337 [error localizedFailureReason]]];
340 - (void)sendErrorString:(NSString*)error {
341 DLOG(ERROR) << [error UTF8String];
344 frameReceiver_->ReceiveError([error UTF8String]);