diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 9db3eadff53..a96bfcefd99 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.0+32 + +* Removes all remaining `unawaited` calls to fix potential race conditions and updates the + camera state when video capture starts. + ## 0.5.0+31 * Wraps CameraX classes needed to set capture request options, which is needed to implement setting the exposure mode. diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index a6834280178..b05788d68a4 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -357,9 +357,9 @@ class AndroidCameraCameraX extends CameraPlatform { @override Future dispose(int cameraId) async { preview?.releaseFlutterSurfaceTexture(); - unawaited(liveCameraState?.removeObservers()); + await liveCameraState?.removeObservers(); processCameraProvider?.unbindAll(); - unawaited(imageAnalysis?.clearAnalyzer()); + await imageAnalysis?.clearAnalyzer(); } /// The camera has been initialized. @@ -645,6 +645,7 @@ class AndroidCameraCameraX extends CameraPlatform { if (!(await processCameraProvider!.isBound(videoCapture!))) { camera = await processCameraProvider! .bindToLifecycle(cameraSelector!, [videoCapture!]); + await _updateCameraInfoAndLiveCameraState(options.cameraId); } // Set target rotation to default CameraX rotation only if capture @@ -681,7 +682,7 @@ class AndroidCameraCameraX extends CameraPlatform { if (videoOutputPath == null) { // Stop the current active recording as we will be unable to complete it // in this error case. - unawaited(recording!.close()); + await recording!.close(); recording = null; pendingRecording = null; throw CameraException( @@ -690,7 +691,7 @@ class AndroidCameraCameraX extends CameraPlatform { 'while reporting success. The platform should always ' 'return a valid path or report an error.'); } - unawaited(recording!.close()); + await recording!.close(); recording = null; pendingRecording = null; return XFile(videoOutputPath!); @@ -813,7 +814,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Removes the previously set analyzer on the [imageAnalysis] instance, since /// image information should no longer be streamed. FutureOr _onFrameStreamCancel() async { - unawaited(imageAnalysis!.clearAnalyzer()); + await imageAnalysis!.clearAnalyzer(); } /// Converts between Android ImageFormat constants and [ImageFormatGroup]s. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f40f6a3d55d..c9da1786693 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+31 +version: 0.5.0+32 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 415c5ee6448..bc6a0d1c6a3 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -662,7 +662,9 @@ void main() { 'initializeCamera throws a CameraException when createCamera has not been called before initializedCamera', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - expect(() => camera.initializeCamera(3), throwsA(isA())); + await expectLater(() async { + await camera.initializeCamera(3); + }, throwsA(isA())); }); test('initializeCamera sends expected CameraInitializedEvent', () async { @@ -1006,26 +1008,37 @@ void main() { group('video recording', () { test( - 'startVideoCapturing binds video capture use case and starts the recording', + 'startVideoCapturing binds video capture use case, updates saved camera instance and its properties, and starts the recording', () async { // Set up mocks and constants. final AndroidCameraCameraX camera = AndroidCameraCameraX(); final MockPendingRecording mockPendingRecording = MockPendingRecording(); final MockRecording mockRecording = MockRecording(); + final MockCamera mockCamera = MockCamera(); + final MockCamera newMockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); + final MockLiveCameraState newMockLiveCameraState = MockLiveCameraState(); final TestSystemServicesHostApi mockSystemServicesApi = MockTestSystemServicesHostApi(); TestSystemServicesHostApi.setup(mockSystemServicesApi); // Set directly for test versus calling createCamera. camera.processCameraProvider = MockProcessCameraProvider(); - camera.camera = MockCamera(); + camera.camera = mockCamera; camera.recorder = MockRecorder(); camera.videoCapture = MockVideoCapture(); camera.cameraSelector = MockCameraSelector(); + camera.liveCameraState = mockLiveCameraState; // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; + // Tell plugin to create detached Observer when camera info updated. + camera.proxy = CameraXProxy( + createCameraStateObserver: (void Function(Object) onChanged) => + Observer.detached(onChanged: onChanged)); + const int cameraId = 17; const String outputPath = '/temp/MOV123.temp'; @@ -1039,12 +1052,30 @@ void main() { .thenAnswer((_) async => false); when(camera.processCameraProvider!.bindToLifecycle( camera.cameraSelector!, [camera.videoCapture!])) - .thenAnswer((_) async => camera.camera!); + .thenAnswer((_) async => newMockCamera); + when(newMockCamera.getCameraInfo()) + .thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => newMockLiveCameraState); await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); + // Verify VideoCapture UseCase is bound and camera & its properties + // are updated. verify(camera.processCameraProvider!.bindToLifecycle( camera.cameraSelector!, [camera.videoCapture!])); + expect(camera.camera, equals(newMockCamera)); + expect(camera.cameraInfo, equals(mockCameraInfo)); + verify(mockLiveCameraState.removeObservers()); + expect( + await testCameraClosingObserver( + camera, + cameraId, + verify(newMockLiveCameraState.observe(captureAny)).captured.single + as Observer), + isTrue); + + // Verify recording is started. expect(camera.pendingRecording, equals(mockPendingRecording)); expect(camera.recording, mockRecording); }); @@ -1056,6 +1087,8 @@ void main() { final AndroidCameraCameraX camera = AndroidCameraCameraX(); final MockPendingRecording mockPendingRecording = MockPendingRecording(); final MockRecording mockRecording = MockRecording(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); final TestSystemServicesHostApi mockSystemServicesApi = MockTestSystemServicesHostApi(); TestSystemServicesHostApi.setup(mockSystemServicesApi); @@ -1069,6 +1102,11 @@ void main() { // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; + // Tell plugin to create detached Observer when camera info updated. + camera.proxy = CameraXProxy( + createCameraStateObserver: (void Function(Object) onChanged) => + Observer.detached(onChanged: onChanged)); + const int cameraId = 17; const String outputPath = '/temp/MOV123.temp'; @@ -1082,7 +1120,11 @@ void main() { .thenAnswer((_) async => false); when(camera.processCameraProvider!.bindToLifecycle( camera.cameraSelector!, [camera.videoCapture!])) - .thenAnswer((_) async => MockCamera()); + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()) + .thenAnswer((_) => Future.value(mockCameraInfo)); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); @@ -1267,9 +1309,14 @@ void main() { camera.videoCapture = videoCapture; camera.videoOutputPath = videoOutputPath; + // Tell plugin that videoCapture use case was bound to start recording. + when(camera.processCameraProvider!.isBound(videoCapture)) + .thenAnswer((_) async => true); + final XFile file = await camera.stopVideoRecording(0); expect(file.path, videoOutputPath); + // Verify that recording stops. verify(recording.close()); verifyNoMoreInteractions(recording); }); @@ -1284,47 +1331,57 @@ void main() { camera.recording = null; camera.videoOutputPath = videoOutputPath; - expect( - () => camera.stopVideoRecording(0), throwsA(isA())); + await expectLater(() async { + await camera.stopVideoRecording(0); + }, throwsA(isA())); }); + }); - test( - 'stopVideoRecording throws a camera exception if ' - 'videoOutputPath is null, and sets recording to null', () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final MockRecording recording = MockRecording(); + test( + 'stopVideoRecording throws a camera exception if ' + 'videoOutputPath is null, and sets recording to null', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording mockRecording = MockRecording(); + final MockVideoCapture mockVideoCapture = MockVideoCapture(); - // Set directly for test versus calling startVideoCapturing. - camera.recording = recording; - camera.videoOutputPath = null; + // Set directly for test versus calling startVideoCapturing. + camera.processCameraProvider = MockProcessCameraProvider(); + camera.recording = mockRecording; + camera.videoOutputPath = null; + camera.videoCapture = mockVideoCapture; - expect( - () => camera.stopVideoRecording(0), throwsA(isA())); - expect(camera.recording, null); - }); + // Tell plugin that videoCapture use case was bound to start recording. + when(camera.processCameraProvider!.isBound(mockVideoCapture)) + .thenAnswer((_) async => true); - test( - 'calling stopVideoRecording twice stops the recording ' - 'and then throws a CameraException', () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final MockRecording recording = MockRecording(); - final MockProcessCameraProvider processCameraProvider = - MockProcessCameraProvider(); - final MockVideoCapture videoCapture = MockVideoCapture(); - const String videoOutputPath = '/test/output/path'; + await expectLater(() async { + await camera.stopVideoRecording(0); + }, throwsA(isA())); + expect(camera.recording, null); + }); - // Set directly for test versus calling createCamera and startVideoCapturing. - camera.processCameraProvider = processCameraProvider; - camera.recording = recording; - camera.videoCapture = videoCapture; - camera.videoOutputPath = videoOutputPath; + test( + 'calling stopVideoRecording twice stops the recording ' + 'and then throws a CameraException', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + final MockProcessCameraProvider processCameraProvider = + MockProcessCameraProvider(); + final MockVideoCapture videoCapture = MockVideoCapture(); + const String videoOutputPath = '/test/output/path'; - final XFile file = await camera.stopVideoRecording(0); - expect(file.path, videoOutputPath); + // Set directly for test versus calling createCamera and startVideoCapturing. + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; - expect( - () => camera.stopVideoRecording(0), throwsA(isA())); - }); + final XFile file = await camera.stopVideoRecording(0); + expect(file.path, videoOutputPath); + + await expectLater(() async { + await camera.stopVideoRecording(0); + }, throwsA(isA())); }); test(