blob: 0f9dc6825e9135461a6d8d8c5ad713c00c2c01b3 [file] [log] [blame]
Zeke Chin57cc74e2015-05-05 07:52:31 -07001/*
2 * libjingle
3 * Copyright 2015 Google Inc.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 * 3. The name of the author may not be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#include "talk/app/webrtc/objc/avfoundationvideocapturer.h"
29
30#include "webrtc/base/bind.h"
31
32#import <AVFoundation/AVFoundation.h>
33#import <Foundation/Foundation.h>
34#import <UIKit/UIKit.h>
35
hayscedd8fef2015-12-08 11:08:39 -080036#import "webrtc/base/objc/RTCDispatcher.h"
37
Zeke Chin57cc74e2015-05-05 07:52:31 -070038// TODO(tkchin): support other formats.
39static NSString* const kDefaultPreset = AVCaptureSessionPreset640x480;
40static cricket::VideoFormat const kDefaultFormat =
41 cricket::VideoFormat(640,
42 480,
43 cricket::VideoFormat::FpsToInterval(30),
44 cricket::FOURCC_NV12);
45
Zeke Chin57cc74e2015-05-05 07:52:31 -070046// This class used to capture frames using AVFoundation APIs on iOS. It is meant
47// to be owned by an instance of AVFoundationVideoCapturer. The reason for this
48// because other webrtc objects own cricket::VideoCapturer, which is not
49// ref counted. To prevent bad behavior we do not expose this class directly.
50@interface RTCAVFoundationVideoCapturerInternal : NSObject
51 <AVCaptureVideoDataOutputSampleBufferDelegate>
52
53@property(nonatomic, readonly) AVCaptureSession* captureSession;
54@property(nonatomic, readonly) BOOL isRunning;
55@property(nonatomic, assign) BOOL useBackCamera; // Defaults to NO.
56
57// We keep a pointer back to AVFoundationVideoCapturer to make callbacks on it
58// when we receive frames. This is safe because this object should be owned by
59// it.
60- (instancetype)initWithCapturer:(webrtc::AVFoundationVideoCapturer*)capturer;
61- (void)startCaptureAsync;
62- (void)stopCaptureAsync;
63
64@end
65
66@implementation RTCAVFoundationVideoCapturerInternal {
67 // Keep pointers to inputs for convenience.
68 AVCaptureDeviceInput* _frontDeviceInput;
69 AVCaptureDeviceInput* _backDeviceInput;
70 AVCaptureVideoDataOutput* _videoOutput;
71 // The cricket::VideoCapturer that owns this class. Should never be NULL.
72 webrtc::AVFoundationVideoCapturer* _capturer;
73 BOOL _orientationHasChanged;
74}
75
76@synthesize captureSession = _captureSession;
77@synthesize useBackCamera = _useBackCamera;
78@synthesize isRunning = _isRunning;
79
Zeke Chin57cc74e2015-05-05 07:52:31 -070080- (instancetype)initWithCapturer:(webrtc::AVFoundationVideoCapturer*)capturer {
81 NSParameterAssert(capturer);
82 if (self = [super init]) {
83 _capturer = capturer;
84 if (![self setupCaptureSession]) {
85 return nil;
86 }
87 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
88 [center addObserver:self
89 selector:@selector(deviceOrientationDidChange:)
90 name:UIDeviceOrientationDidChangeNotification
91 object:nil];
92 [center addObserverForName:AVCaptureSessionRuntimeErrorNotification
93 object:nil
94 queue:nil
95 usingBlock:^(NSNotification* notification) {
96 NSLog(@"Capture session error: %@", notification.userInfo);
97 }];
98 }
99 return self;
100}
101
102- (void)dealloc {
103 [self stopCaptureAsync];
104 [[NSNotificationCenter defaultCenter] removeObserver:self];
105 _capturer = nullptr;
106}
107
108- (void)setUseBackCamera:(BOOL)useBackCamera {
109 if (_useBackCamera == useBackCamera) {
110 return;
111 }
112 _useBackCamera = useBackCamera;
113 [self updateSessionInput];
114}
115
116- (void)startCaptureAsync {
117 if (_isRunning) {
118 return;
119 }
120 _orientationHasChanged = NO;
121 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
122 AVCaptureSession* session = _captureSession;
hayscedd8fef2015-12-08 11:08:39 -0800123 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession
124 block:^{
Zeke Chin57cc74e2015-05-05 07:52:31 -0700125 [session startRunning];
hayscedd8fef2015-12-08 11:08:39 -0800126 }];
Zeke Chin57cc74e2015-05-05 07:52:31 -0700127 _isRunning = YES;
128}
129
130- (void)stopCaptureAsync {
131 if (!_isRunning) {
132 return;
133 }
Jon Hjelle2f65ac12015-06-12 11:33:45 -0700134 [_videoOutput setSampleBufferDelegate:nil queue:nullptr];
Zeke Chin57cc74e2015-05-05 07:52:31 -0700135 AVCaptureSession* session = _captureSession;
hayscedd8fef2015-12-08 11:08:39 -0800136 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession
137 block:^{
Zeke Chin57cc74e2015-05-05 07:52:31 -0700138 [session stopRunning];
hayscedd8fef2015-12-08 11:08:39 -0800139 }];
Zeke Chin57cc74e2015-05-05 07:52:31 -0700140 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
141 _isRunning = NO;
142}
143
144#pragma mark AVCaptureVideoDataOutputSampleBufferDelegate
145
146- (void)captureOutput:(AVCaptureOutput*)captureOutput
147 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
148 fromConnection:(AVCaptureConnection*)connection {
149 NSParameterAssert(captureOutput == _videoOutput);
150 if (!_isRunning) {
151 return;
152 }
153 _capturer->CaptureSampleBuffer(sampleBuffer);
154}
155
156- (void)captureOutput:(AVCaptureOutput*)captureOutput
157 didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
158 fromConnection:(AVCaptureConnection*)connection {
159 NSLog(@"Dropped sample buffer.");
160}
161
162#pragma mark - Private
163
164- (BOOL)setupCaptureSession {
165 _captureSession = [[AVCaptureSession alloc] init];
166#if defined(__IPHONE_7_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_0
167 NSString* version = [[UIDevice currentDevice] systemVersion];
168 if ([version integerValue] >= 7) {
169 _captureSession.usesApplicationAudioSession = NO;
170 }
171#endif
172 if (![_captureSession canSetSessionPreset:kDefaultPreset]) {
173 NSLog(@"Default video capture preset unsupported.");
174 return NO;
175 }
176 _captureSession.sessionPreset = kDefaultPreset;
177
178 // Make the capturer output NV12. Ideally we want I420 but that's not
179 // currently supported on iPhone / iPad.
180 _videoOutput = [[AVCaptureVideoDataOutput alloc] init];
181 _videoOutput.videoSettings = @{
182 (NSString*)kCVPixelBufferPixelFormatTypeKey :
183 @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
184 };
185 _videoOutput.alwaysDiscardsLateVideoFrames = NO;
186 [_videoOutput setSampleBufferDelegate:self
187 queue:dispatch_get_main_queue()];
188 if (![_captureSession canAddOutput:_videoOutput]) {
189 NSLog(@"Default video capture output unsupported.");
190 return NO;
191 }
192 [_captureSession addOutput:_videoOutput];
193
194 // Find the capture devices.
195 AVCaptureDevice* frontCaptureDevice = nil;
196 AVCaptureDevice* backCaptureDevice = nil;
197 for (AVCaptureDevice* captureDevice in
198 [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
199 if (captureDevice.position == AVCaptureDevicePositionBack) {
200 backCaptureDevice = captureDevice;
201 }
202 if (captureDevice.position == AVCaptureDevicePositionFront) {
203 frontCaptureDevice = captureDevice;
204 }
205 }
206 if (!frontCaptureDevice || !backCaptureDevice) {
207 NSLog(@"Failed to get capture devices.");
208 return NO;
209 }
210
211 // Set up the session inputs.
212 NSError* error = nil;
213 _frontDeviceInput =
214 [AVCaptureDeviceInput deviceInputWithDevice:frontCaptureDevice
215 error:&error];
216 if (!_frontDeviceInput) {
217 NSLog(@"Failed to get capture device input: %@",
218 error.localizedDescription);
219 return NO;
220 }
221 _backDeviceInput =
222 [AVCaptureDeviceInput deviceInputWithDevice:backCaptureDevice
223 error:&error];
224 if (!_backDeviceInput) {
225 NSLog(@"Failed to get capture device input: %@",
226 error.localizedDescription);
227 return NO;
228 }
229
230 // Add the inputs.
231 if (![_captureSession canAddInput:_frontDeviceInput] ||
232 ![_captureSession canAddInput:_backDeviceInput]) {
233 NSLog(@"Session does not support capture inputs.");
234 return NO;
235 }
236 [self updateSessionInput];
237
238 return YES;
239}
240
241- (void)deviceOrientationDidChange:(NSNotification*)notification {
242 _orientationHasChanged = YES;
243 [self updateOrientation];
244}
245
246- (void)updateOrientation {
247 AVCaptureConnection* connection =
248 [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
249 if (!connection.supportsVideoOrientation) {
250 // TODO(tkchin): set rotation bit on frames.
251 return;
252 }
253 AVCaptureVideoOrientation orientation = AVCaptureVideoOrientationPortrait;
254 switch ([UIDevice currentDevice].orientation) {
255 case UIDeviceOrientationPortrait:
Zeke Chin7be99bd2015-05-29 16:34:38 -0700256 orientation = AVCaptureVideoOrientationPortrait;
Jon Hjelle14c26952015-05-29 15:24:52 -0700257 break;
Jon Hjellec2cb2662015-05-29 16:38:26 -0700258 case UIDeviceOrientationPortraitUpsideDown:
259 orientation = AVCaptureVideoOrientationPortraitUpsideDown;
260 break;
Zeke Chin57cc74e2015-05-05 07:52:31 -0700261 case UIDeviceOrientationLandscapeLeft:
262 orientation = AVCaptureVideoOrientationLandscapeRight;
263 break;
264 case UIDeviceOrientationLandscapeRight:
265 orientation = AVCaptureVideoOrientationLandscapeLeft;
266 break;
267 case UIDeviceOrientationFaceUp:
268 case UIDeviceOrientationFaceDown:
269 case UIDeviceOrientationUnknown:
270 if (!_orientationHasChanged) {
271 connection.videoOrientation = orientation;
272 }
273 return;
274 }
275 connection.videoOrientation = orientation;
276}
277
278- (void)updateSessionInput {
279 // Update the current session input to match what's stored in _useBackCamera.
280 [_captureSession beginConfiguration];
281 AVCaptureDeviceInput* oldInput = _backDeviceInput;
282 AVCaptureDeviceInput* newInput = _frontDeviceInput;
283 if (_useBackCamera) {
284 oldInput = _frontDeviceInput;
285 newInput = _backDeviceInput;
286 }
287 // Ok to remove this even if it's not attached. Will be no-op.
288 [_captureSession removeInput:oldInput];
289 [_captureSession addInput:newInput];
290 [self updateOrientation];
291 [_captureSession commitConfiguration];
292}
293
294@end
295
296namespace webrtc {
297
298AVFoundationVideoCapturer::AVFoundationVideoCapturer()
magjedb09b6602015-10-01 03:02:44 -0700299 : _capturer(nil), _startThread(nullptr) {
Zeke Chin57cc74e2015-05-05 07:52:31 -0700300 // Set our supported formats. This matches kDefaultPreset.
301 std::vector<cricket::VideoFormat> supportedFormats;
302 supportedFormats.push_back(cricket::VideoFormat(kDefaultFormat));
303 SetSupportedFormats(supportedFormats);
304 _capturer =
305 [[RTCAVFoundationVideoCapturerInternal alloc] initWithCapturer:this];
306}
307
308AVFoundationVideoCapturer::~AVFoundationVideoCapturer() {
309 _capturer = nil;
310}
311
312cricket::CaptureState AVFoundationVideoCapturer::Start(
313 const cricket::VideoFormat& format) {
314 if (!_capturer) {
315 LOG(LS_ERROR) << "Failed to create AVFoundation capturer.";
316 return cricket::CaptureState::CS_FAILED;
317 }
318 if (_capturer.isRunning) {
319 LOG(LS_ERROR) << "The capturer is already running.";
320 return cricket::CaptureState::CS_FAILED;
321 }
322 if (format != kDefaultFormat) {
323 LOG(LS_ERROR) << "Unsupported format provided.";
324 return cricket::CaptureState::CS_FAILED;
325 }
326
327 // Keep track of which thread capture started on. This is the thread that
328 // frames need to be sent to.
henrikg91d6ede2015-09-17 00:24:34 -0700329 RTC_DCHECK(!_startThread);
Zeke Chin57cc74e2015-05-05 07:52:31 -0700330 _startThread = rtc::Thread::Current();
331
332 SetCaptureFormat(&format);
333 // This isn't super accurate because it takes a while for the AVCaptureSession
334 // to spin up, and this call returns async.
335 // TODO(tkchin): make this better.
336 [_capturer startCaptureAsync];
Zeke Chin57cc74e2015-05-05 07:52:31 -0700337 SetCaptureState(cricket::CaptureState::CS_RUNNING);
338
339 return cricket::CaptureState::CS_STARTING;
340}
341
342void AVFoundationVideoCapturer::Stop() {
343 [_capturer stopCaptureAsync];
344 SetCaptureFormat(NULL);
345 _startThread = nullptr;
346}
347
348bool AVFoundationVideoCapturer::IsRunning() {
349 return _capturer.isRunning;
350}
351
352AVCaptureSession* AVFoundationVideoCapturer::GetCaptureSession() {
353 return _capturer.captureSession;
354}
355
356void AVFoundationVideoCapturer::SetUseBackCamera(bool useBackCamera) {
357 _capturer.useBackCamera = useBackCamera;
358}
359
360bool AVFoundationVideoCapturer::GetUseBackCamera() const {
361 return _capturer.useBackCamera;
362}
363
364void AVFoundationVideoCapturer::CaptureSampleBuffer(
365 CMSampleBufferRef sampleBuffer) {
366 if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
367 !CMSampleBufferIsValid(sampleBuffer) ||
368 !CMSampleBufferDataIsReady(sampleBuffer)) {
369 return;
370 }
371
372 CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
373 if (imageBuffer == NULL) {
374 return;
375 }
376
377 // Base address must be unlocked to access frame data.
378 CVOptionFlags lockFlags = kCVPixelBufferLock_ReadOnly;
379 CVReturn ret = CVPixelBufferLockBaseAddress(imageBuffer, lockFlags);
380 if (ret != kCVReturnSuccess) {
381 return;
382 }
383
384 static size_t const kYPlaneIndex = 0;
385 static size_t const kUVPlaneIndex = 1;
386 uint8_t* yPlaneAddress =
387 (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, kYPlaneIndex);
388 size_t yPlaneHeight =
389 CVPixelBufferGetHeightOfPlane(imageBuffer, kYPlaneIndex);
390 size_t yPlaneWidth =
391 CVPixelBufferGetWidthOfPlane(imageBuffer, kYPlaneIndex);
392 size_t yPlaneBytesPerRow =
393 CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, kYPlaneIndex);
394 size_t uvPlaneHeight =
395 CVPixelBufferGetHeightOfPlane(imageBuffer, kUVPlaneIndex);
396 size_t uvPlaneBytesPerRow =
397 CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, kUVPlaneIndex);
398 size_t frameSize =
399 yPlaneBytesPerRow * yPlaneHeight + uvPlaneBytesPerRow * uvPlaneHeight;
400
401 // Sanity check assumption that planar bytes are contiguous.
402 uint8_t* uvPlaneAddress =
403 (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, kUVPlaneIndex);
henrikg91d6ede2015-09-17 00:24:34 -0700404 RTC_DCHECK(
405 uvPlaneAddress == yPlaneAddress + yPlaneHeight * yPlaneBytesPerRow);
Zeke Chin57cc74e2015-05-05 07:52:31 -0700406
407 // Stuff data into a cricket::CapturedFrame.
Peter Boström0c4e06b2015-10-07 12:23:21 +0200408 int64_t currentTime = rtc::TimeNanos();
Zeke Chin57cc74e2015-05-05 07:52:31 -0700409 cricket::CapturedFrame frame;
410 frame.width = yPlaneWidth;
411 frame.height = yPlaneHeight;
412 frame.pixel_width = 1;
413 frame.pixel_height = 1;
Peter Boström0c4e06b2015-10-07 12:23:21 +0200414 frame.fourcc = static_cast<uint32_t>(cricket::FOURCC_NV12);
Zeke Chin57cc74e2015-05-05 07:52:31 -0700415 frame.time_stamp = currentTime;
Zeke Chin57cc74e2015-05-05 07:52:31 -0700416 frame.data = yPlaneAddress;
417 frame.data_size = frameSize;
418
419 if (_startThread->IsCurrent()) {
420 SignalFrameCaptured(this, &frame);
421 } else {
422 _startThread->Invoke<void>(
423 rtc::Bind(&AVFoundationVideoCapturer::SignalFrameCapturedOnStartThread,
424 this, &frame));
425 }
426 CVPixelBufferUnlockBaseAddress(imageBuffer, lockFlags);
427}
428
429void AVFoundationVideoCapturer::SignalFrameCapturedOnStartThread(
430 const cricket::CapturedFrame* frame) {
henrikg91d6ede2015-09-17 00:24:34 -0700431 RTC_DCHECK(_startThread->IsCurrent());
Zeke Chin57cc74e2015-05-05 07:52:31 -0700432 // This will call a superclass method that will perform the frame conversion
433 // to I420.
434 SignalFrameCaptured(this, frame);
435}
436
437} // namespace webrtc