Skip to content

Commit

Permalink
[camerax] Change buildPreview to return Texture versus `FutureBui…
Browse files Browse the repository at this point in the history
…lder` (#6021)

Changes the `buildPreview` implementation to return a `Texture` versus a `FutureBuilder` that would ultimately return that same `Texture`.

The `FutureBuilder` depended on checking a condition that should be true by default assuming `createCamera` was called prior. This seems to be an assumption made consistently across other platform implementations (including `camera_android`-- see [source](https://github.com/flutter/packages/blob/7b1ae1f4cceea02f330716e701c6dcd9f0ca3f7e/packages/camera/camera_android/lib/src/android_camera.dart#L520)), so this PR is achieving consistency with them.

Fixes flutter/flutter#140567. Thanks to @davidmartos96 for the push to investigate deeper on that issue!
  • Loading branch information
camsim99 authored Feb 1, 2024
1 parent 65122e5 commit 450251a
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 78 deletions.
6 changes: 6 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.5.0+29

* Modifies `buildPreview` to return `Texture` that maps to camera preview, building in the assumption
that `createCamera` should have been called before building the preview. Fixes
https://github.com/flutter/flutter/issues/140567.

## 0.5.0+28

* Wraps CameraX classes needed to implement setting focus and exposure points and exposure offset.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ class AndroidCameraCameraX extends CameraPlatform {
@visibleForTesting
String? videoOutputPath;

/// Whether or not [preview] has been bound to the lifecycle of the camera by
/// [createCamera].
@visibleForTesting
bool previewInitiallyBound = false;

bool _previewIsPaused = false;

/// The prefix used to create the filename for video recording files.
Expand Down Expand Up @@ -284,6 +289,7 @@ class AndroidCameraCameraX extends CameraPlatform {
camera = await processCameraProvider!.bindToLifecycle(
cameraSelector!, <UseCase>[preview!, imageCapture!, imageAnalysis!]);
await _updateCameraInfoAndLiveCameraState(flutterSurfaceTextureId);
previewInitiallyBound = true;
_previewIsPaused = false;

return flutterSurfaceTextureId;
Expand Down Expand Up @@ -520,21 +526,20 @@ class AndroidCameraCameraX extends CameraPlatform {
}

/// Returns a widget showing a live camera preview.
///
/// [createCamera] must be called before attempting to build this preview.
@override
Widget buildPreview(int cameraId) {
return FutureBuilder<void>(
future: _bindPreviewToLifecycle(cameraId),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
// Do nothing while waiting for preview to be bound to lifecyle.
return const SizedBox.shrink();
case ConnectionState.done:
return Texture(textureId: cameraId);
}
});
if (!previewInitiallyBound) {
// No camera has been created, and thus, the preview UseCase has not been
// bound to the camera lifecycle, restricting this preview from being
// built.
throw CameraException(
'cameraNotFound',
"Camera not found. Please call the 'create' method before calling 'buildPreview'",
);
}
return Texture(textureId: cameraId);
}

/// Captures an image and returns the file where it was saved.
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+28
version: 0.5.0+29

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ void main() {

// Verify the camera's CameraInfo instance got updated.
expect(camera.cameraInfo, equals(mockCameraInfo));

// Verify preview has been marked as bound to the camera lifecycle by
// createCamera.
expect(camera.previewInitiallyBound, isTrue);
});

test(
Expand Down Expand Up @@ -971,83 +975,33 @@ void main() {
});

test(
'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle',
'buildPreview throws an exception if the preview is not bound to the lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
final MockProcessCameraProvider mockProcessCameraProvider =
MockProcessCameraProvider();
final MockCamera mockCamera = MockCamera();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
const int textureId = 75;

// Set directly for test versus calling createCamera.
camera.processCameraProvider = mockProcessCameraProvider;
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();

// Tell plugin to create a mock Observer<CameraState>, that is created to
// track camera state once preview is bound to the lifecycle.
camera.proxy =
CameraXProxy(createCameraStateObserver: (_) => MockObserver());

when(mockProcessCameraProvider
.bindToLifecycle(camera.cameraSelector, <UseCase>[camera.preview!]))
.thenAnswer((_) async => mockCamera);
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
.thenAnswer((_) async => MockLiveCameraState());
const int cameraId = 73;

final FutureBuilder<void> previewWidget =
camera.buildPreview(textureId) as FutureBuilder<void>;
// Tell camera that createCamera has not been called and thus, preview has
// not been bound to the lifecycle of the camera.
camera.previewInitiallyBound = false;

expect(
previewWidget.builder(
MockBuildContext(), const AsyncSnapshot<void>.nothing()),
isA<SizedBox>());
expect(
previewWidget.builder(
MockBuildContext(), const AsyncSnapshot<void>.waiting()),
isA<SizedBox>());
expect(
previewWidget.builder(MockBuildContext(),
const AsyncSnapshot<void>.withData(ConnectionState.active, null)),
isA<SizedBox>());
() => camera.buildPreview(cameraId), throwsA(isA<CameraException>()));
});

test(
'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle',
'buildPreview returns a Texture once the preview is bound to the lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
final MockProcessCameraProvider mockProcessCameraProvider =
MockProcessCameraProvider();
final MockCamera mockCamera = MockCamera();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
const int textureId = 75;

// Set directly for test versus calling createCamera.
camera.processCameraProvider = mockProcessCameraProvider;
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();

// Tell plugin to create a mock Observer<CameraState>, that is created to
// track camera state once preview is bound to the lifecycle.
camera.proxy =
CameraXProxy(createCameraStateObserver: (_) => MockObserver());
const int cameraId = 37;

when(mockProcessCameraProvider
.bindToLifecycle(camera.cameraSelector, <UseCase>[camera.preview!]))
.thenAnswer((_) async => mockCamera);
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
.thenAnswer((_) async => MockLiveCameraState());
// Tell camera that createCamera has been called and thus, preview has been
// bound to the lifecycle of the camera.
camera.previewInitiallyBound = true;

final FutureBuilder<void> previewWidget =
camera.buildPreview(textureId) as FutureBuilder<void>;
final Widget widget = camera.buildPreview(cameraId);

final Texture previewTexture = previewWidget.builder(MockBuildContext(),
const AsyncSnapshot<void>.withData(ConnectionState.done, null))
as Texture;
expect(previewTexture.textureId, equals(textureId));
expect(widget is Texture, isTrue);
expect((widget as Texture).textureId, cameraId);
});

group('video recording', () {
Expand Down

0 comments on commit 450251a

Please sign in to comment.