From 5af829020cf07914a8c16c4223a9afb80adbdeca Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Tue, 18 Jul 2023 13:38:30 -0700 Subject: [PATCH] [camerax] Fixes relistening to `onStreamedFrameAvailable`'s stream behavior (#4511) Removes incorrect assumption causing image stream to stop emitting data after subscription to stream is canceled and then the stream is listened to again. Fixes https://github.com/flutter/flutter/issues/130005. --- .../camera_android_camerax/CHANGELOG.md | 4 ++ .../lib/src/android_camera_camerax.dart | 13 +++-- .../camera_android_camerax/pubspec.yaml | 2 +- .../test/android_camera_camerax_test.dart | 50 +++++++++++++++++-- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 01fb140b31928..ab60e8aba87a4 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+11 + +* Fixes issue with image data not being emitted after relistening to stream returned by `onStreamedFrameAvailable`. + ## 0.5.0+10 * Implements off, auto, and always flash mode configurations for image capture. 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 99f179bbee2da..9a58e6f4df867 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 @@ -605,12 +605,6 @@ class AndroidCameraCameraX extends CameraPlatform { /// Configures the [imageAnalysis] instance for image streaming and binds it /// to camera lifecycle controlled by the [processCameraProvider]. Future _configureAndBindImageAnalysisToLifecycle() async { - if (imageAnalysis != null && - await processCameraProvider!.isBound(imageAnalysis!)) { - // imageAnalysis already configured and bound to lifecycle. - return; - } - // Create Analyzer that can read image data for image streaming. final WeakReference weakThis = WeakReference(this); @@ -648,9 +642,14 @@ class AndroidCameraCameraX extends CameraPlatform { // TODO(camsim99): Support resolution configuration. // Defaults to YUV_420_888 image format. - imageAnalysis = createImageAnalysis(null); + imageAnalysis ??= createImageAnalysis(null); unawaited(imageAnalysis!.setAnalyzer(analyzer)); + if (await processCameraProvider!.isBound(imageAnalysis!)) { + // No need to bind imageAnalysis to lifecycle again. + return; + } + // TODO(camsim99): Reset live camera state observers here when // https://github.com/flutter/packages/pull/3419 lands. camera = await processCameraProvider! diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 328ae287f54fe..0fea9fc682407 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+10 +version: 0.5.0+11 environment: sdk: ">=2.19.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 b1c57e27dab24..bdebdce095d61 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 @@ -940,7 +940,47 @@ void main() { }); test( - 'onStreamedFrameAvaiable returns stream that responds expectedly to being listened to', + 'onStreamedFrameAvailable emits CameraImageData when listened to after cancelation', + () async { + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); + const int cameraId = 22; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = MockCameraSelector(); + + when(mockProcessCameraProvider.bindToLifecycle(any, any)) + .thenAnswer((_) => Future.value(mockCamera)); + when(mockCamera.getCameraInfo()) + .thenAnswer((_) => Future.value(MockCameraInfo())); + + final CameraImageData mockCameraImageData = MockCameraImageData(); + final Stream imageStream = + camera.onStreamedFrameAvailable(cameraId); + + // Listen to image stream. + final StreamSubscription imageStreamSubscription = + imageStream.listen((CameraImageData data) {}); + + // Cancel subscription to image stream. + await imageStreamSubscription.cancel(); + final Stream imageStream2 = + camera.onStreamedFrameAvailable(cameraId); + + // Listen to image stream again. + final StreamQueue streamQueue = + StreamQueue(imageStream2); + camera.cameraImageDataStreamController!.add(mockCameraImageData); + + expect(await streamQueue.next, equals(mockCameraImageData)); + await streamQueue.cancel(); + }); + + test( + 'onStreamedFrameAvailable returns stream that responds expectedly to being listened to', () async { final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); @@ -963,6 +1003,8 @@ void main() { camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = mockCameraSelector; + when(mockProcessCameraProvider.isBound(camera.mockImageAnalysis)) + .thenAnswer((_) async => Future.value(false)); when(mockProcessCameraProvider.bindToLifecycle( mockCameraSelector, [camera.mockImageAnalysis])) .thenAnswer((_) async => mockCamera); @@ -989,7 +1031,9 @@ void main() { final Analyzer capturedAnalyzer = verify(camera.mockImageAnalysis.setAnalyzer(captureAny)).captured.single as Analyzer; - verify(mockProcessCameraProvider.bindToLifecycle( + await untilCalled( + mockProcessCameraProvider.isBound(camera.mockImageAnalysis)); + await untilCalled(mockProcessCameraProvider.bindToLifecycle( mockCameraSelector, [camera.mockImageAnalysis])); await capturedAnalyzer.analyze(mockImageProxy); @@ -1011,7 +1055,7 @@ void main() { }); test( - 'onStreamedFrameAvaiable returns stream that responds expectedly to being canceled', + 'onStreamedFrameAvailable returns stream that responds expectedly to being canceled', () async { final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true);