summaryrefslogtreecommitdiffstats
path: root/media/capture/video/mac/video_capture_device_qtkit_mac.mm
blob: 2fc0b324da1e97142f1d7d77b1c75da74667fc4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// 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.

#import "media/capture/video/mac/video_capture_device_qtkit_mac.h"

#import <QTKit/QTKit.h>

#include "base/debug/crash_logging.h"
#include "base/logging.h"
#include "media/base/video_capture_types.h"
#include "media/capture/video/mac/video_capture_device_mac.h"
#include "media/capture/video/video_capture_device.h"
#include "ui/gfx/geometry/size.h"

@implementation VideoCaptureDeviceQTKit

#pragma mark Class methods

+ (void)getDeviceNames:(NSMutableDictionary*)deviceNames {
  // Third-party drivers often throw exceptions. The following catches any
  // exceptions and continues in an orderly fashion with no devices detected.
  NSArray* captureDevices = nil;
  @try {
    captureDevices =
        [QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo];
  } @catch (id exception) {
  }

  for (QTCaptureDevice* device in captureDevices) {
    if ([[device attributeForKey:QTCaptureDeviceSuspendedAttribute] boolValue])
      continue;
    DeviceNameAndTransportType* nameAndTransportType = [[
        [DeviceNameAndTransportType alloc]
         initWithName:[device localizedDisplayName]
        transportType:media::kIOAudioDeviceTransportTypeUnknown] autorelease];
    [deviceNames setObject:nameAndTransportType forKey:[device uniqueID]];
  }
}

+ (NSDictionary*)deviceNames {
  NSMutableDictionary* deviceNames =
      [[[NSMutableDictionary alloc] init] autorelease];

  // TODO(shess): Post to the main thread to see if that helps
  // http://crbug.com/139164
  [self performSelectorOnMainThread:@selector(getDeviceNames:)
                         withObject:deviceNames
                      waitUntilDone:YES];
  return deviceNames;
}

#pragma mark Public methods

- (id)initWithFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver {
  self = [super init];
  if (self) {
    frameReceiver_ = frameReceiver;
    lock_ = [[NSLock alloc] init];
  }
  return self;
}

- (void)dealloc {
  [captureSession_ release];
  [captureDeviceInput_ release];
  [super dealloc];
}

- (void)setFrameReceiver:(media::VideoCaptureDeviceMac*)frameReceiver {
  [lock_ lock];
  frameReceiver_ = frameReceiver;
  [lock_ unlock];
}

- (BOOL)setCaptureDevice:(NSString*)deviceId {
  if (deviceId) {
    // Set the capture device.
    if (captureDeviceInput_) {
      DLOG(ERROR) << "Video capture device already set.";
      return NO;
    }

    // TODO(mcasas): Consider using [QTCaptureDevice deviceWithUniqueID] instead
    // of explicitly forcing reenumeration of devices.
    NSArray* captureDevices =
        [QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo];
    NSArray* captureDevicesNames = [captureDevices valueForKey:@"uniqueID"];
    NSUInteger index = [captureDevicesNames indexOfObject:deviceId];
    if (index == NSNotFound) {
      [self sendErrorString:[NSString stringWithUTF8String:
                                          "Video capture device not found."]];
      return NO;
    }
    QTCaptureDevice* device = [captureDevices objectAtIndex:index];
    if ([[device
            attributeForKey:QTCaptureDeviceSuspendedAttribute] boolValue]) {
      [self sendErrorString:
                [NSString stringWithUTF8String:
                              "Cannot open suspended video capture device."]];
      return NO;
    }
    NSError* error;
    if (![device open:&error]) {
      [self sendErrorString:
                [NSString stringWithFormat:
                              @"Could not open video capture device (%@): %@",
                              [error localizedDescription],
                              [error localizedFailureReason]]];
      return NO;
    }
    captureDeviceInput_ = [[QTCaptureDeviceInput alloc] initWithDevice:device];
    captureSession_ = [[QTCaptureSession alloc] init];

    QTCaptureDecompressedVideoOutput* captureDecompressedOutput =
        [[[QTCaptureDecompressedVideoOutput alloc] init] autorelease];
    [captureDecompressedOutput setDelegate:self];
    [captureDecompressedOutput setAutomaticallyDropsLateVideoFrames:YES];
    if (![captureSession_ addOutput:captureDecompressedOutput error:&error]) {
      [self
          sendErrorString:
              [NSString stringWithFormat:
                            @"Could not connect video capture output (%@): %@",
                            [error localizedDescription],
                            [error localizedFailureReason]]];
      return NO;
    }

    // This key can be used to check if video capture code was related to a
    // particular crash.
    base::debug::SetCrashKeyValue("VideoCaptureDeviceQTKit", "OpenedDevice");

    // Set the video pixel format to 2VUY (a.k.a UYVY, packed 4:2:2).
    NSDictionary* captureDictionary = [NSDictionary
        dictionaryWithObject:
            [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8]
                      forKey:(id)kCVPixelBufferPixelFormatTypeKey];
    [captureDecompressedOutput setPixelBufferAttributes:captureDictionary];

    return YES;
  } else {
    // Remove the previously set capture device.
    if (!captureDeviceInput_) {
      // Being here means stopping a device that never started OK in the first
      // place, log it.
      [self sendLogString:[NSString
                              stringWithUTF8String:
                                  "No video capture device set, on removal."]];
      return YES;
    }
    // Tear down input and output, stop the capture and deregister observers.
    [self stopCapture];
    [captureSession_ release];
    captureSession_ = nil;
    [captureDeviceInput_ release];
    captureDeviceInput_ = nil;
    return YES;
  }
}

