Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit e8e213e

Browse files
committed
[camera]request access permission for audio
1 parent 49a0d36 commit e8e213e

File tree

9 files changed

+209
-45
lines changed

9 files changed

+209
-45
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.6
2+
3+
* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result.
4+
15
## 0.9.5+1
26

37
* Suppresses warnings for pre-iOS-11 codepaths.

packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition {
2929
result:^(id _Nullable result) {
3030
[disposeExpectation fulfill];
3131
}];
32-
[camera createCameraOnSessionQueueWithCreateMethodCall:createCall
33-
result:[[FLTThreadSafeFlutterResult alloc]
34-
initWithResult:^(id _Nullable result) {
35-
[createExpectation fulfill];
36-
}]];
32+
[camera createCameraOnCaptureSessionQueueWithCreateMethodCall:createCall
33+
result:[[FLTThreadSafeFlutterResult alloc]
34+
initWithResult:^(
35+
id _Nullable result) {
36+
[createExpectation fulfill];
37+
}]];
3738
[self waitForExpectationsWithTimeout:1 handler:nil];
3839
// `captureSessionQueue` must not be nil after `create` call. Otherwise a nil
3940
// `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:`

packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ - (void)testCreate_ShouldCallResultOnMainThread {
3636
methodCallWithMethodName:@"create"
3737
arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}];
3838

39-
[camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject];
39+
[camera createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:resultObject];
4040
[self waitForExpectationsWithTimeout:1 handler:nil];
4141

4242
// Verify the result

packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ @interface CameraPermissionTests : XCTestCase
1515

1616
@implementation CameraPermissionTests
1717

18+
#pragma mark - camera permissions
19+
1820
- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
1921
XCTestExpectation *expectation =
2022
[self expectationWithDescription:
@@ -24,7 +26,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
2426
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
2527
.andReturn(AVAuthorizationStatusAuthorized);
2628

27-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
29+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
2830
if (error == nil) {
2931
[expectation fulfill];
3032
}
@@ -44,7 +46,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied {
4446
id mockDevice = OCMClassMock([AVCaptureDevice class]);
4547
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
4648
.andReturn(AVAuthorizationStatusDenied);
47-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
49+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
4850
if ([error isEqual:expectedError]) {
4951
[expectation fulfill];
5052
}
@@ -63,7 +65,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted {
6365
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
6466
.andReturn(AVAuthorizationStatusRestricted);
6567

66-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
68+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
6769
if ([error isEqual:expectedError]) {
6870
[expectation fulfill];
6971
}
@@ -85,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess {
8587
return YES;
8688
}]]);
8789

