Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.6.26+4

* Removes restrictions against concurrent `UseCase`s that are now allowed.

## 0.6.26+3

* Bumps kotlin_version to 2.3.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1123,48 +1123,6 @@ class AndroidCameraCameraX extends CameraPlatform {
return;
}

dynamic Function(CameraImageData)? streamCallback = options.streamCallback;
if (!_previewIsPaused) {
// The plugin binds the preview use case to the camera lifecycle when
// createCamera is called, but camera use cases can become limited
// when video recording and displaying a preview concurrently. This logic
// will prioritize attempting to continue displaying the preview,
// stream images, and record video if specified and supported. Otherwise,
// the preview must be paused in order to allow those concurrently. See
// https://developer.android.com/media/camera/camerax/architecture#combine-use-cases
// for more information on supported concurrent camera use cases.
final camera2CameraInfo = Camera2CameraInfo.from(cameraInfo: cameraInfo!);
final cameraInfoSupportedHardwareLevel =
(await camera2CameraInfo.getCameraCharacteristic(
CameraCharacteristics.infoSupportedHardwareLevel,
))!
as InfoSupportedHardwareLevel;

// Handle limited level device restrictions:
final cameraSupportsConcurrentImageCapture =
cameraInfoSupportedHardwareLevel != InfoSupportedHardwareLevel.legacy;
if (!cameraSupportsConcurrentImageCapture) {
// Concurrent preview + video recording + image capture is not supported
// unless the camera device is cameraSupportsHardwareLevelLimited or
// better.
await _unbindUseCaseFromLifecycle(imageCapture!);
}

// Handle level 3 device restrictions:
final cameraSupportsHardwareLevel3 =
cameraInfoSupportedHardwareLevel == InfoSupportedHardwareLevel.level3;
if (!cameraSupportsHardwareLevel3 || streamCallback == null) {
// Concurrent preview + video recording + image streaming is not supported
// unless the camera device is cameraSupportsHardwareLevel3 or better.
streamCallback = null;
await _unbindUseCaseFromLifecycle(imageAnalysis!);
} else {
// If image streaming concurrently with video recording, image capture
// is unsupported.
await _unbindUseCaseFromLifecycle(imageCapture!);
}
}

await _bindUseCaseToLifecycle(videoCapture!, options.cameraId);

// Set target rotation to default CameraX rotation only if capture
Expand Down Expand Up @@ -1194,8 +1152,8 @@ class AndroidCameraCameraX extends CameraPlatform {

recording = await pendingRecording!.start(_videoRecordingEventListener);

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

// Wait for video recording to start.
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.6.26+3
version: 0.6.26+4

environment:
sdk: ^3.9.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6609,231 +6609,6 @@ void main() {
},
);

test(
'startVideoCapturing does not unbind ImageCapture or ImageAnalysis use cases when preview is paused',
() async {
// Set up mocks and constants.
final camera = AndroidCameraCameraX();
final mockPendingRecording = MockPendingRecording();
final mockRecording = MockRecording();
final mockCamera = MockCamera();
final mockCameraInfo = MockCameraInfo();
final mockCamera2CameraInfo = MockCamera2CameraInfo();

// Set directly for test versus calling createCamera.
camera.processCameraProvider = MockProcessCameraProvider();
camera.recorder = MockRecorder();
camera.videoCapture = MockVideoCapture();
camera.cameraSelector = MockCameraSelector();
camera.cameraInfo = MockCameraInfo();
camera.imageAnalysis = MockImageAnalysis();
camera.imageCapture = MockImageCapture();
camera.preview = MockPreview();
camera.enableRecordingAudio = false;

// Ignore setting target rotation for this test; tested seprately.
camera.captureOrientationLocked = true;

// Tell plugin to create detached Observer when camera info updated.
const outputPath = '/temp/REC123.mp4';
PigeonOverrides.analyzer_new =
({required void Function(Analyzer, ImageProxy) analyze}) {
return Analyzer.pigeon_detached(analyze: analyze);
};
GenericsPigeonOverrides.observerNew =
<T>({required void Function(Observer<T>, T) onChanged}) {
return Observer<T>.detached(onChanged: onChanged);
};
PigeonOverrides.camera2CameraInfo_from =
({required dynamic cameraInfo}) => mockCamera2CameraInfo;
PigeonOverrides.systemServicesManager_new =
({
required void Function(SystemServicesManager, String) onCameraError,
}) {
final mockSystemServicesManager = MockSystemServicesManager();
when(
mockSystemServicesManager.getTempFilePath(
camera.videoPrefix,
'.mp4',
),
).thenAnswer((_) async => outputPath);
return mockSystemServicesManager;
};
PigeonOverrides.videoRecordEventListener_new =
({
required void Function(VideoRecordEventListener, VideoRecordEvent)
onEvent,
}) {
return VideoRecordEventListener.pigeon_detached(onEvent: onEvent);
};
PigeonOverrides.cameraCharacteristics_infoSupportedHardwareLevel =
MockCameraCharacteristicsKey();

const cameraId = 97;

// Mock method calls.
when(
camera.recorder!.prepareRecording(outputPath),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.asPersistentRecording(),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.start(any),
).thenAnswer((_) async => mockRecording);
when(
camera.processCameraProvider!.isBound(camera.videoCapture!),
).thenAnswer((_) async => false);
when(
camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!,
<UseCase>[camera.videoCapture!],
),
).thenAnswer((_) async => mockCamera);
when(
mockCamera.getCameraInfo(),
).thenAnswer((_) => Future<CameraInfo>.value(mockCameraInfo));
when(
mockCameraInfo.getCameraState(),
).thenAnswer((_) async => MockLiveCameraState());

await camera.pausePreview(cameraId);

// Simulate video recording being started so startVideoRecording completes.
AndroidCameraCameraX.videoRecordingEventStreamController.add(
VideoRecordEventStart.pigeon_detached(),
);

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

verifyNever(
camera.processCameraProvider!.unbind(<UseCase>[camera.imageCapture!]),
);
verifyNever(
camera.processCameraProvider!.unbind(<UseCase>[camera.imageAnalysis!]),
);
},
);