- (BOOL)setCaptureHeight:(int)height
                   width:(int)width
               frameRate:(float)frameRate {
  if (!captureDeviceInput_) {
    [self sendErrorString:
              [NSString stringWithUTF8String:"No video capture device set."]];
    return NO;
  }
  if ([[captureSession_ outputs] count] != 1) {
    [self sendErrorString:[NSString
                              stringWithUTF8String:
                                  "Video capture capabilities already set."]];
    return NO;
  }
  if (frameRate <= 0.0f) {
    [self sendErrorString:[NSString stringWithUTF8String:"Wrong frame rate."]];
    return NO;
  }

  frameRate_ = frameRate;

  QTCaptureDecompressedVideoOutput* output =
      [[captureSession_ outputs] objectAtIndex:0];

  // Set up desired output properties. The old capture dictionary is used to
  // retrieve the initial pixel format, which must be maintained.
  NSDictionary* videoSettingsDictionary = @{
    (id)kCVPixelBufferWidthKey : @(width), (id)
    kCVPixelBufferHeightKey : @(height), (id)
    kCVPixelBufferPixelFormatTypeKey : [[output pixelBufferAttributes]
        valueForKey:(id)kCVPixelBufferPixelFormatTypeKey]
  };
  [output setPixelBufferAttributes:videoSettingsDictionary];

  [output setMinimumVideoFrameInterval:(NSTimeInterval)1 / frameRate];
  return YES;
}

- (BOOL)startCapture {
  if ([[captureSession_ outputs] count] == 0) {
    // Capture properties not set.
    [self
        sendErrorString:[NSString stringWithUTF8String:
                                      "Video capture device not initialized."]];
    return NO;
  }
  if ([[captureSession_ inputs] count] == 0) {
    NSError* error;
    if (![captureSession_ addInput:captureDeviceInput_ error:&error]) {
      [self
          sendErrorString:
              [NSString stringWithFormat:
                            @"Could not connect video capture device (%@): %@",
                            [error localizedDescription],
                            [error localizedFailureReason]]];

      return NO;
    }
    NSNotificationCenter* notificationCenter =
        [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(handleNotification:)
                               name:QTCaptureSessionRuntimeErrorNotification
                             object:captureSession_];
    [captureSession_ startRunning];
  }
  return YES;
}

- (void)stopCapture {
  // QTKit achieves thread safety and asynchronous execution by posting messages
  // to the main thread, e.g. -addOutput:. Both -removeOutput: and -removeInput:
  // post a message to the main thread while holding a lock that the
  // notification handler might need. To avoid a deadlock, we perform those
  // tasks in the main thread. See bugs http://crbug.com/152757 and
  // http://crbug.com/399792.
  [self performSelectorOnMainThread:@selector(stopCaptureOnUIThread:)
                         withObject:nil
                      waitUntilDone:YES];
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)stopCaptureOnUIThread:(id)dummy {
  if ([[captureSession_ inputs] count] > 0) {
    DCHECK_EQ([[captureSession_ inputs] count], 1u);
    [captureSession_ removeInput:captureDeviceInput_];
    [captureSession_ stopRunning];
  }
  if ([[captureSession_ outputs] count] > 0) {
    DCHECK_EQ([[captureSession_ outputs] count], 1u);
    id output = [[captureSession_ outputs] objectAtIndex:0];
    [output setDelegate:nil];
    [captureSession_ removeOutput:output];
  }
}