88-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
90+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
8991
if (error == nil) {
9092
[grantedExpectation fulfill];
9193
}
@@ -111,7 +113,113 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess {
111113
block(NO);
112114
return YES;
113115
}]]);
114-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
116+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
117+
if ([error isEqual:expectedError]) {
118+
[expectation fulfill];
119+
}
120+
});
121+
122+
[self waitForExpectationsWithTimeout:1 handler:nil];
123+
}
124+
125+
#pragma mark - audio permissions
126+
127+
- (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
128+
XCTestExpectation *expectation =
129+
[self expectationWithDescription:
130+
@"Must copmlete without error if audio access was previously authorized."];
131+
132+
id mockDevice = OCMClassMock([AVCaptureDevice class]);
133+
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
134+
.andReturn(AVAuthorizationStatusAuthorized);
135+
136+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
137+
if (error == nil) {
138+
[expectation fulfill];
139+
}
140+
});
141+
[self waitForExpectationsWithTimeout:1 handler:nil];
142+
}
143+
- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied {
144+
XCTestExpectation *expectation =
145+
[self expectationWithDescription:
146+
@"Must complete with error if audio access was previously denied."];
147+
FlutterError *expectedError =
148+
[FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
149+
message:@"User has previously denied the audio access request. Go to "
150+
@"Settings to enable audio access."
151+
details:nil];
152+
153+
id mockDevice = OCMClassMock([AVCaptureDevice class]);
154+
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
155+
.andReturn(AVAuthorizationStatusDenied);
156+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
157+
if ([error isEqual:expectedError]) {
158+
[expectation fulfill];
159+
}
160+
});
161+
[self waitForExpectationsWithTimeout:1 handler:nil];
162+
}
163+
164+
- (void)testRequestAudioPermission_completeWithErrorIfRestricted {
165+
XCTestExpectation *expectation =
166+
[self expectationWithDescription:@"Must complete with error if audio access is restricted."];
167+
FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessRestricted"
168+
message:@"Audio access is restricted. "
169+
details:nil];
170+
171+
id mockDevice = OCMClassMock([AVCaptureDevice class]);
172+
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
173+
.andReturn(AVAuthorizationStatusRestricted);
174+
175+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
176+
if ([error isEqual:expectedError]) {
177+
[expectation fulfill];
178+
}
179+
});
180+
[self waitForExpectationsWithTimeout:1 handler:nil];
181+
}
182+
183+
- (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess {
184+
XCTestExpectation *grantedExpectation = [self
185+
expectationWithDescription:@"Must complete without error if user choose to grant access"];
186+
187+
id mockDevice = OCMClassMock([AVCaptureDevice class]);
188+
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
189+
.andReturn(AVAuthorizationStatusNotDetermined);
190+
// Mimic user choosing "allow" in permission dialog.
191+
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
192+
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
193+
block(YES);
194+
return YES;
195+
}]]);
196+
197+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
198+
if (error == nil) {
199+
[grantedExpectation fulfill];
200+
}
201+
});
202+
[self waitForExpectationsWithTimeout:1 handler:nil];
203+
}
204+
205+
- (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess {
206+
XCTestExpectation *expectation =
207+
[self expectationWithDescription:@"Must complete with error if user choose to deny access"];
208+
FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessDenied"
209+
message:@"User denied the audio access request."
210+
details:nil];
211+
212+
id mockDevice = OCMClassMock([AVCaptureDevice class]);
213+
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
214+
.andReturn(AVAuthorizationStatusNotDetermined);
215+
216+
// Mimic user choosing "deny" in permission dialog.
217+
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
218+
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
219+
block(NO);
220+
return YES;
221+
}]]);
222+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
115223
if ([error isEqual:expectedError]) {
116224
[expectation fulfill];
117225
}

packages/camera/camera/ios/Classes/CameraPermissionUtils.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *);
1313
/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
1414
/// user will have to update the choice in Settings app.
1515
///
16+
///
17+
/// @param forAudio Requests for `AVMediaTypeAudio` permission if `forAudio` is true, and
18+
/// `AVMediaTypeVideo` permission otherwise.
1619
/// @param handler if access permission is (or was previously) granted, completion handler will be
1720
/// called without error; Otherwise completion handler will be called with error. Handler can be
1821
/// called on an arbitrary dispatch queue.
19-
extern void FLTRequestCameraPermissionWithCompletionHandler(
20-
FLTCameraPermissionRequestCompletionHandler handler);
22+
extern void FLTRequestCameraPermission(BOOL forAudio,
23+
FLTCameraPermissionRequestCompletionHandler handler);

packages/camera/camera/ios/Classes/CameraPermissionUtils.m

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,73 @@
55
@import AVFoundation;
66
#import "CameraPermissionUtils.h"
77