test(
'startVideoCapturing unbinds ImageCapture and ImageAnalysis use cases when running on a legacy hardware device',
() async {
// Set up mocks and constants.
final camera = AndroidCameraCameraX();
final mockPendingRecording = MockPendingRecording();
final mockRecording = MockRecording();
final mockCamera = MockCamera();
final mockCameraInfo = MockCameraInfo();
final mockCamera2CameraInfo = MockCamera2CameraInfo();

// Set directly for test versus calling createCamera.
camera.processCameraProvider = MockProcessCameraProvider();
camera.recorder = MockRecorder();
camera.videoCapture = MockVideoCapture();
camera.cameraSelector = MockCameraSelector();
camera.cameraInfo = MockCameraInfo();
camera.imageAnalysis = MockImageAnalysis();
camera.imageCapture = MockImageCapture();
camera.preview = MockPreview();
camera.enableRecordingAudio = true;

// Ignore setting target rotation for this test; tested seprately.
camera.captureOrientationLocked = true;

// Tell plugin to create detached Observer when camera info updated.
const outputPath = '/temp/REC123.mp4';
PigeonOverrides.analyzer_new =
({required void Function(Analyzer, ImageProxy) analyze}) {
return Analyzer.pigeon_detached(analyze: analyze);
};
GenericsPigeonOverrides.observerNew =
<T>({required void Function(Observer<T>, T) onChanged}) {
return Observer<T>.detached(onChanged: onChanged);
};
PigeonOverrides.camera2CameraInfo_from =
({required dynamic cameraInfo}) => mockCamera2CameraInfo;
PigeonOverrides.systemServicesManager_new =
({
required void Function(SystemServicesManager, String) onCameraError,
}) {
final mockSystemServicesManager = MockSystemServicesManager();
when(
mockSystemServicesManager.getTempFilePath(
camera.videoPrefix,
'.mp4',
),
).thenAnswer((_) async => outputPath);
return mockSystemServicesManager;
};
PigeonOverrides.videoRecordEventListener_new =
({
required void Function(VideoRecordEventListener, VideoRecordEvent)
onEvent,
}) {
return VideoRecordEventListener.pigeon_detached(onEvent: onEvent);
};
PigeonOverrides.cameraCharacteristics_infoSupportedHardwareLevel =
MockCameraCharacteristicsKey();

const cameraId = 44;

// Mock method calls.
when(
camera.recorder!.prepareRecording(outputPath),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.asPersistentRecording(),
).thenAnswer((_) async => mockPendingRecording);
when(
mockPendingRecording.start(any),
).thenAnswer((_) async => mockRecording);
when(
camera.processCameraProvider!.isBound(camera.videoCapture!),
).thenAnswer((_) async => false);
when(
camera.processCameraProvider!.isBound(camera.imageCapture!),
).thenAnswer((_) async => true);
when(
camera.processCameraProvider!.isBound(camera.imageAnalysis!),
).thenAnswer((_) async => true);
when(
camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!,
<UseCase>[camera.videoCapture!],
),
).thenAnswer((_) async => mockCamera);
when(
mockCamera.getCameraInfo(),
).thenAnswer((_) => Future<CameraInfo>.value(mockCameraInfo));
when(
mockCameraInfo.getCameraState(),
).thenAnswer((_) async => MockLiveCameraState());
when(
mockCamera2CameraInfo.getCameraCharacteristic(any),
).thenAnswer((_) async => InfoSupportedHardwareLevel.legacy);

// Simulate video recording being started so startVideoRecording completes.
AndroidCameraCameraX.videoRecordingEventStreamController.add(
VideoRecordEventStart.pigeon_detached(),
);

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

verify(
camera.processCameraProvider!.unbind(<UseCase>[camera.imageCapture!]),
);
verify(
camera.processCameraProvider!.unbind(<UseCase>[camera.imageAnalysis!]),
);
},
);

test(
'prepareForVideoRecording does not make any calls involving starting video recording',
() async {
Expand Down
Loading