diff --git a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m index ee2b8ab369..bd0c1dc0f1 100644 --- a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m +++ b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m @@ -628,13 +628,19 @@ - (void)getUserVideo:(NSDictionary*)constraints [videoDevice unlockForConfiguration]; } + __weak FlutterWebRTCPlugin* weakSelf = self; [self.videoCapturer startCaptureWithDevice:videoDevice format:selectedFormat fps:selectedFps completionHandler:^(NSError* error) { if (error) { NSLog(@"Start capture error: %@", [error localizedDescription]); + return; } + + // This is a freshly created capture session, so it + // starts without multitasking camera access. + [weakSelf applyMultitaskingCameraAccessToCaptureSession]; }]; NSString* trackUUID = [[NSUUID UUID] UUIDString]; diff --git a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m index cb54ab10c6..872ac375e8 100644 --- a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m +++ b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m @@ -1149,6 +1149,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL enable = [argsMap[@"enable"] boolValue]; [self enableMultitaskingCameraAccess:enable result:result]; + } else if ([@"isIOSMultitaskingCameraAccessSupported" isEqualToString:call.method]) { + [self multitaskingCameraAccessSupported:result]; } else if ([@"mediaStreamTrackHasTorch" isEqualToString:call.method]) { NSDictionary* argsMap = call.arguments; NSString* trackId = argsMap[@"trackId"]; @@ -2416,8 +2418,61 @@ - (void)mediaStreamTrackSetVideoEffects:(nonnull NSString*)trackId } } +/// Applies the remembered multitasking camera access request to the capture +/// session in use. +/// +/// A capture session is created per `getUserMedia` call and starts without +/// multitasking camera access, so the request is applied again every time one +/// is created. +- (void)applyMultitaskingCameraAccessToCaptureSession { +#if !TARGET_OS_OSX + if (!self.multitaskingCameraAccessRequested) { + return; + } + + if (@available(iOS 16.0, *)) { + AVCaptureSession* session = self.videoCapturer.captureSession; + if (session == nil || !session.isMultitaskingCameraAccessSupported || + session.multitaskingCameraAccessEnabled) { + return; + } + + @try { + [session beginConfiguration]; + [session setMultitaskingCameraAccessEnabled:YES]; + [session commitConfiguration]; + } @catch (NSException* exception) { + NSLog(@"applyMultitaskingCameraAccessToCaptureSession: Exception occurred: %@ - %@", + exception.name, exception.reason); + } + } +#endif +} + +/// Reports whether the capture session in use supports camera access while +/// multitasking, or nil when there is no capture session to ask. +- (void)multitaskingCameraAccessSupported:(FlutterResult)result { +#if TARGET_OS_OSX + result(@NO); +#else + if (@available(iOS 16.0, *)) { + AVCaptureSession* session = self.videoCapturer.captureSession; + if (session == nil) { + result(nil); + return; + } + + result(@(session.isMultitaskingCameraAccessSupported)); + } else { + result(@NO); + } +#endif +} + - (void)enableMultitaskingCameraAccess:(BOOL)enable result:(FlutterResult)result { @try { + self.multitaskingCameraAccessRequested = enable; + AVCaptureSession* session = self.videoCapturer.captureSession; if (session == nil) { NSLog(@"enableMultitaskingCameraAccess: Capture session is nil."); diff --git a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h index 3def227d90..d1363208cf 100644 --- a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h +++ b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h @@ -90,6 +90,10 @@ typedef void (^CapturerStopHandler)(CompletionHandler _Nonnull handler); @property(nonatomic, strong) NSMutableDictionary* _Nonnull pausedTrackVolumes; @property(nonatomic) BOOL isAudioPlayoutPaused; +/// Whether camera access while multitasking was asked for. Remembered so that +/// a capture session created afterwards is started with it applied. +@property(nonatomic) BOOL multitaskingCameraAccessRequested; + - (void)mediaStreamTrackSetVideoEffects:(nonnull NSString*)trackId names:(nonnull NSArray*)names; - (RTCMediaStream* _Nullable)streamForId:(NSString* _Nonnull)streamId @@ -108,6 +112,7 @@ typedef void (^CapturerStopHandler)(CompletionHandler _Nonnull handler); - (RTCMediaStreamTrack* _Nullable)remoteTrackForId:(NSString* _Nonnull)trackId; - (BOOL)hasLocalAudioTrack; +- (void)applyMultitaskingCameraAccessToCaptureSession; - (void)ensureAudioSession; - (void)deactiveRtcAudioSession; diff --git a/lib/src/helper.dart b/lib/src/helper.dart index c0d35f1dff..d5980deca3 100644 --- a/lib/src/helper.dart +++ b/lib/src/helper.dart @@ -224,6 +224,14 @@ class Helper { static Future enableIOSMultitaskingCameraAccess(bool enable) => IosAudioManagement.enableMultitaskingCameraAccess(enable); + /// Whether the capture session in use supports camera access while + /// multitasking (iOS only). + /// + /// Returns `null` when there is no capture session to ask, which is the case + /// until the camera is started. + static Future isIOSMultitaskingCameraAccessSupported() => + IosAudioManagement.isMultitaskingCameraAccessSupported(); + /// Trigger the iOS audio route selection UI (iOS only). static Future triggeriOSAudioRouteSelectionUI() => IosAudioManagement.triggerAudioRouteSelectionUI(); diff --git a/lib/src/native/ios/audio_management.dart b/lib/src/native/ios/audio_management.dart index 3d24748def..13a5f578a0 100644 --- a/lib/src/native/ios/audio_management.dart +++ b/lib/src/native/ios/audio_management.dart @@ -84,4 +84,20 @@ class IosAudioManagement { 'enableMultitaskingCameraAccess is only supported for iOS'); } } + + /// Whether the capture session in use supports camera access while + /// multitasking (iOS only). + /// + /// Returns `null` when there is no capture session to ask, which is the case + /// until the camera is started. + static Future isMultitaskingCameraAccessSupported() async { + if (WebRTC.platformIsIOS) { + return await WebRTC.invokeMethod( + 'isIOSMultitaskingCameraAccessSupported', + ); + } else { + throw Exception( + 'isMultitaskingCameraAccessSupported is only supported for iOS'); + } + } } diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m index e7fec46cbb..e848ec43ba 100644 --- a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m @@ -975,6 +975,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL enable = [argsMap[@"enable"] boolValue]; [self enableMultitaskingCameraAccess:enable result:result]; + } else if ([@"isIOSMultitaskingCameraAccessSupported" isEqualToString:call.method]) { + [self multitaskingCameraAccessSupported:result]; } else if ([@"mediaStreamTrackHasTorch" isEqualToString:call.method]) { NSDictionary* argsMap = call.arguments; NSString* trackId = argsMap[@"trackId"]; @@ -2151,6 +2153,10 @@ - (void)enableMultitaskingCameraAccess:(BOOL)enable result:(FlutterResult)result result(@NO); } +- (void)multitaskingCameraAccessSupported:(FlutterResult)result { + result(@NO); +} + - (void)mediaStreamGetTracks:(NSString*)streamId result:(FlutterResult)result { RTCMediaStream* stream = [self streamForId:streamId peerConnectionId:@""]; if (stream) {