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