Skip to content

Commit cd45100

Browse files
authored
[camerax] Small fixes to starting/stopping video capture (#6068)
Fixes small issues I noticed in starting/stopping video capture while working on #6059, notably: - Change all remaining `unawaited` calls to `await` to avoid any racy behavior. - Update `camera` info after `VideoCapture` use case is bound to the lifecycle of the plugin's `ProcessCameraProvider` to make sure it is up to date. - ~Unbind `VideoCapture` use case when video recording stops since it was suggested to lazily load it for performance reasons (open to pushback on this).~ this would require potentially more changes than I originally thought - Make tests checking that async methods throw exceptions actually wait for those exceptions as this may cause flaky test behavior. Fixes flutter/flutter#132499 as this PR removes any remaining `unawaited` calls.
1 parent 316bde0 commit cd45100

File tree

4 files changed

+107
-44
lines changed

4 files changed

+107
-44
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.5.0+32
2+
3+
* Removes all remaining `unawaited` calls to fix potential race conditions and updates the
4+
camera state when video capture starts.
5+
16
## 0.5.0+31
27

38
* Wraps CameraX classes needed to set capture request options, which is needed to implement setting the exposure mode.

packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@ class AndroidCameraCameraX extends CameraPlatform {
357357
@override
358358
Future<void> dispose(int cameraId) async {
359359
preview?.releaseFlutterSurfaceTexture();
360-
unawaited(liveCameraState?.removeObservers());
360+
await liveCameraState?.removeObservers();
361361
processCameraProvider?.unbindAll();
362-
unawaited(imageAnalysis?.clearAnalyzer());
362+
await imageAnalysis?.clearAnalyzer();
363363
}
364364

365365
/// The camera has been initialized.
@@ -645,6 +645,7 @@ class AndroidCameraCameraX extends CameraPlatform {
645645
if (!(await processCameraProvider!.isBound(videoCapture!))) {
646646
camera = await processCameraProvider!
647647
.bindToLifecycle(cameraSelector!, <UseCase>[videoCapture!]);
648+
await _updateCameraInfoAndLiveCameraState(options.cameraId);
648649
}
649650

650651
// Set target rotation to default CameraX rotation only if capture
@@ -681,7 +682,7 @@ class AndroidCameraCameraX extends CameraPlatform {
681682
if (videoOutputPath == null) {
682683
// Stop the current active recording as we will be unable to complete it
683684
// in this error case.
684-
unawaited(recording!.close());
685+
await recording!.close();
685686
recording = null;
686687
pendingRecording = null;
687688
throw CameraException(
@@ -690,7 +691,7 @@ class AndroidCameraCameraX extends CameraPlatform {
690691
'while reporting success. The platform should always '
691692
'return a valid path or report an error.');
692693
}
693-
unawaited(recording!.close());
694+
await recording!.close();
694695
recording = null;
695696
pendingRecording = null;
696697
return XFile(videoOutputPath!);
@@ -813,7 +814,7 @@ class AndroidCameraCameraX extends CameraPlatform {
813814
/// Removes the previously set analyzer on the [imageAnalysis] instance, since
814815
/// image information should no longer be streamed.
815816
FutureOr<void> _onFrameStreamCancel() async {
816-
unawaited(imageAnalysis!.clearAnalyzer());
817+
await imageAnalysis!.clearAnalyzer();
817818
}
818819

819820
/// Converts between Android ImageFormat constants and [ImageFormatGroup]s.

packages/camera/camera_android_camerax/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android_camerax
22
description: Android implementation of the camera plugin using the CameraX library.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.5.0+31
5+
version: 0.5.0+32
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart

Lines changed: 95 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,9 @@ void main() {
662662
'initializeCamera throws a CameraException when createCamera has not been called before initializedCamera',
663663
() async {
664664
final AndroidCameraCameraX camera = AndroidCameraCameraX();
665-
expect(() => camera.initializeCamera(3), throwsA(isA<CameraException>()));
665+
await expectLater(() async {
666+
await camera.initializeCamera(3);
667+
}, throwsA(isA<CameraException>()));
666668
});
667669

668670
test('initializeCamera sends expected CameraInitializedEvent', () async {
@@ -1006,26 +1008,37 @@ void main() {
10061008

10071009
group('video recording', () {
10081010
test(
1009-
'startVideoCapturing binds video capture use case and starts the recording',
1011+
'startVideoCapturing binds video capture use case, updates saved camera instance and its properties, and starts the recording',
10101012
() async {
10111013
// Set up mocks and constants.
10121014
final AndroidCameraCameraX camera = AndroidCameraCameraX();
10131015
final MockPendingRecording mockPendingRecording = MockPendingRecording();
10141016
final MockRecording mockRecording = MockRecording();
1017+
final MockCamera mockCamera = MockCamera();
1018+
final MockCamera newMockCamera = MockCamera();
1019+
final MockCameraInfo mockCameraInfo = MockCameraInfo();
1020+
final MockLiveCameraState mockLiveCameraState = MockLiveCameraState();
1021+
final MockLiveCameraState newMockLiveCameraState = MockLiveCameraState();
10151022
final TestSystemServicesHostApi mockSystemServicesApi =
10161023
MockTestSystemServicesHostApi();
10171024
TestSystemServicesHostApi.setup(mockSystemServicesApi);
10181025

10191026
// Set directly for test versus calling createCamera.
10201027
camera.processCameraProvider = MockProcessCameraProvider();
1021-
camera.camera = MockCamera();
1028+
camera.camera = mockCamera;
10221029
camera.recorder = MockRecorder();
10231030
camera.videoCapture = MockVideoCapture();
10241031
camera.cameraSelector = MockCameraSelector();
1032+
camera.liveCameraState = mockLiveCameraState;
10251033

10261034
// Ignore setting target rotation for this test; tested seprately.
10271035
camera.captureOrientationLocked = true;
10281036

1037+
// Tell plugin to create detached Observer when camera info updated.
1038+
camera.proxy = CameraXProxy(
1039+
createCameraStateObserver: (void Function(Object) onChanged) =>
1040+
Observer<CameraState>.detached(onChanged: onChanged));
1041+
10291042
const int cameraId = 17;
10301043
const String outputPath = '/temp/MOV123.temp';
10311044

@@ -1039,12 +1052,30 @@ void main() {
10391052
.thenAnswer((_) async => false);
10401053
when(camera.processCameraProvider!.bindToLifecycle(
10411054
camera.cameraSelector!, <UseCase>[camera.videoCapture!]))
1042-
.thenAnswer((_) async => camera.camera!);
1055+
.thenAnswer((_) async => newMockCamera);
1056+
when(newMockCamera.getCameraInfo())
1057+
.thenAnswer((_) async => mockCameraInfo);
1058+
when(mockCameraInfo.getCameraState())
1059+
.thenAnswer((_) async => newMockLiveCameraState);
10431060

10441061
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
10451062

1063+
// Verify VideoCapture UseCase is bound and camera & its properties
1064+
// are updated.
10461065
verify(camera.processCameraProvider!.bindToLifecycle(
10471066
camera.cameraSelector!, <UseCase>[camera.videoCapture!]));
1067+
expect(camera.camera, equals(newMockCamera));
1068+
expect(camera.cameraInfo, equals(mockCameraInfo));
1069+
verify(mockLiveCameraState.removeObservers());
1070+
expect(
1071+
await testCameraClosingObserver(
1072+
camera,
1073+
cameraId,
1074+
verify(newMockLiveCameraState.observe(captureAny)).captured.single
1075+
as Observer<dynamic>),
1076+
isTrue);
1077+
1078+
// Verify recording is started.
10481079
expect(camera.pendingRecording, equals(mockPendingRecording));
10491080
expect(camera.recording, mockRecording);
10501081
});
@@ -1056,6 +1087,8 @@ void main() {
10561087
final AndroidCameraCameraX camera = AndroidCameraCameraX();
10571088
final MockPendingRecording mockPendingRecording = MockPendingRecording();
10581089
final MockRecording mockRecording = MockRecording();
1090+
final MockCamera mockCamera = MockCamera();
1091+
final MockCameraInfo mockCameraInfo = MockCameraInfo();
10591092
final TestSystemServicesHostApi mockSystemServicesApi =
10601093
MockTestSystemServicesHostApi();
10611094
TestSystemServicesHostApi.setup(mockSystemServicesApi);
@@ -1069,6 +1102,11 @@ void main() {
10691102
// Ignore setting target rotation for this test; tested seprately.
10701103
camera.captureOrientationLocked = true;
10711104

1105+
// Tell plugin to create detached Observer when camera info updated.
1106+
camera.proxy = CameraXProxy(
1107+
createCameraStateObserver: (void Function(Object) onChanged) =>
1108+
Observer<CameraState>.detached(onChanged: onChanged));
1109+
10721110
const int cameraId = 17;
10731111
const String outputPath = '/temp/MOV123.temp';
10741112

@@ -1082,7 +1120,11 @@ void main() {
10821120
.thenAnswer((_) async => false);
10831121
when(camera.processCameraProvider!.bindToLifecycle(
10841122
camera.cameraSelector!, <UseCase>[camera.videoCapture!]))
1085-
.thenAnswer((_) async => MockCamera());
1123+
.thenAnswer((_) async => mockCamera);
1124+
when(mockCamera.getCameraInfo())
1125+
.thenAnswer((_) => Future<CameraInfo>.value(mockCameraInfo));
1126+
when(mockCameraInfo.getCameraState())
1127+
.thenAnswer((_) async => MockLiveCameraState());
10861128

10871129
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
10881130

@@ -1267,9 +1309,14 @@ void main() {
12671309
camera.videoCapture = videoCapture;
12681310
camera.videoOutputPath = videoOutputPath;
12691311

1312+
// Tell plugin that videoCapture use case was bound to start recording.
1313+
when(camera.processCameraProvider!.isBound(videoCapture))
1314+
.thenAnswer((_) async => true);
1315+
12701316
final XFile file = await camera.stopVideoRecording(0);
12711317
expect(file.path, videoOutputPath);
12721318

1319+
// Verify that recording stops.
12731320
verify(recording.close());
12741321
verifyNoMoreInteractions(recording);
12751322
});
@@ -1284,47 +1331,57 @@ void main() {
12841331
camera.recording = null;
12851332
camera.videoOutputPath = videoOutputPath;
12861333

1287-
expect(
1288-
() => camera.stopVideoRecording(0), throwsA(isA<CameraException>()));
1334+
await expectLater(() async {
1335+
await camera.stopVideoRecording(0);
1336+
}, throwsA(isA<CameraException>()));
12891337
});
1338+
});
12901339

1291-
test(
1292-
'stopVideoRecording throws a camera exception if '
1293-
'videoOutputPath is null, and sets recording to null', () async {
1294-
final AndroidCameraCameraX camera = AndroidCameraCameraX();
1295-
final MockRecording recording = MockRecording();
1340+
test(
1341+
'stopVideoRecording throws a camera exception if '
1342+
'videoOutputPath is null, and sets recording to null', () async {
1343+
final AndroidCameraCameraX camera = AndroidCameraCameraX();
1344+
final MockRecording mockRecording = MockRecording();
1345+
final MockVideoCapture mockVideoCapture = MockVideoCapture();
12961346

1297-
// Set directly for test versus calling startVideoCapturing.
1298-
camera.recording = recording;
1299-
camera.videoOutputPath = null;
1347+
// Set directly for test versus calling startVideoCapturing.
1348+
camera.processCameraProvider = MockProcessCameraProvider();
1349+
camera.recording = mockRecording;
1350+
camera.videoOutputPath = null;
1351+
camera.videoCapture = mockVideoCapture;
13001352

1301-
expect(
1302-
() => camera.stopVideoRecording(0), throwsA(isA<CameraException>()));
1303-
expect(camera.recording, null);
1304-
});
1353+
// Tell plugin that videoCapture use case was bound to start recording.
1354+
when(camera.processCameraProvider!.isBound(mockVideoCapture))
1355+
.thenAnswer((_) async => true);
13051356

1306-
test(
1307-
'calling stopVideoRecording twice stops the recording '
1308-
'and then throws a CameraException', () async {
1309-
final AndroidCameraCameraX camera = AndroidCameraCameraX();
1310-
final MockRecording recording = MockRecording();
1311-
final MockProcessCameraProvider processCameraProvider =
1312-
MockProcessCameraProvider();
1313-
final MockVideoCapture videoCapture = MockVideoCapture();
1314-
const String videoOutputPath = '/test/output/path';
1357+
await expectLater(() async {
1358+
await camera.stopVideoRecording(0);
1359+
}, throwsA(isA<CameraException>()));
1360+
expect(camera.recording, null);
1361+
});
13151362

1316-
// Set directly for test versus calling createCamera and startVideoCapturing.
1317-
camera.processCameraProvider = processCameraProvider;
1318-
camera.recording = recording;
1319-
camera.videoCapture = videoCapture;
1320-
camera.videoOutputPath = videoOutputPath;
1363+
test(
1364+
'calling stopVideoRecording twice stops the recording '
1365+
'and then throws a CameraException', () async {
1366+
final AndroidCameraCameraX camera = AndroidCameraCameraX();
1367+
final MockRecording recording = MockRecording();
1368+
final MockProcessCameraProvider processCameraProvider =
1369+
MockProcessCameraProvider();
1370+
final MockVideoCapture videoCapture = MockVideoCapture();
1371+
const String videoOutputPath = '/test/output/path';
13211372

1322-
final XFile file = await camera.stopVideoRecording(0);
1323-
expect(file.path, videoOutputPath);
1373+
// Set directly for test versus calling createCamera and startVideoCapturing.
1374+
camera.processCameraProvider = processCameraProvider;
1375+
camera.recording = recording;
1376+
camera.videoCapture = videoCapture;
1377+
camera.videoOutputPath = videoOutputPath;
13241378

1325-
expect(
1326-
() => camera.stopVideoRecording(0), throwsA(isA<CameraException>()));
1327-
});
1379+
final XFile file = await camera.stopVideoRecording(0);
1380+
expect(file.path, videoOutputPath);
1381+
1382+
await expectLater(() async {
1383+
await camera.stopVideoRecording(0);
1384+
}, throwsA(isA<CameraException>()));
13281385
});
13291386

13301387
test(

0 commit comments

Comments
 (0)