This repository was archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[camerax] Implement camera preview #7112
Merged
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
7894cc2
Add base code from proof of concept
camsim99 efca3ed
Fix analyzer
camsim99 b4ae1db
Add example app and tests
camsim99 7c8168b
Actually add tets and visible for testing annotation
camsim99 93d442b
Make methods private
camsim99 d99ef9c
Merge remote-tracking branch 'upstream/main' into camx_preview
camsim99 bf1ca9a
Fix tests
camsim99 b237d6e
Fix analyze:
camsim99 a21429a
Update changelog
camsim99 dcff8bc
Formatting
camsim99 a183944
Merge remote-tracking branch 'upstream/main' into camx_preview
camsim99 f163907
Update todos with links and modify camera controller
camsim99 285659d
Format and add availableCameras
camsim99 cffc0ac
Fix mocks
camsim99 889802d
Try bumping mockito version
camsim99 24ee512
Review documentation
camsim99 573cf33
Re-generate mocks
camsim99 d08c087
Fix typo
camsim99 1e1dd8b
Fix another typo
camsim99 e1cea85
Address review and fix bug
camsim99 5ccc872
Merge remote-tracking branch 'upstream/main' into camx_preview
camsim99 7129845
Fix analyze
camsim99 c54c547
Merge remote-tracking branch 'upstream/main' into camx_preview
camsim99 5f30bc7
Fix tests
camsim99 8fc71f9
Foramtting
camsim99 d50bd87
Clear preview is paused
camsim99 de2fc29
Add unknown lens
camsim99 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Actually add tets and visible for testing annotation
- Loading branch information
commit 7c8168b39b5c8bdf4339b151c6ed977fa327efe0
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
337 changes: 337 additions & 0 deletions
337
packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,337 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:async/async.dart'; | ||
| import 'package:camera_android_camerax/camera_android_camerax.dart'; | ||
| import 'package:camera_android_camerax/src/camerax_library.g.dart'; | ||
| import 'package:camera_android_camerax/src/camera.dart'; | ||
| import 'package:camera_android_camerax/src/camera_selector.dart'; | ||
| import 'package:camera_android_camerax/src/preview.dart'; | ||
| import 'package:camera_android_camerax/src/process_camera_provider.dart'; | ||
| import 'package:camera_android_camerax/src/surface.dart'; | ||
| import 'package:camera_android_camerax/src/system_services.dart'; | ||
| import 'package:camera_android_camerax/src/use_case.dart'; | ||
| import 'package:camera_platform_interface/camera_platform_interface.dart'; | ||
| import 'package:flutter/widgets.dart'; | ||
| import 'package:flutter/services.dart' show DeviceOrientation; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:mockito/annotations.dart'; | ||
| import 'package:mockito/mockito.dart'; | ||
|
|
||
| import 'android_camera_camerax_test.mocks.dart'; | ||
|
|
||
| @GenerateNiceMocks(<MockSpec<Object>>[ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stuartmorgan @bparrishMines Generate mocks for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the error is about a typedef that was added on master recently, so it's not on stable. You can probably just generate the mocks on |
||
| MockSpec<BuildContext>(), | ||
| MockSpec<Camera>(), | ||
| MockSpec<CameraSelector>(), | ||
| MockSpec<Preview>(), | ||
| MockSpec<ProcessCameraProvider>(), | ||
| ]) | ||
| void main() { | ||
| TestWidgetsFlutterBinding.ensureInitialized(); | ||
|
|
||
| test( | ||
| 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID ', | ||
| () async { | ||
| final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); | ||
| const CameraLensDirection testLensDirection = CameraLensDirection.back; | ||
| const int testSensorOrientation = 90; | ||
| const CameraDescription testCameraDescription = CameraDescription( | ||
| name: 'cameraName', | ||
| lensDirection: testLensDirection, | ||
| sensorOrientation: testSensorOrientation); | ||
| const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; | ||
| const bool enableAudio = true; | ||
| const int testSurfaceTextureId = 6; | ||
|
|
||
| when(camera.testPreview.setSurfaceProvider()) | ||
| .thenAnswer((_) async => testSurfaceTextureId); | ||
|
|
||
| expect( | ||
| await camera.createCamera(testCameraDescription, testResolutionPreset, | ||
| enableAudio: enableAudio), | ||
| equals(testSurfaceTextureId)); | ||
|
|
||
| // Verify permnissions are requested and the camera starts listening for device orientation changes. | ||
| expect(camera.cameraPermissionsRequested, isTrue); | ||
| expect(camera.startedListeningForDeviceOrientationChanges, isTrue); | ||
|
|
||
| // Verify CameraSelector is set with appropriate lens direction. | ||
| expect(camera.cameraSelector, equals(camera.testCameraSelector)); | ||
|
|
||
| // Verify ProcessCameraProvider instance is received. | ||
| expect( | ||
| camera.processCameraProvider, equals(camera.testProcessCameraProvider)); | ||
|
|
||
| // Verify the camera's Preview instance is instantiated properly. | ||
| expect(camera.preview, equals(camera.testPreview)); | ||
|
|
||
| // Verify the camera's Preview instance has its surface provider set. | ||
| verify(camera.preview!.setSurfaceProvider()); | ||
| }); | ||
|
|
||
| test( | ||
| 'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| expect(() => camera.initializeCamera(3), throwsAssertionError); | ||
| }); | ||
|
|
||
| test('initializeCamera sends expected CameraInitializedEvent', () async { | ||
| final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); | ||
| final int cameraId = 10; | ||
| const CameraLensDirection testLensDirection = CameraLensDirection.back; | ||
| const int testSensorOrientation = 90; | ||
| const CameraDescription testCameraDescription = CameraDescription( | ||
| name: 'cameraName', | ||
| lensDirection: testLensDirection, | ||
| sensorOrientation: testSensorOrientation); | ||
| const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; | ||
| const bool enableAudio = true; | ||
| final int resolutionWidth = 350; | ||
| final int resolutionHeight = 750; | ||
| final Camera mockCamera = MockCamera(); | ||
| final ResolutionInfo testResolutionInfo = | ||
| ResolutionInfo(width: resolutionWidth, height: resolutionHeight); | ||
|
|
||
| // TODO(camsim99): Modify this when camera configuration is supported and defualt values no longer being used. | ||
| final CameraInitializedEvent testCameraInitializedEvent = | ||
| CameraInitializedEvent( | ||
| cameraId, | ||
| resolutionWidth.toDouble(), | ||
| resolutionHeight.toDouble(), | ||
| ExposureMode.auto, | ||
| false, | ||
| FocusMode.auto, | ||
| false); | ||
|
|
||
| // Call createCamera. | ||
| when(camera.testPreview.setSurfaceProvider()) | ||
| .thenAnswer((_) async => cameraId); | ||
| await camera.createCamera(testCameraDescription, testResolutionPreset, | ||
| enableAudio: enableAudio); | ||
|
|
||
| when(camera.testProcessCameraProvider.bindToLifecycle( | ||
| camera.cameraSelector, <UseCase>[camera.testPreview])) | ||
| .thenAnswer((_) async => mockCamera); | ||
| when(camera.testPreview.getResolutionInfo()) | ||
| .thenAnswer((_) async => testResolutionInfo); | ||
|
|
||
| // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. | ||
| camera.cameraEventStreamController.stream.listen((CameraEvent event) { | ||
| expect(event, TypeMatcher<CameraInitializedEvent>()); | ||
| expect(event, equals(testCameraInitializedEvent)); | ||
| }); | ||
|
|
||
| await camera.initializeCamera(cameraId); | ||
|
|
||
| // Verify preview was bound and unbound to get preview resolution information. | ||
| verify(camera.testProcessCameraProvider | ||
| .bindToLifecycle(camera.cameraSelector, <UseCase>[camera.testPreview])); | ||
| verify( | ||
| camera.testProcessCameraProvider.unbind(<UseCase>[camera.testPreview])); | ||
|
|
||
| // Check camera instance was received, but preview is no longer bound. | ||
| expect(camera.camera, equals(mockCamera)); | ||
| expect(camera.previewIsBound, isFalse); | ||
| }); | ||
|
|
||
| test('dispose releases Flutter surface texture and unbinds all use cases', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
|
|
||
| camera.preview = MockPreview(); | ||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
|
|
||
| camera.dispose(3); | ||
|
|
||
| verify(camera.preview!.releaseFlutterSurfaceTexture()); | ||
| verify(camera.processCameraProvider!.unbindAll()); | ||
| }); | ||
|
|
||
| test('onCameraInitialized stream emits CameraInitializedEvents', () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| final int cameraId = 16; | ||
| final Stream<CameraInitializedEvent> eventStream = | ||
| camera.onCameraInitialized(cameraId); | ||
| final StreamQueue<CameraInitializedEvent> streamQueue = | ||
| StreamQueue<CameraInitializedEvent>(eventStream); | ||
| final CameraInitializedEvent testEvent = CameraInitializedEvent( | ||
| cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); | ||
|
|
||
| camera.cameraEventStreamController.add(testEvent); | ||
|
|
||
| expect(await streamQueue.next, testEvent); | ||
| await streamQueue.cancel(); | ||
| }); | ||
|
|
||
| test('onCameraError stream emits errors caught by system services', () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| final int cameraId = 27; | ||
| final String testErrorDescription = 'Test error description!'; | ||
| final Stream<CameraErrorEvent> eventStream = camera.onCameraError(cameraId); | ||
| final StreamQueue<CameraErrorEvent> streamQueue = | ||
| StreamQueue<CameraErrorEvent>(eventStream); | ||
|
|
||
| SystemServices.cameraErrorStreamController.add(testErrorDescription); | ||
|
|
||
| expect(await streamQueue.next, | ||
| equals(CameraErrorEvent(cameraId, testErrorDescription))); | ||
| await streamQueue.cancel(); | ||
| }); | ||
|
|
||
| test( | ||
| 'onDeviceOrientationChanged stream emits changes in device oreintation detected by system services', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| final Stream<DeviceOrientationChangedEvent> eventStream = | ||
| camera.onDeviceOrientationChanged(); | ||
| final StreamQueue<DeviceOrientationChangedEvent> streamQueue = | ||
| StreamQueue<DeviceOrientationChangedEvent>(eventStream); | ||
| final DeviceOrientationChangedEvent testEvent = | ||
| DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); | ||
|
|
||
| SystemServices.deviceOrientationChangedStreamController.add(testEvent); | ||
|
|
||
| expect(await streamQueue.next, testEvent); | ||
| await streamQueue.cancel(); | ||
| }); | ||
|
|
||
| test( | ||
| 'pausePreview unbinds preview from lifecycle when preview is nonnull and has been bound to lifecycle', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.preview = MockPreview(); | ||
| camera.previewIsBound = true; | ||
|
|
||
| await camera.pausePreview(579); | ||
|
|
||
| verify(camera.processCameraProvider!.unbind(<UseCase>[camera.preview!])); | ||
| expect(camera.previewIsBound, isFalse); | ||
| }); | ||
|
|
||
| test( | ||
| 'pausePreview does not unbind preview from lifecycle when preview has not been bound to lifecycle', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.preview = MockPreview(); | ||
|
|
||
| await camera.pausePreview(632); | ||
|
|
||
| verifyNever( | ||
| camera.processCameraProvider!.unbind(<UseCase>[camera.preview!])); | ||
| }); | ||
|
|
||
| test('resumePreview does not bind preview to lifecycle if already bound', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.cameraSelector = MockCameraSelector(); | ||
| camera.preview = MockPreview(); | ||
| camera.previewIsBound = true; | ||
|
|
||
| await camera.resumePreview(78); | ||
|
|
||
| verifyNever(camera.processCameraProvider! | ||
| .bindToLifecycle(camera.cameraSelector!, <UseCase>[camera.preview!])); | ||
| }); | ||
|
|
||
| test('resumePreview binds preview to lifecycle if not already bound', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.cameraSelector = MockCameraSelector(); | ||
| camera.preview = MockPreview(); | ||
|
|
||
| await camera.resumePreview(78); | ||
|
|
||
| verify(camera.processCameraProvider! | ||
| .bindToLifecycle(camera.cameraSelector!, <UseCase>[camera.preview!])); | ||
| }); | ||
|
|
||
| test( | ||
| 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| final int textureId = 75; | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.cameraSelector = MockCameraSelector(); | ||
| camera.preview = MockPreview(); | ||
|
|
||
| FutureBuilder<void> previewWidget = | ||
| camera.buildPreview(textureId) as FutureBuilder<void>; | ||
|
|
||
| expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.nothing()), | ||
| isA<SizedBox>()); | ||
| expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.waiting()), | ||
| isA<SizedBox>()); | ||
| expect( | ||
| previewWidget.builder(MockBuildContext(), | ||
| AsyncSnapshot<void>.withData(ConnectionState.active, null)), | ||
| isA<SizedBox>()); | ||
| }); | ||
|
|
||
| test( | ||
| 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', | ||
| () async { | ||
| final AndroidCameraCameraX camera = AndroidCameraCameraX(); | ||
| final int textureId = 75; | ||
|
|
||
| camera.processCameraProvider = MockProcessCameraProvider(); | ||
| camera.cameraSelector = MockCameraSelector(); | ||
| camera.preview = MockPreview(); | ||
|
|
||
| FutureBuilder<void> previewWidget = | ||
| camera.buildPreview(textureId) as FutureBuilder<void>; | ||
|
|
||
| Texture previewTexture = previewWidget.builder(MockBuildContext(), | ||
| AsyncSnapshot<void>.withData(ConnectionState.done, null)) as Texture; | ||
| expect(previewTexture.textureId, equals(textureId)); | ||
| }); | ||
| } | ||
|
|
||
| /// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for | ||
| /// testing. | ||
| class MockAndroidCameraCamerax extends AndroidCameraCameraX { | ||
| bool cameraPermissionsRequested = false; | ||
| bool startedListeningForDeviceOrientationChanges = false; | ||
| final MockProcessCameraProvider testProcessCameraProvider = | ||
| MockProcessCameraProvider(); | ||
| final MockPreview testPreview = MockPreview(); | ||
| final MockCameraSelector testCameraSelector = MockCameraSelector(); | ||
|
|
||
| @override | ||
| Future<void> requestCameraPermissions(bool enableAudio) async { | ||
| cameraPermissionsRequested = true; | ||
| } | ||
|
|
||
| @override | ||
| void startListeningForDeviceOrientationChange( | ||
| bool cameraIsFrontFacing, int sensorOrientation) { | ||
| startedListeningForDeviceOrientationChanges = true; | ||
| return; | ||
| } | ||
|
|
||
| @override | ||
| Future<ProcessCameraProvider> getProcessCameraProviderInstance() async { | ||
| return testProcessCameraProvider; | ||
| } | ||
|
|
||
| @override | ||
| CameraSelector createCameraSelector(int cameraSelectorLensDirection) { | ||
| return testCameraSelector; | ||
| } | ||
|
|
||
| @override | ||
| Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { | ||
| return testPreview; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.