[camera] Reland implementation of flip camera while recording. App facing changes (#3496)
[camera] Reland implementation of flip camera while recording. App facing changes
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index f68d6e3..1a783b1 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 0.10.4
+* Allows camera to be switched while video recording.
* Updates minimum Flutter version to 3.3.
* Aligns Dart and Flutter SDK constraints.
diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md
index db70831..095da3c 100644
--- a/packages/camera/camera/README.md
+++ b/packages/camera/camera/README.md
@@ -70,7 +70,7 @@
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
- onNewCameraSelected(cameraController.description);
+ _initializeCameraController(cameraController.description);
}
}
```
diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart
index f0cc67f..6bef30e 100644
--- a/packages/camera/camera/example/integration_test/camera_test.dart
+++ b/packages/camera/camera/example/integration_test/camera_test.dart
@@ -265,6 +265,45 @@
return completer.future;
}
+ testWidgets('Set description while recording', (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.length < 2) {
+ return;
+ }
+
+ final CameraController controller = CameraController(
+ cameras[0],
+ ResolutionPreset.low,
+ enableAudio: false,
+ );
+
+ await controller.initialize();
+ await controller.prepareForVideoRecording();
+
+ await controller.startVideoRecording();
+ await controller.setDescription(cameras[1]);
+
+ expect(controller.description, cameras[1]);
+ });
+
+ testWidgets('Set description', (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.length < 2) {
+ return;
+ }
+
+ final CameraController controller = CameraController(
+ cameras[0],
+ ResolutionPreset.low,
+ enableAudio: false,
+ );
+
+ await controller.initialize();
+ await controller.setDescription(cameras[1]);
+
+ expect(controller.description, cameras[1]);
+ });
+
testWidgets(
'iOS image streaming with imageFormatGroup',
(WidgetTester tester) async {
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
index 2fa2ae6..73c9f05 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -120,7 +120,7 @@
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
- onNewCameraSelected(cameraController.description);
+ _initializeCameraController(cameraController.description);
}
}
// #enddocregion AppLifecycle
@@ -597,10 +597,7 @@
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
groupValue: controller?.description,
value: cameraDescription,
- onChanged:
- controller != null && controller!.value.isRecordingVideo
- ? null
- : onChanged,
+ onChanged: onChanged,
),
),
);
@@ -633,17 +630,15 @@
}
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
- final CameraController? oldController = controller;
- if (oldController != null) {
- // `controller` needs to be set to null before getting disposed,
- // to avoid a race condition when we use the controller that is being
- // disposed. This happens when camera permission dialog shows up,
- // which triggers `didChangeAppLifecycleState`, which disposes and
- // re-creates the controller.
- controller = null;
- await oldController.dispose();
+ if (controller != null) {
+ return controller!.setDescription(cameraDescription);
+ } else {
+ return _initializeCameraController(cameraDescription);
}
+ }
+ Future<void> _initializeCameraController(
+ CameraDescription cameraDescription) async {
final CameraController cameraController = CameraController(
cameraDescription,
kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart
index 7a396c1..69917d3 100644
--- a/packages/camera/camera/lib/src/camera_controller.dart
+++ b/packages/camera/camera/lib/src/camera_controller.dart
@@ -49,6 +49,7 @@
required this.exposurePointSupported,
required this.focusPointSupported,
required this.deviceOrientation,
+ required this.description,
this.lockedCaptureOrientation,
this.recordingOrientation,
this.isPreviewPaused = false,
@@ -56,7 +57,7 @@
}) : _isRecordingPaused = isRecordingPaused;
/// Creates a new camera controller state for an uninitialized controller.
- const CameraValue.uninitialized()
+ const CameraValue.uninitialized(CameraDescription description)
: this(
isInitialized: false,
isRecordingVideo: false,
@@ -70,6 +71,7 @@
focusPointSupported: false,
deviceOrientation: DeviceOrientation.portraitUp,
isPreviewPaused: false,
+ description: description,
);
/// True after [CameraController.initialize] has completed successfully.
@@ -143,6 +145,9 @@
/// The orientation of the currently running video recording.
final DeviceOrientation? recordingOrientation;
+ /// The properties of the camera device controlled by this controller.
+ final CameraDescription description;
+
/// Creates a modified copy of the object.
///
/// Explicitly specified fields get the specified value, all other fields get
@@ -164,6 +169,7 @@
Optional<DeviceOrientation>? lockedCaptureOrientation,
Optional<DeviceOrientation>? recordingOrientation,
bool? isPreviewPaused,
+ CameraDescription? description,
Optional<DeviceOrientation>? previewPauseOrientation,
}) {
return CameraValue(
@@ -188,6 +194,7 @@
? this.recordingOrientation
: recordingOrientation.orNull,
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
+ description: description ?? this.description,
previewPauseOrientation: previewPauseOrientation == null
? this.previewPauseOrientation
: previewPauseOrientation.orNull,
@@ -211,7 +218,8 @@
'lockedCaptureOrientation: $lockedCaptureOrientation, '
'recordingOrientation: $recordingOrientation, '
'isPreviewPaused: $isPreviewPaused, '
- 'previewPausedOrientation: $previewPauseOrientation)';
+ 'previewPausedOrientation: $previewPauseOrientation, '
+ 'description: $description)';
}
}
@@ -225,14 +233,14 @@
class CameraController extends ValueNotifier<CameraValue> {
/// Creates a new camera controller in an uninitialized state.
CameraController(
- this.description,
+ CameraDescription description,
this.resolutionPreset, {
this.enableAudio = true,
this.imageFormatGroup,
- }) : super(const CameraValue.uninitialized());
+ }) : super(CameraValue.uninitialized(description));
/// The properties of the camera device controlled by this controller.
- final CameraDescription description;
+ CameraDescription get description => value.description;
/// The resolution this controller is targeting.
///
@@ -274,7 +282,12 @@
/// Initializes the camera on the device.
///
/// Throws a [CameraException] if the initialization fails.
- Future<void> initialize() async {
+ Future<void> initialize() => _initializeWithDescription(description);
+
+ /// Initializes the camera on the device with the specified description.
+ ///
+ /// Throws a [CameraException] if the initialization fails.
+ Future<void> _initializeWithDescription(CameraDescription description) async {
if (_isDisposed) {
throw CameraException(
'Disposed CameraController',
@@ -313,6 +326,7 @@
value = value.copyWith(
isInitialized: true,
+ description: description,
previewSize: await initializeCompleter.future
.then((CameraInitializedEvent event) => Size(
event.previewWidth,
@@ -380,6 +394,18 @@
}
}
+ /// Sets the description of the camera.
+ ///
+ /// Throws a [CameraException] if setting the description fails.
+ Future<void> setDescription(CameraDescription description) async {
+ if (value.isRecordingVideo) {
+ await CameraPlatform.instance.setDescriptionWhileRecording(description);
+ value = value.copyWith(description: description);
+ } else {
+ await _initializeWithDescription(description);
+ }
+ }
+
/// Captures an image and returns the file where it was saved.
///
/// Throws a [CameraException] if the capture fails.
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index f2196e7..839f064 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
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.10.3+2
+version: 0.10.4
environment:
sdk: ">=2.18.0 <4.0.0"
@@ -21,9 +21,9 @@
default_package: camera_web
dependencies:
- camera_android: ^0.10.1
- camera_avfoundation: ^0.9.9
- camera_platform_interface: ^2.3.2
+ camera_android: ^0.10.5
+ camera_avfoundation: ^0.9.13
+ camera_platform_interface: ^2.4.0
camera_web: ^0.3.1
flutter:
sdk: flutter
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
index 6677fcf..c73e181 100644
--- a/packages/camera/camera/test/camera_preview_test.dart
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -11,7 +11,10 @@
class FakeController extends ValueNotifier<CameraValue>
implements CameraController {
- FakeController() : super(const CameraValue.uninitialized());
+ FakeController() : super(const CameraValue.uninitialized(fakeDescription));
+
+ static const CameraDescription fakeDescription = CameraDescription(
+ name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
@override
Future<void> dispose() async {
@@ -30,10 +33,6 @@
void debugCheckIsDisposed() {}
@override
- CameraDescription get description => const CameraDescription(
- name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
-
- @override
bool get enableAudio => false;
@override
@@ -117,6 +116,12 @@
@override
Future<void> resumePreview() async {}
+
+ @override
+ Future<void> setDescription(CameraDescription description) async {}
+
+ @override
+ CameraDescription get description => value.description;
}
void main() {
diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart
index 37168db..e23b865 100644
--- a/packages/camera/camera/test/camera_value_test.dart
+++ b/packages/camera/camera/test/camera_value_test.dart
@@ -13,6 +13,8 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'camera_preview_test.dart';
+
void main() {
group('camera_value', () {
test('Can be created', () {
@@ -32,6 +34,7 @@
recordingOrientation: DeviceOrientation.portraitUp,
focusPointSupported: true,
previewPauseOrientation: DeviceOrientation.portraitUp,
+ description: FakeController.fakeDescription,
);
expect(cameraValue, isA<CameraValue>());
@@ -54,7 +57,8 @@
});
test('Can be created as uninitialized', () {
- const CameraValue cameraValue = CameraValue.uninitialized();
+ const CameraValue cameraValue =
+ CameraValue.uninitialized(FakeController.fakeDescription);
expect(cameraValue, isA<CameraValue>());
expect(cameraValue.isInitialized, isFalse);
@@ -76,7 +80,8 @@
});
test('Can be copied with isInitialized', () {
- const CameraValue cv = CameraValue.uninitialized();
+ const CameraValue cv =
+ CameraValue.uninitialized(FakeController.fakeDescription);
final CameraValue cameraValue = cv.copyWith(isInitialized: true);
expect(cameraValue, isA<CameraValue>());
@@ -99,7 +104,8 @@
});
test('Has aspectRatio after setting size', () {
- const CameraValue cv = CameraValue.uninitialized();
+ const CameraValue cv =
+ CameraValue.uninitialized(FakeController.fakeDescription);
final CameraValue cameraValue =
cv.copyWith(isInitialized: true, previewSize: const Size(20, 10));
@@ -107,7 +113,8 @@
});
test('hasError is true after setting errorDescription', () {
- const CameraValue cv = CameraValue.uninitialized();
+ const CameraValue cv =
+ CameraValue.uninitialized(FakeController.fakeDescription);
final CameraValue cameraValue = cv.copyWith(errorDescription: 'error');
expect(cameraValue.hasError, isTrue);
@@ -115,7 +122,8 @@
});
test('Recording paused is false when not recording', () {
- const CameraValue cv = CameraValue.uninitialized();
+ const CameraValue cv =
+ CameraValue.uninitialized(FakeController.fakeDescription);
final CameraValue cameraValue = cv.copyWith(
isInitialized: true,
isRecordingVideo: false,
@@ -126,25 +134,27 @@
test('toString() works as expected', () {
const CameraValue cameraValue = CameraValue(
- isInitialized: false,
- previewSize: Size(10, 10),
- isRecordingPaused: false,
- isRecordingVideo: false,
- isTakingPicture: false,
- isStreamingImages: false,
- flashMode: FlashMode.auto,
- exposureMode: ExposureMode.auto,
- focusMode: FocusMode.auto,
- exposurePointSupported: true,
- focusPointSupported: true,
- deviceOrientation: DeviceOrientation.portraitUp,
- lockedCaptureOrientation: DeviceOrientation.portraitUp,
- recordingOrientation: DeviceOrientation.portraitUp,
- isPreviewPaused: true,
- previewPauseOrientation: DeviceOrientation.portraitUp);
+ isInitialized: false,
+ previewSize: Size(10, 10),
+ isRecordingPaused: false,
+ isRecordingVideo: false,
+ isTakingPicture: false,
+ isStreamingImages: false,
+ flashMode: FlashMode.auto,
+ exposureMode: ExposureMode.auto,
+ focusMode: FocusMode.auto,
+ exposurePointSupported: true,
+ focusPointSupported: true,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation: DeviceOrientation.portraitUp,
+ recordingOrientation: DeviceOrientation.portraitUp,
+ isPreviewPaused: true,
+ previewPauseOrientation: DeviceOrientation.portraitUp,
+ description: FakeController.fakeDescription,
+ );
expect(cameraValue.toString(),
- 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)');
+ 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, description: CameraDescription(, CameraLensDirection.back, 0))');
});
});
}