99 */
1010
1111#import < Foundation/Foundation.h>
12+ #import < os/lock.h>
1213
1314#import " RTCCameraVideoCapturer.h"
1415#import " base/RTCLogging.h"
1516#import " base/RTCVideoFrameBuffer.h"
1617#import " components/video_frame_buffer/RTCCVPixelBuffer.h"
1718
18- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST
19+ // AVCaptureMultiCamSession iOS 13.0+, iPadOS 13.0+, Mac Catalyst 14.0+, tvOS 17.0+
20+ #define TARGET_MULTICAM_CAPABLE (TARGET_OS_IPHONE && !TARGET_OS_VISION)
21+
22+ // iOS 2.0+, iPadOS 2.0+, Mac Catalyst 13.0+
23+ #define TARGET_WATCH_DEVICE_ROTATION \
24+ (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION)
25+
26+ #if TARGET_WATCH_DEVICE_ROTATION
1927#import " helpers/UIDevice+RTCDevice.h"
2028#endif
2129
@@ -41,12 +49,24 @@ @implementation RTC_OBJC_TYPE (RTCCameraVideoCapturer) {
4149 FourCharCode _preferredOutputPixelFormat;
4250 FourCharCode _outputPixelFormat;
4351 RTCVideoRotation _rotation;
44- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST
52+
53+ #if TARGET_WATCH_DEVICE_ROTATION
4554 UIInterfaceOrientation _orientation;
4655 BOOL _generatingOrientationNotifications;
4756#endif
57+
58+ #if TARGET_MULTICAM_CAPABLE
59+ AVCaptureConnection *_captureConnection;
60+ #endif
4861}
4962
63+ #if TARGET_MULTICAM_CAPABLE
64+ // Shared multi-camera session across capturers.
65+ static AVCaptureMultiCamSession *_sharedMultiCamSession = nil ;
66+ static os_unfair_lock _sharedMultiCamSessionLock = OS_UNFAIR_LOCK_INIT;
67+ static NSUInteger _sharedMultiCamSessionCount = 0 ;
68+ #endif
69+
5070@synthesize frameQueue = _frameQueue;
5171@synthesize captureSession = _captureSession;
5272@synthesize currentDevice = _currentDevice;
@@ -55,14 +75,13 @@ @implementation RTC_OBJC_TYPE (RTCCameraVideoCapturer) {
5575@synthesize willBeRunning = _willBeRunning;
5676
5777- (instancetype )init {
58- return [self initWithDelegate: nil captureSession: [[AVCaptureSession alloc ] init ]];
78+ return [self initWithDelegate: nil captureSession: [self createCaptureSession ]];
5979}
6080
6181- (instancetype )initWithDelegate : (__weak id <RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate {
62- return [self initWithDelegate: delegate captureSession: [[AVCaptureSession alloc ] init ]];
82+ return [self initWithDelegate: delegate captureSession: [self createCaptureSession ]];
6383}
6484
65- // This initializer is used for testing.
6685- (instancetype )initWithDelegate : (__weak id <RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate
6786 captureSession : (AVCaptureSession *)captureSession {
6887 if (self = [super initWithDelegate: delegate]) {
@@ -73,8 +92,10 @@ - (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelega
7392 if (![self setupCaptureSession: captureSession]) {
7493 return nil ;
7594 }
95+
7696 NSNotificationCenter *center = [NSNotificationCenter defaultCenter ];
77- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
97+
98+ #if TARGET_WATCH_DEVICE_ROTATION
7899 _orientation = UIInterfaceOrientationPortrait;
79100 _rotation = RTCVideoRotation_90;
80101 [center addObserver: self
@@ -94,6 +115,7 @@ - (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelega
94115 name: UIApplicationDidBecomeActiveNotification
95116 object: [UIApplication sharedApplication ]];
96117#endif
118+
97119 [center addObserver: self
98120 selector: @selector (handleCaptureSessionRuntimeError: )
99121 name: AVCaptureSessionRuntimeErrorNotification
@@ -107,6 +129,7 @@ - (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelega
107129 name: AVCaptureSessionDidStopRunningNotification
108130 object: _captureSession];
109131 }
132+
110133 return self;
111134}
112135
@@ -119,8 +142,8 @@ - (void)dealloc {
119142
120143+ (NSArray <AVCaptureDevice *> *)captureDevices {
121144#if TARGET_OS_VISION
122- // Simply return an empty array.
123- return [ NSArray array ];
145+ AVCaptureDevice *device = AVCaptureDevice. systemPreferredCamera ;
146+ return device ? @[ device ] : @[ ];
124147#else
125148 AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession
126149 discoverySessionWithDeviceTypes: @[ AVCaptureDeviceTypeBuiltInWideAngleCamera ]
@@ -182,7 +205,7 @@ - (void)startCaptureWithDevice:(AVCaptureDevice *)device
182205 block: ^{
183206 RTCLogInfo (" startCaptureWithDevice %@ @ %ld fps" , format, (long )fps);
184207
185- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
208+ #if TARGET_WATCH_DEVICE_ROTATION
186209 dispatch_async (dispatch_get_main_queue (), ^{
187210 if (!self->_generatingOrientationNotifications ) {
188211 [[UIDevice currentDevice ] beginGeneratingDeviceOrientationNotifications ];
@@ -211,8 +234,8 @@ - (void)startCaptureWithDevice:(AVCaptureDevice *)device
211234 [self updateZoomFactor ];
212235 [self .currentDevice unlockForConfiguration ];
213236
214- [self .captureSession startRunning ];
215- self. isRunning = YES ;
237+ [self startRunning ];
238+
216239 if (completionHandler) {
217240 completionHandler (nil );
218241 }
@@ -225,21 +248,31 @@ - (void)stopCaptureWithCompletionHandler:(nullable void (^)(void))completionHand
225248 dispatchAsyncOnType: RTCDispatcherTypeCaptureSession
226249 block: ^{
227250 RTCLogInfo (" Stop" );
228- self.currentDevice = nil ;
251+
252+ #if TARGET_MULTICAM_CAPABLE
253+ [self .captureSession removeConnection: self ->_captureConnection];
254+ self->_captureConnection = nil ;
255+ #endif
256+
229257 for (AVCaptureDeviceInput *oldInput in [self .captureSession.inputs copy ]) {
230- [self .captureSession removeInput: oldInput];
258+ // Remove any old input with same device.
259+ if ([self ->_currentDevice isEqual: oldInput.device]) {
260+ [self .captureSession removeInput: oldInput];
261+ }
231262 }
232- [self .captureSession stopRunning ];
263+ self.currentDevice = nil ;
264+
265+ [self stopRunning ];
233266
234- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
267+ #if TARGET_WATCH_DEVICE_ROTATION
235268 dispatch_async (dispatch_get_main_queue (), ^{
236269 if (self->_generatingOrientationNotifications ) {
237270 [[UIDevice currentDevice ] endGeneratingDeviceOrientationNotifications ];
238271 self->_generatingOrientationNotifications = NO ;
239272 }
240273 });
241274#endif
242- self. isRunning = NO ;
275+
243276 if (completionHandler) {
244277 completionHandler ();
245278 }
@@ -248,7 +281,7 @@ - (void)stopCaptureWithCompletionHandler:(nullable void (^)(void))completionHand
248281
249282#pragma mark iOS notifications
250283
251- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
284+ #if TARGET_WATCH_DEVICE_ROTATION
252285- (void )deviceOrientationDidChange:(NSNotification *)notification {
253286 [self updateOrientation ];
254287}
@@ -271,7 +304,7 @@ - (void)captureOutput:(AVCaptureOutput *)captureOutput
271304 return ;
272305 }
273306
274- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
307+ #if TARGET_WATCH_DEVICE_ROTATION
275308 // Default to portrait orientation on iPhone.
276309 BOOL usingFrontCamera = NO ;
277310 // Check the image's EXIF for the camera the image came from as the image could have been
@@ -433,10 +466,70 @@ - (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
433466 }];
434467}
435468
436- #endif // TARGET_OS_IPHONE
469+ #endif
437470
438471#pragma mark - Private
439472
473+ - (AVCaptureSession *)createCaptureSession {
474+ #if TARGET_MULTICAM_CAPABLE
475+ if (AVCaptureMultiCamSession.isMultiCamSupported ) {
476+ // AVCaptureMultiCamSession exists and device supports multi-cam.
477+ if (_sharedMultiCamSession == nil ) {
478+ _sharedMultiCamSession = [[AVCaptureMultiCamSession alloc ] init ];
479+ }
480+ return _sharedMultiCamSession;
481+ } else {
482+ // AVCaptureMultiCamSession exists but device doesn't support multi-cam.
483+ return [[AVCaptureSession alloc ] init ];
484+ }
485+ #else
486+ // AVCaptureMultiCamSession doesn't exist with this platform, use AVCaptureSession.
487+ return [[AVCaptureSession alloc ] init ];
488+ #endif
489+ }
490+
491+ - (BOOL )isUsingSelfCreatedMultiCamSession {
492+ #if TARGET_MULTICAM_CAPABLE
493+ return _sharedMultiCamSession != nil && _sharedMultiCamSession == _captureSession;
494+ #else
495+ return NO ;
496+ #endif
497+ }
498+
499+ - (void )startRunning {
500+ BOOL shouldStartRunning = YES ;
501+ #if TARGET_MULTICAM_CAPABLE
502+ if ([self isUsingSelfCreatedMultiCamSession ]) {
503+ os_unfair_lock_lock (&_sharedMultiCamSessionLock);
504+ shouldStartRunning = _sharedMultiCamSessionCount == 0 ;
505+ _sharedMultiCamSessionCount += 1 ;
506+ os_unfair_lock_unlock (&_sharedMultiCamSessionLock);
507+ }
508+ #endif
509+ if (shouldStartRunning) {
510+ [_captureSession startRunning ];
511+ }
512+ self.isRunning = YES ;
513+ }
514+
515+ - (void )stopRunning {
516+ BOOL shouldStopRunning = YES ;
517+ #if TARGET_MULTICAM_CAPABLE
518+ if ([self isUsingSelfCreatedMultiCamSession ]) {
519+ os_unfair_lock_lock (&_sharedMultiCamSessionLock);
520+ if (_sharedMultiCamSessionCount > 0 ) {
521+ _sharedMultiCamSessionCount -= 1 ;
522+ shouldStopRunning = _sharedMultiCamSessionCount == 0 ;
523+ }
524+ os_unfair_lock_unlock (&_sharedMultiCamSessionLock);
525+ }
526+ #endif
527+ if (shouldStopRunning) {
528+ [_captureSession stopRunning ];
529+ }
530+ self.isRunning = NO ;
531+ }
532+
440533- (dispatch_queue_t )frameQueue {
441534 if (!_frameQueue) {
442535 _frameQueue = RTCDispatchQueueCreateWithTarget (
@@ -459,7 +552,12 @@ - (BOOL)setupCaptureSession:(AVCaptureSession *)captureSession {
459552 RTCLogError (@" Video data output unsupported." );
460553 return NO ;
461554 }
555+
556+ #if TARGET_MULTICAM_CAPABLE
557+ [_captureSession addOutputWithNoConnections: _videoDataOutput];
558+ #else
462559 [_captureSession addOutput: _videoDataOutput];
560+ #endif
463561
464562 return YES ;
465563}
@@ -544,22 +642,40 @@ - (void)reconfigureCaptureSessionInput {
544642 }
545643 [_captureSession beginConfiguration ];
546644 for (AVCaptureDeviceInput *oldInput in [_captureSession.inputs copy ]) {
547- [_captureSession removeInput: oldInput];
645+ // Remove any old input with same device.
646+ if ([_currentDevice isEqual: oldInput.device]) {
647+ [_captureSession removeInput: oldInput];
648+ }
548649 }
650+
549651 if ([_captureSession canAddInput: input]) {
652+ #if TARGET_MULTICAM_CAPABLE
653+ [_captureSession addInputWithNoConnections: input];
654+
655+ AVCaptureInputPort *videoPort = input.ports .firstObject ;
656+ _captureConnection = [AVCaptureConnection connectionWithInputPorts: @[ videoPort ]
657+ output: _videoDataOutput];
658+
659+ [_captureSession addConnection: _captureConnection];
660+ #else
550661 [_captureSession addInput: input];
662+ #endif
551663 } else {
552664 RTCLogError (@" Cannot add camera as an input to the session." );
553665 }
666+
554667 [_captureSession commitConfiguration ];
555668}
556669
557- #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST && !TARGET_OS_VISION
670+ #if TARGET_WATCH_DEVICE_ROTATION
558671- (void )updateOrientation {
559672 NSAssert ([RTC_OBJC_TYPE (RTCDispatcher) isOnQueueForType: RTCDispatcherTypeMain],
560- @" statusBarOrientation must be called on the main queue." );
561- // statusBarOrientation must be called on the main queue
562- UIInterfaceOrientation newOrientation = [UIApplication sharedApplication ].statusBarOrientation ;
673+ @" Retrieving device orientation must be called on the main queue." );
674+
675+ // Must be called on the main queue.
676+ UIWindowScene *windowScene =
677+ (UIWindowScene *)[UIApplication sharedApplication ].connectedScenes .anyObject ;
678+ UIInterfaceOrientation newOrientation = windowScene.interfaceOrientation ;
563679
564680 [RTC_OBJC_TYPE (RTCDispatcher) dispatchAsyncOnType: RTCDispatcherTypeCaptureSession
565681 block: ^{
0 commit comments