From e8e213ed7b3b08c662152ace3630738aa75aca20 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Mon, 16 May 2022 14:45:09 -0700 Subject: [PATCH 1/5] [camera]request access permission for audio --- packages/camera/camera/CHANGELOG.md | 4 + ...eraCaptureSessionQueueRaceConditionTests.m | 11 +- .../RunnerTests/CameraMethodChannelTests.m | 2 +- .../ios/RunnerTests/CameraPermissionTests.m | 118 +++++++++++++++++- .../ios/Classes/CameraPermissionUtils.h | 7 +- .../ios/Classes/CameraPermissionUtils.m | 83 ++++++++---- .../camera/camera/ios/Classes/CameraPlugin.m | 21 +++- .../camera/ios/Classes/CameraPlugin_Test.h | 6 +- packages/camera/camera/pubspec.yaml | 2 +- 9 files changed, 209 insertions(+), 45 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d101f60cf041..9af23011ec50 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.6 + +* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result. + ## 0.9.5+1 * Suppresses warnings for pre-iOS-11 codepaths. diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m index e99ce4e89a94..8344b72ee605 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -29,11 +29,12 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { result:^(id _Nullable result) { [disposeExpectation fulfill]; }]; - [camera createCameraOnSessionQueueWithCreateMethodCall:createCall - result:[[FLTThreadSafeFlutterResult alloc] - initWithResult:^(id _Nullable result) { - [createExpectation fulfill]; - }]]; + [camera createCameraOnCaptureSessionQueueWithCreateMethodCall:createCall + result:[[FLTThreadSafeFlutterResult alloc] + initWithResult:^( + id _Nullable result) { + [createExpectation fulfill]; + }]]; [self waitForExpectationsWithTimeout:1 handler:nil]; // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 62b9cda2ef7b..dbfbdb42d501 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -36,7 +36,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject]; + [camera createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:resultObject]; [self waitForExpectationsWithTimeout:1 handler:nil]; // Verify the result diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m index 961b931b7704..ceef034f9503 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m @@ -15,6 +15,8 @@ @interface CameraPermissionTests : XCTestCase @implementation CameraPermissionTests +#pragma mark - camera permissions + - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { XCTestExpectation *expectation = [self expectationWithDescription: @@ -24,7 +26,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusAuthorized); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } @@ -44,7 +46,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { id mockDevice = OCMClassMock([AVCaptureDevice class]); OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusDenied); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -63,7 +65,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusRestricted); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -85,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess { return YES; }]]); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } @@ -111,7 +113,113 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess { block(NO); return YES; }]]); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - audio permissions + +- (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must copmlete without error if audio access was previously authorized."]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusAuthorized); + + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + if (error == nil) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} +- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must complete with error if audio access was previously denied."]; + FlutterError *expectedError = + [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" + message:@"User has previously denied the audio access request. Go to " + @"Settings to enable audio access." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusDenied); + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithErrorIfRestricted { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if audio access is restricted."]; + FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessRestricted" + message:@"Audio access is restricted. " + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusRestricted); + + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess { + XCTestExpectation *grantedExpectation = [self + expectationWithDescription:@"Must complete without error if user choose to grant access"]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusNotDetermined); + // Mimic user choosing "allow" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(YES); + return YES; + }]]); + + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + if (error == nil) { + [grantedExpectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if user choose to deny access"]; + FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessDenied" + message:@"User denied the audio access request." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusNotDetermined); + + // Mimic user choosing "deny" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(NO); + return YES; + }]]); + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h index 80f55db7be32..9a60e94732e1 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h @@ -13,8 +13,11 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); /// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the /// user will have to update the choice in Settings app. /// +/// +/// @param forAudio Requests for `AVMediaTypeAudio` permission if `forAudio` is true, and +/// `AVMediaTypeVideo` permission otherwise. /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestCameraPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler); +extern void FLTRequestCameraPermission(BOOL forAudio, + FLTCameraPermissionRequestCompletionHandler handler); diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m index 6318338ea6a2..313a62fbed0c 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m @@ -5,34 +5,73 @@ @import AVFoundation; #import "CameraPermissionUtils.h" -void FLTRequestCameraPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler) { - switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { +void FLTRequestCameraPermission(BOOL forAudio, + FLTCameraPermissionRequestCompletionHandler handler) { + AVMediaType mediaType; + if (forAudio) { + mediaType = AVMediaTypeAudio; + } else { + mediaType = AVMediaTypeVideo; + } + + switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) { case AVAuthorizationStatusAuthorized: handler(nil); break; - case AVAuthorizationStatusDenied: - handler([FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" - message:@"User has previously denied the camera access request. " - @"Go to Settings to enable camera access." - details:nil]); + case AVAuthorizationStatusDenied: { + FlutterError *flutterError; + if (forAudio) { + flutterError = + [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" + message:@"User has previously denied the audio access request. " + @"Go to Settings to enable audio access." + details:nil]; + } else { + flutterError = + [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" + message:@"User has previously denied the camera access request. " + @"Go to Settings to enable camera access." + details:nil]; + } + handler(flutterError); break; - case AVAuthorizationStatusRestricted: - handler([FlutterError errorWithCode:@"CameraAccessRestricted" - message:@"Camera access is restricted. " - details:nil]); + } + case AVAuthorizationStatusRestricted: { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted" + message:@"Audio access is restricted. " + details:nil]; + } else { + flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted" + message:@"Camera access is restricted. " + details:nil]; + } + handler(flutterError); break; + } case AVAuthorizationStatusNotDetermined: { - [AVCaptureDevice - requestAccessForMediaType:AVMediaTypeVideo - completionHandler:^(BOOL granted) { - // handler can be invoked on an arbitrary dispatch queue. - handler(granted ? nil - : [FlutterError - errorWithCode:@"CameraAccessDenied" - message:@"User denied the camera access request." - details:nil]); - }]; + [AVCaptureDevice requestAccessForMediaType:mediaType + completionHandler:^(BOOL granted) { + // handler can be invoked on an arbitrary dispatch queue. + if (granted) { + handler(nil); + } else { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError + errorWithCode:@"AudioAccessDenied" + message:@"User denied the audio access request." + details:nil]; + } else { + flutterError = [FlutterError + errorWithCode:@"CameraAccessDenied" + message:@"User denied the camera access request." + details:nil]; + } + handler(flutterError); + } + }]; break; } } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 43d541e411b4..51ee38a104b2 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -132,12 +132,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { // Create FLTCam only if granted camera access. if (error) { [result sendFlutterError:error]; } else { - [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + [self createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:result]; } }); } else if ([@"startImageStream" isEqualToString:call.method]) { @@ -194,8 +194,17 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera close]; [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { - [_camera setUpCaptureSessionForAudio]; - [result sendSuccess]; + FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + // Setup audio capture session only if granted audio access + if (error) { + [result sendFlutterError:error]; + } else { + dispatch_async(self.captureSessionQueue, ^{ + [self.camera setUpCaptureSessionForAudio]; + [result sendSuccess]; + }); + } + }); } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -258,8 +267,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } } -- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall - result:(FLTThreadSafeFlutterResult *)result { +- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result { dispatch_async(self.captureSessionQueue, ^{ NSString *cameraName = createMethodCall.arguments[@"cameraName"]; NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"]; diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index d1903e0829b4..6696a9ce171c 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -38,10 +38,10 @@ /// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; -/// Creates FLTCam on session queue and reports the creation result. +/// Creates FLTCam on capture session queue and reports the creation result. /// @param createMethodCall the create method call /// @param result a thread safe flutter result wrapper object to report creation result. -- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall - result:(FLTThreadSafeFlutterResult *)result; +- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result; @end diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 14acf32e2324..593e7b5bb978 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.5+1 +version: 0.9.6 environment: sdk: ">=2.14.0 <3.0.0" From 141025c1b8aeb94122d2eafc1568d99539f31e63 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Mon, 16 May 2022 15:00:22 -0700 Subject: [PATCH 2/5] updates readme and example to add audio exception codes --- packages/camera/camera/README.md | 8 +++++++- .../CameraCaptureSessionQueueRaceConditionTests.m | 11 +++++------ .../ios/RunnerTests/CameraMethodChannelTests.m | 2 +- packages/camera/camera/example/lib/main.dart | 11 +++++++++++ .../camera/camera/ios/Classes/CameraPermissionUtils.h | 1 - packages/camera/camera/ios/Classes/CameraPlugin.m | 10 ++++++---- .../camera/camera/ios/Classes/CameraPlugin_Test.h | 6 +++--- 7 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 6b2ed7a6b687..ec9d7379c60b 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -88,10 +88,16 @@ Here is a list of all permission error codes that can be thrown: - `CameraAccessDenied`: Thrown when user denies the camera access permission. -- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy in order to enable camera access. +- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Camera in order to enable camera access. - `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control). +- `AudioAccessDenied`: Thrown when user denies the audio access permission. + +- `AudioAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Microphone in order to enable audio access. + +- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control). + - `cameraPermission`: Android and Web only. A legacy error code for all kinds of camera permission errors. ### Example diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m index 8344b72ee605..e99ce4e89a94 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -29,12 +29,11 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { result:^(id _Nullable result) { [disposeExpectation fulfill]; }]; - [camera createCameraOnCaptureSessionQueueWithCreateMethodCall:createCall - result:[[FLTThreadSafeFlutterResult alloc] - initWithResult:^( - id _Nullable result) { - [createExpectation fulfill]; - }]]; + [camera createCameraOnSessionQueueWithCreateMethodCall:createCall + result:[[FLTThreadSafeFlutterResult alloc] + initWithResult:^(id _Nullable result) { + [createExpectation fulfill]; + }]]; [self waitForExpectationsWithTimeout:1 handler:nil]; // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index dbfbdb42d501..62b9cda2ef7b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -36,7 +36,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [camera createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:resultObject]; + [camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject]; [self waitForExpectationsWithTimeout:1 handler:nil]; // Verify the result diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 34942ba5aa77..c0181a5d36a1 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -697,6 +697,17 @@ class _CameraExampleHomeState extends State // iOS only showInSnackBar('Camera access is restricted.'); break; + case 'AudioAccessDenied': + showInSnackBar('You have denied audio access.'); + break; + case 'AudioAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable audio access.'); + break; + case 'AudioAccessRestricted': + // iOS only + showInSnackBar('Audio access is restricted.'); + break; case 'cameraPermission': // Android & web only showInSnackBar('Unknown permission error.'); diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h index 9a60e94732e1..47a42da382f5 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h @@ -13,7 +13,6 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); /// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the /// user will have to update the choice in Settings app. /// -/// /// @param forAudio Requests for `AVMediaTypeAudio` permission if `forAudio` is true, and /// `AVMediaTypeVideo` permission otherwise. /// @param handler if access permission is (or was previously) granted, completion handler will be diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 51ee38a104b2..a3cd54566206 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -137,7 +137,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call if (error) { [result sendFlutterError:error]; } else { - [self createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:result]; + [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; } }); } else if ([@"startImageStream" isEqualToString:call.method]) { @@ -194,11 +194,13 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera close]; [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { + // Setup audio capture session only if granted audio access. FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { - // Setup audio capture session only if granted audio access if (error) { [result sendFlutterError:error]; } else { + // Permission completion handler may be called on arbitrary queue. + // Dispatch to `captureSessionQueue` to setup audio capture session. dispatch_async(self.captureSessionQueue, ^{ [self.camera setUpCaptureSessionForAudio]; [result sendSuccess]; @@ -267,8 +269,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } } -- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall - result:(FLTThreadSafeFlutterResult *)result { +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result { dispatch_async(self.captureSessionQueue, ^{ NSString *cameraName = createMethodCall.arguments[@"cameraName"]; NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"]; diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index 6696a9ce171c..d1903e0829b4 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -38,10 +38,10 @@ /// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; -/// Creates FLTCam on capture session queue and reports the creation result. +/// Creates FLTCam on session queue and reports the creation result. /// @param createMethodCall the create method call /// @param result a thread safe flutter result wrapper object to report creation result. -- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall - result:(FLTThreadSafeFlutterResult *)result; +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result; @end From 67f94355edcf71c1893aede61d314330970d4603 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Mon, 16 May 2022 17:46:39 -0700 Subject: [PATCH 3/5] split audio and video request functions --- .../ios/RunnerTests/CameraPermissionTests.m | 20 +++++++++---------- .../ios/Classes/CameraPermissionUtils.h | 16 +++++++++++---- .../ios/Classes/CameraPermissionUtils.m | 11 ++++++++-- .../camera/camera/ios/Classes/CameraPlugin.m | 4 ++-- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m index ceef034f9503..e0ed5a533715 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m @@ -26,7 +26,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusAuthorized); - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } @@ -46,7 +46,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { id mockDevice = OCMClassMock([AVCaptureDevice class]); OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusDenied); - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -65,7 +65,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusRestricted); - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -87,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess { return YES; }]]); - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } @@ -113,7 +113,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess { block(NO); return YES; }]]); - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -133,7 +133,7 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusAuthorized); - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } @@ -153,7 +153,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { id mockDevice = OCMClassMock([AVCaptureDevice class]); OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusDenied); - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -172,7 +172,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfRestricted { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusRestricted); - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -194,7 +194,7 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess { return YES; }]]); - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } @@ -219,7 +219,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess { block(NO); return YES; }]]); - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h index 47a42da382f5..a41766e52a8c 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h @@ -13,10 +13,18 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); /// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the /// user will have to update the choice in Settings app. /// -/// @param forAudio Requests for `AVMediaTypeAudio` permission if `forAudio` is true, and -/// `AVMediaTypeVideo` permission otherwise. /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestCameraPermission(BOOL forAudio, - FLTCameraPermissionRequestCompletionHandler handler); +extern void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler); + +/// Requests audio access permission. +/// +/// If it is the first time requesting audio access, a permission dialog will show up on the +/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the +/// user will have to update the choice in Settings app. +/// +/// @param handler if access permission is (or was previously) granted, completion handler will be +/// called without error; Otherwise completion handler will be called with error. Handler can be +/// called on an arbitrary dispatch queue. +extern void FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler); diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m index 313a62fbed0c..45d3456d5063 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m @@ -5,8 +5,7 @@ @import AVFoundation; #import "CameraPermissionUtils.h" -void FLTRequestCameraPermission(BOOL forAudio, - FLTCameraPermissionRequestCompletionHandler handler) { +void RequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) { AVMediaType mediaType; if (forAudio) { mediaType = AVMediaTypeAudio; @@ -76,3 +75,11 @@ void FLTRequestCameraPermission(BOOL forAudio, } } } + +void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler) { + RequestPermission(/*forAudio*/ NO, handler); +} + +void FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler) { + RequestPermission(/*forAudio*/ YES, handler); +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index a3cd54566206..d20eb50811f5 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -132,7 +132,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { - FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) { + FLTRequestCameraPermission(^(FlutterError *error) { // Create FLTCam only if granted camera access. if (error) { [result sendFlutterError:error]; @@ -195,7 +195,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { // Setup audio capture session only if granted audio access. - FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) { + FLTRequestAudioPermission(^(FlutterError *error) { if (error) { [result sendFlutterError:error]; } else { From cb3ecc6e477e2a4e8372dbdb7a13e11087785846 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Mon, 16 May 2022 20:09:34 -0700 Subject: [PATCH 4/5] add back WithCompletionHandler --- .../ios/RunnerTests/CameraPermissionTests.m | 20 +++++++++---------- .../ios/Classes/CameraPermissionUtils.h | 6 ++++-- .../ios/Classes/CameraPermissionUtils.m | 12 ++++++----- .../camera/camera/ios/Classes/CameraPlugin.m | 4 ++-- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m index e0ed5a533715..541e0288254c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m @@ -26,7 +26,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusAuthorized); - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } @@ -46,7 +46,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { id mockDevice = OCMClassMock([AVCaptureDevice class]); OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusDenied); - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -65,7 +65,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) .andReturn(AVAuthorizationStatusRestricted); - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -87,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess { return YES; }]]); - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } @@ -113,7 +113,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess { block(NO); return YES; }]]); - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -133,7 +133,7 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusAuthorized); - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } @@ -153,7 +153,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { id mockDevice = OCMClassMock([AVCaptureDevice class]); OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusDenied); - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -172,7 +172,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfRestricted { OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) .andReturn(AVAuthorizationStatusRestricted); - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } @@ -194,7 +194,7 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess { return YES; }]]); - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } @@ -219,7 +219,7 @@ - (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess { block(NO); return YES; }]]); - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h index a41766e52a8c..5cbbab055f34 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h @@ -16,7 +16,8 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler); +extern void FLTRequestCameraPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler); /// Requests audio access permission. /// @@ -27,4 +28,5 @@ extern void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandl /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler); +extern void FLTRequestAudioPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler); diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m index 45d3456d5063..098265a6b74d 100644 --- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m @@ -5,7 +5,7 @@ @import AVFoundation; #import "CameraPermissionUtils.h" -void RequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) { +void FLTRequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) { AVMediaType mediaType; if (forAudio) { mediaType = AVMediaTypeAudio; @@ -76,10 +76,12 @@ void RequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandle } } -void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler) { - RequestPermission(/*forAudio*/ NO, handler); +void FLTRequestCameraPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler) { + FLTRequestPermission(/*forAudio*/ NO, handler); } -void FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler) { - RequestPermission(/*forAudio*/ YES, handler); +void FLTRequestAudioPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler) { + FLTRequestPermission(/*forAudio*/ YES, handler); } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d20eb50811f5..41818f58b0ec 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -132,7 +132,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { - FLTRequestCameraPermission(^(FlutterError *error) { + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { // Create FLTCam only if granted camera access. if (error) { [result sendFlutterError:error]; @@ -195,7 +195,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { // Setup audio capture session only if granted audio access. - FLTRequestAudioPermission(^(FlutterError *error) { + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { if (error) { [result sendFlutterError:error]; } else { From a169b344eb540328833f6c65696c11e1d95e8387 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 17 May 2022 11:53:25 -0700 Subject: [PATCH 5/5] rqeuest audio permission on create call instead of prepareForVideoRecording call --- .../camera/camera/ios/Classes/CameraPlugin.m | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 41818f58b0ec..64952e8d70f1 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -132,14 +132,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { - // Create FLTCam only if granted camera access. - if (error) { - [result sendFlutterError:error]; - } else { - [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; - } - }); + [self handleCreateMethodCall:call result:result]; } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; [result sendSuccess]; @@ -194,19 +187,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera close]; [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { - // Setup audio capture session only if granted audio access. - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { - if (error) { - [result sendFlutterError:error]; - } else { - // Permission completion handler may be called on arbitrary queue. - // Dispatch to `captureSessionQueue` to setup audio capture session. - dispatch_async(self.captureSessionQueue, ^{ - [self.camera setUpCaptureSessionForAudio]; - [result sendSuccess]; - }); - } - }); + [self.camera setUpCaptureSessionForAudio]; + [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -269,6 +251,33 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } } +- (void)handleCreateMethodCall:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { + // Create FLTCam only if granted camera access (and audio access if audio is enabled) + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if (error) { + [result sendFlutterError:error]; + } else { + // Request audio permission on `create` call with `enableAudio` argument instead of the + // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is + // optional, and used as a workaround to fix a missing frame issue on iOS. + BOOL audioEnabled = [call.arguments[@"enableAudio"] boolValue]; + if (audioEnabled) { + // Setup audio capture session only if granted audio access. + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if (error) { + [result sendFlutterError:error]; + } else { + [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + }); + } else { + [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + } + }); +} + - (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall result:(FLTThreadSafeFlutterResult *)result { dispatch_async(self.captureSessionQueue, ^{