Skip to content

Commit

Permalink
[camerax] Implement startVideoCapturing and onVideoRecordedEvent (f…
Browse files Browse the repository at this point in the history
…lutter#4815)

Implements `startVideoCapturing` (with the image streaming option, others currently unsupported) and `onVideoRecordedEvent` (empty implementation; same as the other plugins).

Fixes flutter/flutter#126477.
Fixes flutter/flutter#127896.
  • Loading branch information
camsim99 authored Sep 12, 2023
1 parent e04ba88 commit bec74e0
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 14 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+18

* Implements `startVideoCapturing`.

## 0.5.0+17

* Implements resolution configuration for all camera use cases.
Expand Down
8 changes: 5 additions & 3 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ Calling `setFlashMode` with mode `FlashMode.torch` currently does nothing.

`setZoomLevel` is unimplemented.

### Some video capture functionality \[[Issue #127896][127896], [Issue #126477][126477]\]
### Setting maximum duration and stream options for video capture

`startVideoCapturing` is unimplemented; use `startVideoRecording` instead.
`onVideoRecordedEvent` is also unimplemented.
Calling `startVideoCapturing` with `VideoCaptureOptions` configured with
`maxVideoDuration` and `streamOptions` is currently unsupported do to the
limitations of the CameraX library and the platform interface, respectively,
and thus, those parameters will silently be ignored.

## Contributing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ class AndroidCameraCameraX extends CameraPlatform {
]);
}

/// The camera finished recording a video.
@override
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
return _cameraEvents(cameraId).whereType<VideoRecordedEvent>();
}

/// Gets the minimum supported exposure offset for the selected camera in EV units.
///
/// [cameraId] not used.
Expand Down Expand Up @@ -507,12 +513,23 @@ class AndroidCameraCameraX extends CameraPlatform {
/// Note that the preset resolution is used to configure the recording, but
/// 240p ([ResolutionPreset.low]) is unsupported and will fallback to
/// configure the recording as the next highest available quality.
///
/// This method is deprecated in favour of [startVideoCapturing].
@override
Future<void> startVideoRecording(int cameraId,
{Duration? maxVideoDuration}) async {
assert(cameraSelector != null);
assert(processCameraProvider != null);
return startVideoCapturing(
VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration));
}

/// Starts a video recording and/or streaming session.
///
/// Please see [VideoCaptureOptions] for documentation on the
/// configuration options. Currently, maxVideoDuration and streamOptions
/// are unsupported due to the limitations of CameraX and the platform
/// interface, respectively.
@override
Future<void> startVideoCapturing(VideoCaptureOptions options) async {
if (recording != null) {
// There is currently an active recording, so do not start a new one.
return;
Expand All @@ -527,6 +544,10 @@ class AndroidCameraCameraX extends CameraPlatform {
await SystemServices.getTempFilePath(videoPrefix, '.temp');
pendingRecording = await recorder!.prepareRecording(videoOutputPath!);
recording = await pendingRecording!.start();

if (options.streamCallback != null) {
onStreamedFrameAvailable(options.cameraId).listen(options.streamCallback);
}
}

/// Stops the video recording and returns the file where it was saved.
Expand Down
4 changes: 2 additions & 2 deletions 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+17
version: 0.5.0+18

environment:
sdk: ">=2.19.0 <4.0.0"
Expand All @@ -19,7 +19,7 @@ flutter:

dependencies:
async: ^2.5.0
camera_platform_interface: ^2.2.0
camera_platform_interface: ^2.3.2
flutter:
sdk: flutter
integration_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -785,9 +785,9 @@ void main() {

group('video recording', () {
test(
'startVideoRecording binds video capture use case and starts the recording',
'startVideoCapturing binds video capture use case and starts the recording',
() async {
//Set up mocks and constants.
// Set up mocks and constants.
final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
Expand Down Expand Up @@ -815,7 +815,7 @@ void main() {
camera.cameraSelector!, <UseCase>[camera.videoCapture!]))
.thenAnswer((_) async => camera.camera!);

await camera.startVideoRecording(cameraId);
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));

verify(camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!, <UseCase>[camera.videoCapture!]));
Expand All @@ -824,9 +824,9 @@ void main() {
});

test(
'startVideoRecording binds video capture use case and starts the recording'
'startVideoCapturing binds video capture use case and starts the recording'
' on first call, and does nothing on second call', () async {
//Set up mocks and constants.
// Set up mocks and constants.
final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
Expand Down Expand Up @@ -854,14 +854,14 @@ void main() {
camera.cameraSelector!, <UseCase>[camera.videoCapture!]))
.thenAnswer((_) async => camera.camera!);

await camera.startVideoRecording(cameraId);
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));

verify(camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!, <UseCase>[camera.videoCapture!]));
expect(camera.pendingRecording, equals(mockPendingRecording));
expect(camera.recording, mockRecording);

await camera.startVideoRecording(cameraId);
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
// Verify that each of these calls happened only once.
verify(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
.called(1);
Expand All @@ -872,6 +872,55 @@ void main() {
verifyNoMoreInteractions(mockPendingRecording);
});

test(
'startVideoCapturing called with stream options starts image streaming',
() async {
// Set up mocks and constants.
final FakeAndroidCameraCameraX camera =
FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true);
final MockProcessCameraProvider mockProcessCameraProvider =
MockProcessCameraProvider();
camera.processCameraProvider = mockProcessCameraProvider;
camera.cameraSelector = MockCameraSelector();
camera.recorder = camera.testRecorder;
camera.videoCapture = camera.testVideoCapture;
camera.imageAnalysis = camera.testImageAnalysis;
camera.camera = MockCamera();
final MockPendingRecording mockPendingRecording = MockPendingRecording();
final TestSystemServicesHostApi mockSystemServicesApi =
MockTestSystemServicesHostApi();
TestSystemServicesHostApi.setup(mockSystemServicesApi);

const int cameraId = 17;
const String outputPath = '/temp/MOV123.temp';
final Completer<CameraImageData> imageDataCompleter =
Completer<CameraImageData>();
final VideoCaptureOptions videoCaptureOptions = VideoCaptureOptions(
cameraId,
streamCallback: (CameraImageData imageData) =>
imageDataCompleter.complete(imageData));

// Mock method calls.
when(camera.processCameraProvider!.isBound(camera.videoCapture!))
.thenAnswer((_) async => true);
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
.thenReturn(outputPath);
when(camera.testRecorder.prepareRecording(outputPath))
.thenAnswer((_) async => mockPendingRecording);
when(mockProcessCameraProvider.bindToLifecycle(any, any))
.thenAnswer((_) => Future<Camera>.value(camera.camera));
when(camera.camera!.getCameraInfo())
.thenAnswer((_) => Future<CameraInfo>.value(MockCameraInfo()));

await camera.startVideoCapturing(videoCaptureOptions);

final CameraImageData mockCameraImageData = MockCameraImageData();
camera.cameraImageDataStreamController!.add(mockCameraImageData);

expect(imageDataCompleter.future, isNotNull);
await camera.cameraImageDataStreamController!.close();
});

test('pauseVideoRecording pauses the recording', () async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
final MockRecording recording = MockRecording();
Expand Down

0 comments on commit bec74e0

Please sign in to comment.