Skip to content
5 changes: 3 additions & 2 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 0.11.3

* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
* Adds support to configure persistent recording on Android. See `CameraController.startVideoRecording(enablePersistentRecording)`.
* Updates minimum supported SDK version to Flutter 3.35.0/Dart 3.9.
* Updates README to reflect that only Android API 24+ is supported.

## 0.11.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ void main() {
return completer.future;
}

testWidgets('Set description while recording', (WidgetTester tester) async {
testWidgets('Set description while recording captures full video', (
WidgetTester tester,
) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.length < 2) {
return;
Expand All @@ -269,7 +271,6 @@ void main() {
final CameraController controller = CameraController(
cameras[0],
ResolutionPreset.low,
enableAudio: false,
);

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

expect(controller.description, cameras[1]);
await tester.pumpAndSettle(const Duration(seconds: 4));

await controller.setDescription(cameras[0]);

await tester.pumpAndSettle(const Duration(seconds: 1));

final XFile file = await controller.stopVideoRecording();

final File videoFile = File(file.path);
final VideoPlayerController videoController = VideoPlayerController.file(
videoFile,
);
await videoController.initialize();
final int duration = videoController.value.duration.inMilliseconds;
await videoController.dispose();

expect(
duration,
greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds),
);
await controller.dispose();
});

testWidgets('Set description', (WidgetTester tester) async {
Expand Down
38 changes: 25 additions & 13 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,17 @@ class CameraValue {
exposurePointSupported ?? this.exposurePointSupported,
focusPointSupported: focusPointSupported ?? this.focusPointSupported,
deviceOrientation: deviceOrientation ?? this.deviceOrientation,
lockedCaptureOrientation:
lockedCaptureOrientation == null
? this.lockedCaptureOrientation
: lockedCaptureOrientation.orNull,
recordingOrientation:
recordingOrientation == null
? this.recordingOrientation
: recordingOrientation.orNull,
lockedCaptureOrientation: lockedCaptureOrientation == null
? this.lockedCaptureOrientation
: lockedCaptureOrientation.orNull,
recordingOrientation: recordingOrientation == null
? this.recordingOrientation
: recordingOrientation.orNull,
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
description: description ?? this.description,
previewPauseOrientation:
previewPauseOrientation == null
? this.previewPauseOrientation
: previewPauseOrientation.orNull,
previewPauseOrientation: previewPauseOrientation == null
? this.previewPauseOrientation
: previewPauseOrientation.orNull,
);
}

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

/// Sets the description of the camera.
///
/// On Android, you must start the recording with [startVideoRecording]
/// with `enablePersistentRecording` set to `true`
/// to avoid cancelling any active recording.
///
/// Throws a [CameraException] if setting the description fails.
Future<void> setDescription(CameraDescription description) async {
if (value.isRecordingVideo) {
Expand Down Expand Up @@ -554,8 +555,15 @@ class CameraController extends ValueNotifier<CameraValue> {
///
/// The video is returned as a [XFile] after calling [stopVideoRecording].
/// Throws a [CameraException] if the capture fails.
///
/// `enablePersistentRecording` parameter configures the recording to be a persistent recording.
/// A persistent recording can only be stopped by explicitly calling [stopVideoRecording]
/// and will ignore events that would normally cause recording to stop,
/// such as lifecycle events or explicit calls to [setDescription] while recording is in progress.
/// Currently a no-op on platforms other than Android.
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enablePersistentRecording = true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Setting enablePersistentRecording to true by default changes the existing behavior of video recording. As the documentation states, a persistent recording will ignore events that would normally stop it, such as lifecycle events. This is a behavioral breaking change for users who might be relying on the previous behavior.

For a patch release, it's best to avoid such breaking changes. I recommend changing the default value to false to make this new persistent recording behavior opt-in. This maintains backward compatibility, and developers who need to switch cameras during recording can explicitly enable it.

If you apply this change, please remember to update the integration test in packages/camera/camera/example/integration_test/camera_test.dart to explicitly call startVideoRecording(enablePersistentRecording: true) to ensure the test for the new functionality continues to pass.

Suggested change
bool enablePersistentRecording = true,
bool enablePersistentRecording = false,

}) async {
_throwIfNotInitialized('startVideoRecording');
if (value.isRecordingVideo) {
Expand All @@ -574,7 +582,11 @@ class CameraController extends ValueNotifier<CameraValue> {

try {
await CameraPlatform.instance.startVideoCapturing(
VideoCaptureOptions(_cameraId, streamCallback: streamCallback),
VideoCaptureOptions(
_cameraId,
streamCallback: streamCallback,
enablePersistentRecording: enablePersistentRecording,
),
);
value = value.copyWith(
isRecordingVideo: true,
Expand Down
39 changes: 19 additions & 20 deletions packages/camera/camera/lib/src/camera_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,23 @@ class CameraPreview extends StatelessWidget {
Widget build(BuildContext context) {
return controller.value.isInitialized
? ValueListenableBuilder<CameraValue>(
valueListenable: controller,
builder: (BuildContext context, Object? value, Widget? child) {
return AspectRatio(
aspectRatio:
_isLandscape()
? controller.value.aspectRatio
: (1 / controller.value.aspectRatio),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_wrapInRotatedBox(child: controller.buildPreview()),
child ?? Container(),
],
),
);
},
child: child,
)
valueListenable: controller,
builder: (BuildContext context, Object? value, Widget? child) {
return AspectRatio(
aspectRatio: _isLandscape()
? controller.value.aspectRatio
: (1 / controller.value.aspectRatio),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_wrapInRotatedBox(child: controller.buildPreview()),
child ?? Container(),
],
),
);
},
child: child,
)
: Container();
}

Expand Down Expand Up @@ -73,7 +72,7 @@ class CameraPreview extends StatelessWidget {
return controller.value.isRecordingVideo
? controller.value.recordingOrientation!
: (controller.value.previewPauseOrientation ??
controller.value.lockedCaptureOrientation ??
controller.value.deviceOrientation);
controller.value.lockedCaptureOrientation ??
controller.value.deviceOrientation);
}
}
10 changes: 5 additions & 5 deletions packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ description: A Flutter plugin for controlling the camera. Supports previewing
Dart.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.11.2
version: 0.11.3

environment:
sdk: ^3.7.0
flutter: ">=3.29.0"
sdk: ^3.9.0
flutter: ">=3.35.0"

flutter:
plugin:
Expand All @@ -21,9 +21,9 @@ flutter:
default_package: camera_web

dependencies:
camera_android_camerax: ^0.6.13
camera_android_camerax: ^0.6.22
camera_avfoundation: ^0.9.18
camera_platform_interface: ^2.10.0
camera_platform_interface: ^2.11.0
camera_web: ^0.3.3
flutter:
sdk: flutter
Expand Down
1 change: 1 addition & 0 deletions packages/camera/camera/test/camera_preview_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class FakeController extends ValueNotifier<CameraValue>
@override
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enablePersistentRecording = true,
}) async {}

@override
Expand Down
14 changes: 6 additions & 8 deletions packages/camera/camera/test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1838,10 +1838,9 @@ class MockCameraPlatform extends Mock
Future<int> createCameraWithSettings(
CameraDescription cameraDescription,
MediaSettings? mediaSettings,
) =>
mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<int>.value(mockInitializeCamera);
) => mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<int>.value(mockInitializeCamera);

@override
Future<int> createCamera(
Expand Down Expand Up @@ -1869,10 +1868,9 @@ class MockCameraPlatform extends Mock
);

@override
Future<XFile> takePicture(int cameraId) =>
mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<XFile>.value(mockTakePicture);
Future<XFile> takePicture(int cameraId) => mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<XFile>.value(mockTakePicture);

@override
Future<void> prepareForVideoRecording() async =>
Expand Down