8-
void FLTRequestCameraPermissionWithCompletionHandler(
9-
FLTCameraPermissionRequestCompletionHandler handler) {
10-
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
8+
void FLTRequestCameraPermission(BOOL forAudio,
9+
FLTCameraPermissionRequestCompletionHandler handler) {
10+
AVMediaType mediaType;
11+
if (forAudio) {
12+
mediaType = AVMediaTypeAudio;
13+
} else {
14+
mediaType = AVMediaTypeVideo;
15+
}
16+
17+
switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) {
1118
case AVAuthorizationStatusAuthorized:
1219
handler(nil);
1320
break;
14-
case AVAuthorizationStatusDenied:
15-
handler([FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
16-
message:@"User has previously denied the camera access request. "
17-
@"Go to Settings to enable camera access."
18-
details:nil]);
21+
case AVAuthorizationStatusDenied: {
22+
FlutterError *flutterError;
23+
if (forAudio) {
24+
flutterError =
25+
[FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
26+
message:@"User has previously denied the audio access request. "
27+
@"Go to Settings to enable audio access."
28+
details:nil];
29+
} else {
30+
flutterError =
31+
[FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
32+
message:@"User has previously denied the camera access request. "
33+
@"Go to Settings to enable camera access."
34+
details:nil];
35+
}
36+
handler(flutterError);
1937
break;
20-
case AVAuthorizationStatusRestricted:
21-
handler([FlutterError errorWithCode:@"CameraAccessRestricted"
22-
message:@"Camera access is restricted. "
23-
details:nil]);
38+
}
39+
case AVAuthorizationStatusRestricted: {
40+
FlutterError *flutterError;
41+
if (forAudio) {
42+
flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted"
43+
message:@"Audio access is restricted. "
44+
details:nil];
45+
} else {
46+
flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted"
47+
message:@"Camera access is restricted. "
48+
details:nil];
49+
}
50+
handler(flutterError);
2451
break;
52+
}
2553
case AVAuthorizationStatusNotDetermined: {
26-
[AVCaptureDevice
27-
requestAccessForMediaType:AVMediaTypeVideo
28-
completionHandler:^(BOOL granted) {
29-
// handler can be invoked on an arbitrary dispatch queue.
30-
handler(granted ? nil
31-
: [FlutterError
32-
errorWithCode:@"CameraAccessDenied"
33-
message:@"User denied the camera access request."
34-
details:nil]);
35-
}];
54+
[AVCaptureDevice requestAccessForMediaType:mediaType
55+
completionHandler:^(BOOL granted) {
56+
// handler can be invoked on an arbitrary dispatch queue.
57+
if (granted) {
58+
handler(nil);
59+
} else {
60+
FlutterError *flutterError;
61+
if (forAudio) {
62+
flutterError = [FlutterError
63+
errorWithCode:@"AudioAccessDenied"
64+
message:@"User denied the audio access request."
65+
details:nil];
66+
} else {
67+
flutterError = [FlutterError
68+
errorWithCode:@"CameraAccessDenied"
69+
message:@"User denied the camera access request."
70+
details:nil];
71+
}
72+
handler(flutterError);
73+
}
74+
}];
3675
break;
3776
}
3877
}

packages/camera/camera/ios/Classes/CameraPlugin.m

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
132132
[result sendNotImplemented];
133133
}
134134
} else if ([@"create" isEqualToString:call.method]) {
135-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
135+
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
136136
// Create FLTCam only if granted camera access.
137137
if (error) {
138138
[result sendFlutterError:error];
139139
} else {
140-
[self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
140+
[self createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:result];
141141
}
142142
});
143143
} else if ([@"startImageStream" isEqualToString:call.method]) {
@@ -194,8 +194,17 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
194194
[_camera close];
195195
[result sendSuccess];
196196
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
197-
[_camera setUpCaptureSessionForAudio];
198-
[result sendSuccess];
197+
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
198+
// Setup audio capture session only if granted audio access
199+
if (error) {
200+
[result sendFlutterError:error];
201+
} else {
202+
dispatch_async(self.captureSessionQueue, ^{
203+
[self.camera setUpCaptureSessionForAudio];
204+
[result sendSuccess];
205+
});
206+
}
207+
});
199208
} else if ([@"startVideoRecording" isEqualToString:call.method]) {
200209
[_camera startVideoRecordingWithResult:result];
201210
} else if ([@"stopVideoRecording" isEqualToString:call.method]) {
@@ -258,8 +267,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
258267
}
259268
}
260269

261-
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
262-
result:(FLTThreadSafeFlutterResult *)result {
270+
- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
271+
result:(FLTThreadSafeFlutterResult *)result {
263272
dispatch_async(self.captureSessionQueue, ^{
264273
NSString *cameraName = createMethodCall.arguments[@"cameraName"];
265274
NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"];

packages/camera/camera/ios/Classes/CameraPlugin_Test.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
/// that triggered the orientation change.
3939
- (void)orientationChanged:(NSNotification *)notification;
4040

41-
/// Creates FLTCam on session queue and reports the creation result.
41+
/// Creates FLTCam on capture session queue and reports the creation result.
4242
/// @param createMethodCall the create method call
4343
/// @param result a thread safe flutter result wrapper object to report creation result.
44-
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
45-
result:(FLTThreadSafeFlutterResult *)result;
44+
- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
45+
result:(FLTThreadSafeFlutterResult *)result;
4646

4747
@end

packages/camera/camera/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
44
Dart.
55
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
7-
version: 0.9.5+1
7+
version: 0.9.6
88

99
environment:
1010
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)