// |captureOutput| is called by the capture device to deliver a new frame.
- (void)captureOutput:(QTCaptureOutput*)captureOutput
    didOutputVideoFrame:(CVImageBufferRef)videoFrame
       withSampleBuffer:(QTSampleBuffer*)sampleBuffer
         fromConnection:(QTCaptureConnection*)connection {
  [lock_ lock];
  if (!frameReceiver_) {
    [lock_ unlock];
    return;
  }

  // Lock the frame and calculate frame size.
  const int kLockFlags = 0;
  if (CVPixelBufferLockBaseAddress(videoFrame, kLockFlags) ==
      kCVReturnSuccess) {
    void* baseAddress = CVPixelBufferGetBaseAddress(videoFrame);
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(videoFrame);
    size_t frameWidth = CVPixelBufferGetWidth(videoFrame);
    size_t frameHeight = CVPixelBufferGetHeight(videoFrame);
    size_t frameSize = bytesPerRow * frameHeight;

    // TODO(shess): bytesPerRow may not correspond to frameWidth_*2,
    // but VideoCaptureController::OnIncomingCapturedData() requires
    // it to do so.  Plumbing things through is intrusive, for now
    // just deliver an adjusted buffer.
    // TODO(nick): This workaround could probably be eliminated by using
    // VideoCaptureController::OnIncomingCapturedVideoFrame, which supports
    // pitches.
    UInt8* addressToPass = static_cast<UInt8*>(baseAddress);
    // UYVY is 2 bytes per pixel.
    size_t expectedBytesPerRow = frameWidth * 2;
    if (bytesPerRow > expectedBytesPerRow) {
      // TODO(shess): frameHeight and frameHeight_ are not the same,
      // try to do what the surrounding code seems to assume.
      // Ironically, captureCapability and frameSize are ignored
      // anyhow.
      adjustedFrame_.resize(expectedBytesPerRow * frameHeight);
      // std::vector is contiguous according to standard.
      UInt8* adjustedAddress = &adjustedFrame_[0];

      for (size_t y = 0; y < frameHeight; ++y) {
        memcpy(adjustedAddress + y * expectedBytesPerRow,
               addressToPass + y * bytesPerRow, expectedBytesPerRow);
      }

      addressToPass = adjustedAddress;
      frameSize = frameHeight * expectedBytesPerRow;
    }

    media::VideoCaptureFormat captureFormat(
        gfx::Size(frameWidth, frameHeight), frameRate_,
        media::VIDEO_CAPTURE_PIXEL_FORMAT_UYVY);

    // The aspect ratio dictionary is often missing, in which case we report
    // a pixel aspect ratio of 0:0.
    int aspectNumerator = 0, aspectDenominator = 0;
    CFDictionaryRef aspectRatioDict = (CFDictionaryRef)CVBufferGetAttachment(
        videoFrame, kCVImageBufferPixelAspectRatioKey, NULL);
    if (aspectRatioDict) {
      CFNumberRef aspectNumeratorRef = (CFNumberRef)CFDictionaryGetValue(
          aspectRatioDict, kCVImageBufferPixelAspectRatioHorizontalSpacingKey);
      CFNumberRef aspectDenominatorRef = (CFNumberRef)CFDictionaryGetValue(
          aspectRatioDict, kCVImageBufferPixelAspectRatioVerticalSpacingKey);
      DCHECK(aspectNumeratorRef && aspectDenominatorRef)
          << "Aspect Ratio dictionary missing its entries.";
      CFNumberGetValue(aspectNumeratorRef, kCFNumberIntType, &aspectNumerator);
      CFNumberGetValue(aspectDenominatorRef, kCFNumberIntType,
                       &aspectDenominator);
    }

    // Deliver the captured video frame.
    frameReceiver_->ReceiveFrame(addressToPass, frameSize, captureFormat,
                                 aspectNumerator, aspectDenominator);

    CVPixelBufferUnlockBaseAddress(videoFrame, kLockFlags);
  }
  [lock_ unlock];
}

- (void)handleNotification:(NSNotification*)errorNotification {
  NSError* error = (NSError*)
      [[errorNotification userInfo] objectForKey:QTCaptureSessionErrorKey];
  [self sendErrorString:
            [NSString stringWithFormat:@"%@: %@", [error localizedDescription],
                                       [error localizedFailureReason]]];
}

- (void)sendErrorString:(NSString*)error {
  DLOG(ERROR) << [error UTF8String];
  [lock_ lock];
  if (frameReceiver_)
    frameReceiver_->ReceiveError([error UTF8String]);
  [lock_ unlock];
}

- (void)sendLogString:(NSString*)message {
  DVLOG(1) << [message UTF8String];
  [lock_ lock];
  if (frameReceiver_)
    frameReceiver_->LogMessage([message UTF8String]);
  [lock_ unlock];
}

@end