[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.
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index 01fb140..ab60e8a 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 99f179b..9a58e6f 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 @@
/// Configures the [imageAnalysis] instance for image streaming and binds it
/// to camera lifecycle controlled by the [processCameraProvider].
Future<void> _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<AndroidCameraCameraX> weakThis =
WeakReference<AndroidCameraCameraX>(this);
@@ -648,9 +642,14 @@
// 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 328ae28..0fea9fc 100644
--- a/packages/camera/camera_android_camerax/pubspec.yaml
+++ b/packages/camera/camera_android_camerax/pubspec.yaml
@@ -2,7 +2,7 @@
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 b1c57e2..bdebdce 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 @@
});
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<Camera>.value(mockCamera));
+ when(mockCamera.getCameraInfo())
+ .thenAnswer((_) => Future<CameraInfo>.value(MockCameraInfo()));
+
+ final CameraImageData mockCameraImageData = MockCameraImageData();
+ final Stream<CameraImageData> imageStream =
+ camera.onStreamedFrameAvailable(cameraId);
+
+ // Listen to image stream.
+ final StreamSubscription<CameraImageData> imageStreamSubscription =
+ imageStream.listen((CameraImageData data) {});
+
+ // Cancel subscription to image stream.
+ await imageStreamSubscription.cancel();
+ final Stream<CameraImageData> imageStream2 =
+ camera.onStreamedFrameAvailable(cameraId);
+
+ // Listen to image stream again.
+ final StreamQueue<CameraImageData> streamQueue =
+ StreamQueue<CameraImageData>(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 @@
camera.processCameraProvider = mockProcessCameraProvider;
camera.cameraSelector = mockCameraSelector;
+ when(mockProcessCameraProvider.isBound(camera.mockImageAnalysis))
+ .thenAnswer((_) async => Future<bool>.value(false));
when(mockProcessCameraProvider.bindToLifecycle(
mockCameraSelector, <UseCase>[camera.mockImageAnalysis]))
.thenAnswer((_) async => mockCamera);
@@ -989,7 +1031,9 @@
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, <UseCase>[camera.mockImageAnalysis]));
await capturedAnalyzer.analyze(mockImageProxy);
@@ -1011,7 +1055,7 @@
});
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);