Skip to content

Commit

Permalink
[camerax] Fixes relistening to onStreamedFrameAvailable's stream be…
Browse files Browse the repository at this point in the history
…havior (flutter#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 flutter#130005.
  • Loading branch information
camsim99 authored Jul 18, 2023
1 parent 1fd191e commit 5af8290
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 11 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);
Expand Down Expand Up @@ -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!
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);
Expand All @@ -963,6 +1003,8 @@ void main() {
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);
Expand All @@ -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, <UseCase>[camera.mockImageAnalysis]));

await capturedAnalyzer.analyze(mockImageProxy);
Expand All @@ -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);
Expand Down

0 comments on commit 5af8290

Please sign in to comment.