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

Commit bd3f490

Browse files
authored
[camera] Request access permission for audio (#5766)
1 parent 364c53f commit bd3f490

File tree

8 files changed

+242
-33
lines changed

8 files changed

+242
-33
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/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,16 @@ Here is a list of all permission error codes that can be thrown:
8888

8989
- `CameraAccessDenied`: Thrown when user denies the camera access permission.
9090

91-
- `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.
91+
- `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.
9292

9393
- `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control).
9494

95+
- `AudioAccessDenied`: Thrown when user denies the audio access permission.
96+
97+
- `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.
98+
99+
- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control).
100+
95101
- `cameraPermission`: Android and Web only. A legacy error code for all kinds of camera permission errors.
96102

97103
### Example

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

Lines changed: 108 additions & 0 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:
@@ -120,4 +122,110 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess {
120122
[self waitForExpectationsWithTimeout:1 handler:nil];
121123
}
122124

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+
FLTRequestAudioPermissionWithCompletionHandler(^(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+
FLTRequestAudioPermissionWithCompletionHandler(^(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+
FLTRequestAudioPermissionWithCompletionHandler(^(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+
FLTRequestAudioPermissionWithCompletionHandler(^(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+
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
223+
if ([error isEqual:expectedError]) {
224+
[expectation fulfill];
225+
}
226+
});
227+
228+
[self waitForExpectationsWithTimeout:1 handler:nil];
229+
}
230+
123231
@end

packages/camera/camera/example/lib/main.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,17 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
697697
// iOS only
698698
showInSnackBar('Camera access is restricted.');
699699
break;
700+
case 'AudioAccessDenied':
701+
showInSnackBar('You have denied audio access.');
702+
break;
703+
case 'AudioAccessDeniedWithoutPrompt':
704+
// iOS only
705+
showInSnackBar('Please go to Settings app to enable audio access.');
706+
break;
707+
case 'AudioAccessRestricted':
708+
// iOS only
709+
showInSnackBar('Audio access is restricted.');
710+
break;
700711
case 'cameraPermission':
701712
// Android & web only
702713
showInSnackBar('Unknown permission error.');

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,15 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *);
1818
/// called on an arbitrary dispatch queue.
1919
extern void FLTRequestCameraPermissionWithCompletionHandler(
2020
FLTCameraPermissionRequestCompletionHandler handler);
21+
22+
/// Requests audio access permission.
23+
///
24+
/// If it is the first time requesting audio access, a permission dialog will show up on the
25+
/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
26+
/// user will have to update the choice in Settings app.
27+
///
28+
/// @param handler if access permission is (or was previously) granted, completion handler will be
29+
/// called without error; Otherwise completion handler will be called with error. Handler can be
30+
/// called on an arbitrary dispatch queue.
31+
extern void FLTRequestAudioPermissionWithCompletionHandler(
32+
FLTCameraPermissionRequestCompletionHandler handler);

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

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

8-
void FLTRequestCameraPermissionWithCompletionHandler(
9-
FLTCameraPermissionRequestCompletionHandler handler) {
10-
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
8+
void FLTRequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) {
9+
AVMediaType mediaType;
10+
if (forAudio) {
11+
mediaType = AVMediaTypeAudio;
12+
} else {
13+
mediaType = AVMediaTypeVideo;
14+
}
15+
16+
switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) {
1117
case AVAuthorizationStatusAuthorized:
1218
handler(nil);
1319
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]);
20+
case AVAuthorizationStatusDenied: {
21+
FlutterError *flutterError;
22+
if (forAudio) {
23+
flutterError =
24+
[FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
25+
message:@"User has previously denied the audio access request. "
26+
@"Go to Settings to enable audio access."
27+
details:nil];
28+
} else {
29+
flutterError =
30+
[FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
31+
message:@"User has previously denied the camera access request. "
32+
@"Go to Settings to enable camera access."
33+
details:nil];
34+
}
35+
handler(flutterError);
1936
break;
20-
case AVAuthorizationStatusRestricted:
21-
handler([FlutterError errorWithCode:@"CameraAccessRestricted"
22-
message:@"Camera access is restricted. "
23-
details:nil]);
37+
}
38+
case AVAuthorizationStatusRestricted: {
39+
FlutterError *flutterError;
40+
if (forAudio) {
41+
flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted"
42+
message:@"Audio access is restricted. "
43+
details:nil];
44+
} else {
45+
flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted"
46+
message:@"Camera access is restricted. "
47+
details:nil];
48+
}
49+
handler(flutterError);
2450
break;
51+
}
2552
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-
}];
53+
[AVCaptureDevice requestAccessForMediaType:mediaType
54+
completionHandler:^(BOOL granted) {
55+
// handler can be invoked on an arbitrary dispatch queue.
56+
if (granted) {
57+
handler(nil);
58+
} else {
59+
FlutterError *flutterError;
60+
if (forAudio) {
61+
flutterError = [FlutterError
62+
errorWithCode:@"AudioAccessDenied"
63+
message:@"User denied the audio access request."
64+
details:nil];
65+
} else {
66+
flutterError = [FlutterError
67+
errorWithCode:@"CameraAccessDenied"
68+
message:@"User denied the camera access request."
69+
details:nil];
70+
}
71+
handler(flutterError);
72+
}
73+
}];
3674
break;
3775
}
3876
}
3977
}
78+
79+
void FLTRequestCameraPermissionWithCompletionHandler(
80+
FLTCameraPermissionRequestCompletionHandler handler) {
81+
FLTRequestPermission(/*forAudio*/ NO, handler);
82+
}
83+
84+
void FLTRequestAudioPermissionWithCompletionHandler(
85+
FLTCameraPermissionRequestCompletionHandler handler) {
86+
FLTRequestPermission(/*forAudio*/ YES, handler);
87+
}

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
132132
[result sendNotImplemented];
133133
}
134134
} else if ([@"create" isEqualToString:call.method]) {
135-
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
136-
// Create FLTCam only if granted camera access.
137-
if (error) {
138-
[result sendFlutterError:error];
139-
} else {
140-
[self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
141-
}
142-
});
135+
[self handleCreateMethodCall:call result:result];
143136
} else if ([@"startImageStream" isEqualToString:call.method]) {
144137
[_camera startImageStreamWithMessenger:_messenger];
145138
[result sendSuccess];
@@ -194,7 +187,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
194187
[_camera close];
195188
[result sendSuccess];
196189
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
197-
[_camera setUpCaptureSessionForAudio];
190+
[self.camera setUpCaptureSessionForAudio];
198191
[result sendSuccess];
199192
} else if ([@"startVideoRecording" isEqualToString:call.method]) {
200193
[_camera startVideoRecordingWithResult:result];
@@ -258,6 +251,33 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
258251
}
259252
}
260253

254+
- (void)handleCreateMethodCall:(FlutterMethodCall *)call
255+
result:(FLTThreadSafeFlutterResult *)result {
256+
// Create FLTCam only if granted camera access (and audio access if audio is enabled)
257+
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
258+
if (error) {
259+
[result sendFlutterError:error];
260+
} else {
261+
// Request audio permission on `create` call with `enableAudio` argument instead of the
262+
// `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is
263+
// optional, and used as a workaround to fix a missing frame issue on iOS.
264+
BOOL audioEnabled = [call.arguments[@"enableAudio"] boolValue];
265+
if (audioEnabled) {
266+
// Setup audio capture session only if granted audio access.
267+
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
268+
if (error) {
269+
[result sendFlutterError:error];
270+
} else {
271+
[self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
272+
}
273+
});
274+
} else {
275+
[self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
276+
}
277+
}
278+
});
279+
}
280+
261281
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
262282
result:(FLTThreadSafeFlutterResult *)result {
263283
dispatch_async(self.captureSessionQueue, ^{

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)