blob: c466512bd0ed4976e9927a8eb1d6b54990607624 [file] [log] [blame]
Jon Hjelle7ac8bab2016-01-21 11:44:55 -08001/*
2 * Copyright 2015 The WebRTC project authors. All Rights Reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include "webrtc/api/objc/avfoundationvideocapturer.h"
12
13#include "webrtc/base/bind.h"
14
15#import <AVFoundation/AVFoundation.h>
16#import <Foundation/Foundation.h>
17#import <UIKit/UIKit.h>
18
19#import "webrtc/base/objc/RTCDispatcher.h"
hjona1cf3662016-03-14 20:55:22 -070020#import "webrtc/base/objc/RTCLogging.h"
Jon Hjelle7ac8bab2016-01-21 11:44:55 -080021
22// TODO(tkchin): support other formats.
23static NSString* const kDefaultPreset = AVCaptureSessionPreset640x480;
24static cricket::VideoFormat const kDefaultFormat =
25 cricket::VideoFormat(640,
26 480,
27 cricket::VideoFormat::FpsToInterval(30),
28 cricket::FOURCC_NV12);
29
30// This class used to capture frames using AVFoundation APIs on iOS. It is meant
31// to be owned by an instance of AVFoundationVideoCapturer. The reason for this
32// because other webrtc objects own cricket::VideoCapturer, which is not
33// ref counted. To prevent bad behavior we do not expose this class directly.
34@interface RTCAVFoundationVideoCapturerInternal : NSObject
35 <AVCaptureVideoDataOutputSampleBufferDelegate>
36
37@property(nonatomic, readonly) AVCaptureSession *captureSession;
38@property(nonatomic, readonly) BOOL isRunning;
hjona1cf3662016-03-14 20:55:22 -070039@property(nonatomic, readonly) BOOL canUseBackCamera;
Jon Hjelle7ac8bab2016-01-21 11:44:55 -080040@property(nonatomic, assign) BOOL useBackCamera; // Defaults to NO.
41
42// We keep a pointer back to AVFoundationVideoCapturer to make callbacks on it
43// when we receive frames. This is safe because this object should be owned by
44// it.
45- (instancetype)initWithCapturer:(webrtc::AVFoundationVideoCapturer *)capturer;
46- (void)startCaptureAsync;
47- (void)stopCaptureAsync;
48
49@end
50
51@implementation RTCAVFoundationVideoCapturerInternal {
52 // Keep pointers to inputs for convenience.
53 AVCaptureDeviceInput *_frontDeviceInput;
54 AVCaptureDeviceInput *_backDeviceInput;
55 AVCaptureVideoDataOutput *_videoOutput;
56 // The cricket::VideoCapturer that owns this class. Should never be NULL.
57 webrtc::AVFoundationVideoCapturer *_capturer;
58 BOOL _orientationHasChanged;
59}
60
61@synthesize captureSession = _captureSession;
62@synthesize useBackCamera = _useBackCamera;
63@synthesize isRunning = _isRunning;
64
65- (instancetype)initWithCapturer:(webrtc::AVFoundationVideoCapturer *)capturer {
66 NSParameterAssert(capturer);
67 if (self = [super init]) {
68 _capturer = capturer;
69 if (![self setupCaptureSession]) {
70 return nil;
71 }
72 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
73 [center addObserver:self
74 selector:@selector(deviceOrientationDidChange:)
75 name:UIDeviceOrientationDidChangeNotification
76 object:nil];
77 [center addObserverForName:AVCaptureSessionRuntimeErrorNotification
78 object:nil
79 queue:nil
80 usingBlock:^(NSNotification *notification) {
81 NSLog(@"Capture session error: %@", notification.userInfo);
82 }];
83 }
84 return self;
85}
86
87- (void)dealloc {
88 [self stopCaptureAsync];
89 [[NSNotificationCenter defaultCenter] removeObserver:self];
90 _capturer = nullptr;
91}
92
hjona1cf3662016-03-14 20:55:22 -070093- (BOOL)canUseBackCamera {
94 return _backDeviceInput != nil;
95}
96
Jon Hjelle7ac8bab2016-01-21 11:44:55 -080097- (void)setUseBackCamera:(BOOL)useBackCamera {
98 if (_useBackCamera == useBackCamera) {
99 return;
100 }
hjona1cf3662016-03-14 20:55:22 -0700101 if (!self.canUseBackCamera) {
102 RTCLog(@"No rear-facing camera exists or it cannot be used;"
103 "not switching.");
104 return;
105 }
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800106 _useBackCamera = useBackCamera;
107 [self updateSessionInput];
108}
109
110- (void)startCaptureAsync {
111 if (_isRunning) {
112 return;
113 }
114 _orientationHasChanged = NO;
115 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
116 AVCaptureSession* session = _captureSession;
117 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession
118 block:^{
119 [session startRunning];
120 }];
121 _isRunning = YES;
122}
123
124- (void)stopCaptureAsync {
125 if (!_isRunning) {
126 return;
127 }
128 [_videoOutput setSampleBufferDelegate:nil queue:nullptr];
129 AVCaptureSession* session = _captureSession;
130 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession
131 block:^{
132 [session stopRunning];
133 }];
134 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
135 _isRunning = NO;
136}
137
138#pragma mark AVCaptureVideoDataOutputSampleBufferDelegate
139
140- (void)captureOutput:(AVCaptureOutput *)captureOutput
141 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
142 fromConnection:(AVCaptureConnection *)connection {
143 NSParameterAssert(captureOutput == _videoOutput);
144 if (!_isRunning) {
145 return;
146 }
147 _capturer->CaptureSampleBuffer(sampleBuffer);
148}
149
150- (void)captureOutput:(AVCaptureOutput *)captureOutput
151 didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
152 fromConnection:(AVCaptureConnection *)connection {
153 NSLog(@"Dropped sample buffer.");
154}
155
156#pragma mark - Private
157
158- (BOOL)setupCaptureSession {
159 _captureSession = [[AVCaptureSession alloc] init];
160#if defined(__IPHONE_7_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_0
161 NSString *version = [[UIDevice currentDevice] systemVersion];
162 if ([version integerValue] >= 7) {
163 _captureSession.usesApplicationAudioSession = NO;
164 }
165#endif
166 if (![_captureSession canSetSessionPreset:kDefaultPreset]) {
167 NSLog(@"Default video capture preset unsupported.");
168 return NO;
169 }
170 _captureSession.sessionPreset = kDefaultPreset;
171
172 // Make the capturer output NV12. Ideally we want I420 but that's not
173 // currently supported on iPhone / iPad.
174 _videoOutput = [[AVCaptureVideoDataOutput alloc] init];
175 _videoOutput.videoSettings = @{
176 (NSString *)kCVPixelBufferPixelFormatTypeKey :
177 @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
178 };
179 _videoOutput.alwaysDiscardsLateVideoFrames = NO;
180 [_videoOutput setSampleBufferDelegate:self
181 queue:dispatch_get_main_queue()];
182 if (![_captureSession canAddOutput:_videoOutput]) {
183 NSLog(@"Default video capture output unsupported.");
184 return NO;
185 }
186 [_captureSession addOutput:_videoOutput];
187
188 // Find the capture devices.
189 AVCaptureDevice *frontCaptureDevice = nil;
190 AVCaptureDevice *backCaptureDevice = nil;
191 for (AVCaptureDevice *captureDevice in
192 [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
193 if (captureDevice.position == AVCaptureDevicePositionBack) {
194 backCaptureDevice = captureDevice;
195 }
196 if (captureDevice.position == AVCaptureDevicePositionFront) {
197 frontCaptureDevice = captureDevice;
198 }
199 }
hjona1cf3662016-03-14 20:55:22 -0700200 if (!frontCaptureDevice) {
201 RTCLog(@"Failed to get front capture device.");
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800202 return NO;
203 }
hjona1cf3662016-03-14 20:55:22 -0700204 if (!backCaptureDevice) {
205 RTCLog(@"Failed to get back capture device");
206 // Don't return NO here because devices exist (16GB 5th generation iPod
207 // Touch) that don't have a rear-facing camera.
208 }
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800209
210 // Set up the session inputs.
211 NSError *error = nil;
212 _frontDeviceInput =
213 [AVCaptureDeviceInput deviceInputWithDevice:frontCaptureDevice
214 error:&error];
215 if (!_frontDeviceInput) {
216 NSLog(@"Failed to get capture device input: %@",
217 error.localizedDescription);
218 return NO;
219 }
hjona1cf3662016-03-14 20:55:22 -0700220 if (backCaptureDevice) {
221 error = nil;
222 _backDeviceInput =
223 [AVCaptureDeviceInput deviceInputWithDevice:backCaptureDevice
224 error:&error];
225 if (error) {
226 RTCLog(@"Failed to get capture device input: %@",
227 error.localizedDescription);
228 _backDeviceInput = nil;
229 }
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800230 }
231
232 // Add the inputs.
233 if (![_captureSession canAddInput:_frontDeviceInput] ||
hjona1cf3662016-03-14 20:55:22 -0700234 (_backDeviceInput && ![_captureSession canAddInput:_backDeviceInput])) {
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800235 NSLog(@"Session does not support capture inputs.");
236 return NO;
237 }
238 [self updateSessionInput];
239
240 return YES;
241}
242
243- (void)deviceOrientationDidChange:(NSNotification *)notification {
244 _orientationHasChanged = YES;
245 [self updateOrientation];
246}
247
248- (void)updateOrientation {
249 AVCaptureConnection *connection =
250 [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
251 if (!connection.supportsVideoOrientation) {
252 // TODO(tkchin): set rotation bit on frames.
253 return;
254 }
255 AVCaptureVideoOrientation orientation = AVCaptureVideoOrientationPortrait;
256 switch ([UIDevice currentDevice].orientation) {
257 case UIDeviceOrientationPortrait:
258 orientation = AVCaptureVideoOrientationPortrait;
259 break;
260 case UIDeviceOrientationPortraitUpsideDown:
261 orientation = AVCaptureVideoOrientationPortraitUpsideDown;
262 break;
263 case UIDeviceOrientationLandscapeLeft:
264 orientation = AVCaptureVideoOrientationLandscapeRight;
265 break;
266 case UIDeviceOrientationLandscapeRight:
267 orientation = AVCaptureVideoOrientationLandscapeLeft;
268 break;
269 case UIDeviceOrientationFaceUp:
270 case UIDeviceOrientationFaceDown:
271 case UIDeviceOrientationUnknown:
272 if (!_orientationHasChanged) {
273 connection.videoOrientation = orientation;
274 }
275 return;
276 }
277 connection.videoOrientation = orientation;
278}
279
280- (void)updateSessionInput {
281 // Update the current session input to match what's stored in _useBackCamera.
282 [_captureSession beginConfiguration];
283 AVCaptureDeviceInput *oldInput = _backDeviceInput;
284 AVCaptureDeviceInput *newInput = _frontDeviceInput;
285 if (_useBackCamera) {
286 oldInput = _frontDeviceInput;
287 newInput = _backDeviceInput;
288 }
289 // Ok to remove this even if it's not attached. Will be no-op.
290 [_captureSession removeInput:oldInput];
291 [_captureSession addInput:newInput];
292 [self updateOrientation];
293 [_captureSession commitConfiguration];
294}
295
296@end
297
298namespace webrtc {
299
300AVFoundationVideoCapturer::AVFoundationVideoCapturer()
301 : _capturer(nil), _startThread(nullptr) {
302 // Set our supported formats. This matches kDefaultPreset.
303 std::vector<cricket::VideoFormat> supportedFormats;
304 supportedFormats.push_back(cricket::VideoFormat(kDefaultFormat));
305 SetSupportedFormats(supportedFormats);
306 _capturer =
307 [[RTCAVFoundationVideoCapturerInternal alloc] initWithCapturer:this];
308}
309
310AVFoundationVideoCapturer::~AVFoundationVideoCapturer() {
311 _capturer = nil;
312}
313
314cricket::CaptureState AVFoundationVideoCapturer::Start(
315 const cricket::VideoFormat& format) {
316 if (!_capturer) {
317 LOG(LS_ERROR) << "Failed to create AVFoundation capturer.";
318 return cricket::CaptureState::CS_FAILED;
319 }
320 if (_capturer.isRunning) {
321 LOG(LS_ERROR) << "The capturer is already running.";
322 return cricket::CaptureState::CS_FAILED;
323 }
324 if (format != kDefaultFormat) {
325 LOG(LS_ERROR) << "Unsupported format provided.";
326 return cricket::CaptureState::CS_FAILED;
327 }
328
329 // Keep track of which thread capture started on. This is the thread that
330 // frames need to be sent to.
331 RTC_DCHECK(!_startThread);
332 _startThread = rtc::Thread::Current();
333
334 SetCaptureFormat(&format);
335 // This isn't super accurate because it takes a while for the AVCaptureSession
336 // to spin up, and this call returns async.
337 // TODO(tkchin): make this better.
338 [_capturer startCaptureAsync];
339 SetCaptureState(cricket::CaptureState::CS_RUNNING);
340
341 return cricket::CaptureState::CS_STARTING;
342}
343
344void AVFoundationVideoCapturer::Stop() {
345 [_capturer stopCaptureAsync];
346 SetCaptureFormat(NULL);
347 _startThread = nullptr;
348}
349
350bool AVFoundationVideoCapturer::IsRunning() {
351 return _capturer.isRunning;
352}
353
354AVCaptureSession* AVFoundationVideoCapturer::GetCaptureSession() {
355 return _capturer.captureSession;
356}
357
hjona1cf3662016-03-14 20:55:22 -0700358bool AVFoundationVideoCapturer::CanUseBackCamera() const {
359 return _capturer.canUseBackCamera;
360}
361
Jon Hjelle7ac8bab2016-01-21 11:44:55 -0800362void AVFoundationVideoCapturer::SetUseBackCamera(bool useBackCamera) {
363 _capturer.useBackCamera = useBackCamera;
364}
365
366bool AVFoundationVideoCapturer::GetUseBackCamera() const {
367 return _capturer.useBackCamera;
368}
369
370void AVFoundationVideoCapturer::CaptureSampleBuffer(
371 CMSampleBufferRef sampleBuffer) {
372 if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
373 !CMSampleBufferIsValid(sampleBuffer) ||
374 !CMSampleBufferDataIsReady(sampleBuffer)) {
375 return;
376 }
377
378 CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
379 if (imageBuffer == NULL) {
380 return;
381 }
382
383 // Base address must be unlocked to access frame data.
384 CVOptionFlags lockFlags = kCVPixelBufferLock_ReadOnly;
385 CVReturn ret = CVPixelBufferLockBaseAddress(imageBuffer, lockFlags);
386 if (ret != kCVReturnSuccess) {
387 return;
388 }
389
390 static size_t const kYPlaneIndex = 0;
391 static size_t const kUVPlaneIndex = 1;
392 uint8_t *yPlaneAddress =
393 (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, kYPlaneIndex);
394 size_t yPlaneHeight =
395 CVPixelBufferGetHeightOfPlane(imageBuffer, kYPlaneIndex);
396 size_t yPlaneWidth =
397 CVPixelBufferGetWidthOfPlane(imageBuffer, kYPlaneIndex);
398 size_t yPlaneBytesPerRow =
399 CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, kYPlaneIndex);
400 size_t uvPlaneHeight =
401 CVPixelBufferGetHeightOfPlane(imageBuffer, kUVPlaneIndex);
402 size_t uvPlaneBytesPerRow =
403 CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, kUVPlaneIndex);
404 size_t frameSize =
405 yPlaneBytesPerRow * yPlaneHeight + uvPlaneBytesPerRow * uvPlaneHeight;
406
407 // Sanity check assumption that planar bytes are contiguous.
408 uint8_t *uvPlaneAddress =
409 (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, kUVPlaneIndex);
410 RTC_DCHECK(
411 uvPlaneAddress == yPlaneAddress + yPlaneHeight * yPlaneBytesPerRow);
412
413 // Stuff data into a cricket::CapturedFrame.
414 int64_t currentTime = rtc::TimeNanos();
415 cricket::CapturedFrame frame;
416 frame.width = yPlaneWidth;
417 frame.height = yPlaneHeight;
418 frame.pixel_width = 1;
419 frame.pixel_height = 1;
420 frame.fourcc = static_cast<uint32_t>(cricket::FOURCC_NV12);
421 frame.time_stamp = currentTime;
422 frame.data = yPlaneAddress;
423 frame.data_size = frameSize;
424
425 if (_startThread->IsCurrent()) {
426 SignalFrameCaptured(this, &frame);
427 } else {
428 _startThread->Invoke<void>(
429 rtc::Bind(&AVFoundationVideoCapturer::SignalFrameCapturedOnStartThread,
430 this, &frame));
431 }
432 CVPixelBufferUnlockBaseAddress(imageBuffer, lockFlags);
433}
434
435void AVFoundationVideoCapturer::SignalFrameCapturedOnStartThread(
436 const cricket::CapturedFrame *frame) {
437 RTC_DCHECK(_startThread->IsCurrent());
438 // This will call a superclass method that will perform the frame conversion
439 // to I420.
440 SignalFrameCaptured(this, frame);
441}
442
443} // namespace webrtc