Skip to content

Commit 1715f0e

Browse files
[camera] Implement setDescription with android camerax (#10117)
setDescription allows switching camera while a video recording is in progress. This feature was only working with camera2 setup, but the camera plugin uses camerax by default. So, this PR includes update to address the issue. Fixes [#148013](flutter/flutter#148013) ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent c8ba0cc commit 1715f0e

File tree

7 files changed

+85
-49
lines changed

7 files changed

+85
-49
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.11.3
2+
3+
* Adds support to configure persistent recording on Android. See `CameraController.startVideoRecording(enablePersistentRecording)`.
4+
* Updates minimum supported SDK version to Flutter 3.35.0/Dart 3.9.
5+
16
## 0.11.2+1
27

38
* Updates examples to use the new RadioGroup API instead of deprecated Radio parameters.

packages/camera/camera/example/integration_test/camera_test.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,9 @@ void main() {
260260
return completer.future;
261261
}
262262

263-
testWidgets('Set description while recording', (WidgetTester tester) async {
263+
testWidgets('Set description while recording captures full video', (
264+
WidgetTester tester,
265+
) async {
264266
final List<CameraDescription> cameras = await availableCameras();
265267
if (cameras.length < 2) {
266268
return;
@@ -269,7 +271,6 @@ void main() {
269271
final CameraController controller = CameraController(
270272
cameras[0],
271273
ResolutionPreset.low,
272-
enableAudio: false,
273274
);
274275

275276
await controller.initialize();
@@ -278,7 +279,27 @@ void main() {
278279
await controller.startVideoRecording();
279280
await controller.setDescription(cameras[1]);
280281

281-
expect(controller.description, cameras[1]);
282+
await tester.pumpAndSettle(const Duration(seconds: 4));
283+
284+
await controller.setDescription(cameras[0]);
285+
286+
await tester.pumpAndSettle(const Duration(seconds: 1));
287+
288+
final XFile file = await controller.stopVideoRecording();
289+
290+
final File videoFile = File(file.path);
291+
final VideoPlayerController videoController = VideoPlayerController.file(
292+
videoFile,
293+
);
294+
await videoController.initialize();
295+
final int duration = videoController.value.duration.inMilliseconds;
296+
await videoController.dispose();
297+
298+
expect(
299+
duration,
300+
greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds),
301+
);
302+
await controller.dispose();
282303
});
283304

284305
testWidgets('Set description', (WidgetTester tester) async {

packages/camera/camera/lib/src/camera_controller.dart

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,20 +187,17 @@ class CameraValue {
187187
exposurePointSupported ?? this.exposurePointSupported,
188188
focusPointSupported: focusPointSupported ?? this.focusPointSupported,
189189
deviceOrientation: deviceOrientation ?? this.deviceOrientation,
190-
lockedCaptureOrientation:
191-
lockedCaptureOrientation == null
192-
? this.lockedCaptureOrientation
193-
: lockedCaptureOrientation.orNull,
194-
recordingOrientation:
195-
recordingOrientation == null
196-
? this.recordingOrientation
197-
: recordingOrientation.orNull,
190+
lockedCaptureOrientation: lockedCaptureOrientation == null
191+
? this.lockedCaptureOrientation
192+
: lockedCaptureOrientation.orNull,
193+
recordingOrientation: recordingOrientation == null
194+
? this.recordingOrientation
195+
: recordingOrientation.orNull,
198196
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
199197
description: description ?? this.description,
200-
previewPauseOrientation:
201-
previewPauseOrientation == null
202-
? this.previewPauseOrientation
203-
: previewPauseOrientation.orNull,
198+
previewPauseOrientation: previewPauseOrientation == null
199+
? this.previewPauseOrientation
200+
: previewPauseOrientation.orNull,
204201
);
205202
}
206203

@@ -439,6 +436,10 @@ class CameraController extends ValueNotifier<CameraValue> {
439436

440437
/// Sets the description of the camera.
441438
///
439+
/// On Android, you must start the recording with [startVideoRecording]
440+
/// with `enablePersistentRecording` set to `true`
441+
/// to avoid cancelling any active recording.
442+
///
442443
/// Throws a [CameraException] if setting the description fails.
443444
Future<void> setDescription(CameraDescription description) async {
444445
if (value.isRecordingVideo) {
@@ -554,8 +555,15 @@ class CameraController extends ValueNotifier<CameraValue> {
554555
///
555556
/// The video is returned as a [XFile] after calling [stopVideoRecording].
556557
/// Throws a [CameraException] if the capture fails.
558+
///
559+
/// `enablePersistentRecording` parameter configures the recording to be a persistent recording.
560+
/// A persistent recording can only be stopped by explicitly calling [stopVideoRecording]
561+
/// and will ignore events that would normally cause recording to stop,
562+
/// such as lifecycle events or explicit calls to [setDescription] while recording is in progress.
563+
/// Currently a no-op on platforms other than Android.
557564
Future<void> startVideoRecording({
558565
onLatestImageAvailable? onAvailable,
566+
bool enablePersistentRecording = true,
559567
}) async {
560568
_throwIfNotInitialized('startVideoRecording');
561569
if (value.isRecordingVideo) {
@@ -574,7 +582,11 @@ class CameraController extends ValueNotifier<CameraValue> {
574582

575583
try {
576584
await CameraPlatform.instance.startVideoCapturing(
577-
VideoCaptureOptions(_cameraId, streamCallback: streamCallback),
585+
VideoCaptureOptions(
586+
_cameraId,
587+
streamCallback: streamCallback,
588+
enablePersistentRecording: enablePersistentRecording,
589+
),
578590
);
579591
value = value.copyWith(
580592
isRecordingVideo: true,

packages/camera/camera/lib/src/camera_preview.dart

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,23 @@ class CameraPreview extends StatelessWidget {
2323
Widget build(BuildContext context) {
2424
return controller.value.isInitialized
2525
? ValueListenableBuilder<CameraValue>(
26-
valueListenable: controller,
27-
builder: (BuildContext context, Object? value, Widget? child) {
28-
return AspectRatio(
29-
aspectRatio:
30-
_isLandscape()
31-
? controller.value.aspectRatio
32-
: (1 / controller.value.aspectRatio),
33-
child: Stack(
34-
fit: StackFit.expand,
35-
children: <Widget>[
36-
_wrapInRotatedBox(child: controller.buildPreview()),
37-
child ?? Container(),
38-
],
39-
),
40-
);
41-
},
42-
child: child,
43-
)
26+
valueListenable: controller,
27+
builder: (BuildContext context, Object? value, Widget? child) {
28+
return AspectRatio(
29+
aspectRatio: _isLandscape()
30+
? controller.value.aspectRatio
31+
: (1 / controller.value.aspectRatio),
32+
child: Stack(
33+
fit: StackFit.expand,
34+
children: <Widget>[
35+
_wrapInRotatedBox(child: controller.buildPreview()),
36+
child ?? Container(),
37+
],
38+
),
39+
);
40+
},
41+
child: child,
42+
)
4443
: Container();
4544
}
4645

@@ -73,7 +72,7 @@ class CameraPreview extends StatelessWidget {
7372
return controller.value.isRecordingVideo
7473
? controller.value.recordingOrientation!
7574
: (controller.value.previewPauseOrientation ??
76-
controller.value.lockedCaptureOrientation ??
77-
controller.value.deviceOrientation);
75+
controller.value.lockedCaptureOrientation ??
76+
controller.value.deviceOrientation);
7877
}
7978
}

packages/camera/camera/pubspec.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ description: A Flutter plugin for controlling the camera. Supports previewing
44
Dart.
55
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
7-
version: 0.11.2+1
7+
version: 0.11.3
88

99
environment:
10-
sdk: ^3.7.0
11-
flutter: ">=3.29.0"
10+
sdk: ^3.9.0
11+
flutter: ">=3.35.0"
1212

1313
flutter:
1414
plugin:
@@ -21,9 +21,9 @@ flutter:
2121
default_package: camera_web
2222

2323
dependencies:
24-
camera_android_camerax: ^0.6.13
24+
camera_android_camerax: ^0.6.22
2525
camera_avfoundation: ^0.9.18
26-
camera_platform_interface: ^2.10.0
26+
camera_platform_interface: ^2.11.0
2727
camera_web: ^0.3.3
2828
flutter:
2929
sdk: flutter

packages/camera/camera/test/camera_preview_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class FakeController extends ValueNotifier<CameraValue>
112112
@override
113113
Future<void> startVideoRecording({
114114
onLatestImageAvailable? onAvailable,
115+
bool enablePersistentRecording = true,
115116
}) async {}
116117

117118
@override

packages/camera/camera/test/camera_test.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,9 @@ class MockCameraPlatform extends Mock
18381838
Future<int> createCameraWithSettings(
18391839
CameraDescription cameraDescription,
18401840
MediaSettings? mediaSettings,
1841-
) =>
1842-
mockPlatformException
1843-
? throw PlatformException(code: 'foo', message: 'bar')
1844-
: Future<int>.value(mockInitializeCamera);
1841+
) => mockPlatformException
1842+
? throw PlatformException(code: 'foo', message: 'bar')
1843+
: Future<int>.value(mockInitializeCamera);
18451844

18461845
@override
18471846
Future<int> createCamera(
@@ -1869,10 +1868,9 @@ class MockCameraPlatform extends Mock
18691868
);
18701869

18711870
@override
1872-
Future<XFile> takePicture(int cameraId) =>
1873-
mockPlatformException
1874-
? throw PlatformException(code: 'foo', message: 'bar')
1875-
: Future<XFile>.value(mockTakePicture);
1871+
Future<XFile> takePicture(int cameraId) => mockPlatformException
1872+
? throw PlatformException(code: 'foo', message: 'bar')
1873+
: Future<XFile>.value(mockTakePicture);
18761874

18771875
@override
18781876
Future<void> prepareForVideoRecording() async =>

0 commit comments

Comments
 (0)