Skip to content

Commit bc6a553

Browse files
authored
[camera] Reland implementation of flip camera while recording. App facing changes (#3496)
[camera] Reland implementation of flip camera while recording. App facing changes
1 parent 7a566e4 commit bc6a553

File tree

8 files changed

+129
-53
lines changed

8 files changed

+129
-53
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 0.10.4
22

3+
* Allows camera to be switched while video recording.
34
* Updates minimum Flutter version to 3.3.
45
* Aligns Dart and Flutter SDK constraints.
56

packages/camera/camera/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ void didChangeAppLifecycleState(AppLifecycleState state) {
7070
if (state == AppLifecycleState.inactive) {
7171
cameraController.dispose();
7272
} else if (state == AppLifecycleState.resumed) {
73-
onNewCameraSelected(cameraController.description);
73+
_initializeCameraController(cameraController.description);
7474
}
7575
}
7676
```

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,45 @@ void main() {
265265
return completer.future;
266266
}
267267

268+
testWidgets('Set description while recording', (WidgetTester tester) async {
269+
final List<CameraDescription> cameras = await availableCameras();
270+
if (cameras.length < 2) {
271+
return;
272+
}
273+
274+
final CameraController controller = CameraController(
275+
cameras[0],
276+
ResolutionPreset.low,
277+
enableAudio: false,
278+
);
279+
280+
await controller.initialize();
281+
await controller.prepareForVideoRecording();
282+
283+
await controller.startVideoRecording();
284+
await controller.setDescription(cameras[1]);
285+
286+
expect(controller.description, cameras[1]);
287+
});
288+
289+
testWidgets('Set description', (WidgetTester tester) async {
290+
final List<CameraDescription> cameras = await availableCameras();
291+
if (cameras.length < 2) {
292+
return;
293+
}
294+
295+
final CameraController controller = CameraController(
296+
cameras[0],
297+
ResolutionPreset.low,
298+
enableAudio: false,
299+
);
300+
301+
await controller.initialize();
302+
await controller.setDescription(cameras[1]);
303+
304+
expect(controller.description, cameras[1]);
305+
});
306+
268307
testWidgets(
269308
'iOS image streaming with imageFormatGroup',
270309
(WidgetTester tester) async {

packages/camera/camera/example/lib/main.dart

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
120120
if (state == AppLifecycleState.inactive) {
121121
cameraController.dispose();
122122
} else if (state == AppLifecycleState.resumed) {
123-
onNewCameraSelected(cameraController.description);
123+
_initializeCameraController(cameraController.description);
124124
}
125125
}
126126
// #enddocregion AppLifecycle
@@ -597,10 +597,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
597597
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
598598
groupValue: controller?.description,
599599
value: cameraDescription,
600-
onChanged:
601-
controller != null && controller!.value.isRecordingVideo
602-
? null
603-
: onChanged,
600+
onChanged: onChanged,
604601
),
605602
),
606603
);
@@ -633,17 +630,15 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
633630
}
634631

635632
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
636-
final CameraController? oldController = controller;
637-
if (oldController != null) {
638-
// `controller` needs to be set to null before getting disposed,
639-
// to avoid a race condition when we use the controller that is being
640-
// disposed. This happens when camera permission dialog shows up,
641-
// which triggers `didChangeAppLifecycleState`, which disposes and
642-
// re-creates the controller.
643-
controller = null;
644-
await oldController.dispose();
633+
if (controller != null) {
634+
return controller!.setDescription(cameraDescription);
635+
} else {
636+
return _initializeCameraController(cameraDescription);
645637
}
638+
}
646639

640+
Future<void> _initializeCameraController(
641+
CameraDescription cameraDescription) async {
647642
final CameraController cameraController = CameraController(
648643
cameraDescription,
649644
kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ class CameraValue {
4949
required this.exposurePointSupported,
5050
required this.focusPointSupported,
5151
required this.deviceOrientation,
52+
required this.description,
5253
this.lockedCaptureOrientation,
5354
this.recordingOrientation,
5455
this.isPreviewPaused = false,
5556
this.previewPauseOrientation,
5657
}) : _isRecordingPaused = isRecordingPaused;
5758

5859
/// Creates a new camera controller state for an uninitialized controller.
59-
const CameraValue.uninitialized()
60+
const CameraValue.uninitialized(CameraDescription description)
6061
: this(
6162
isInitialized: false,
6263
isRecordingVideo: false,
@@ -70,6 +71,7 @@ class CameraValue {
7071
focusPointSupported: false,
7172
deviceOrientation: DeviceOrientation.portraitUp,
7273
isPreviewPaused: false,
74+
description: description,
7375
);
7476

7577
/// True after [CameraController.initialize] has completed successfully.
@@ -143,6 +145,9 @@ class CameraValue {
143145
/// The orientation of the currently running video recording.
144146
final DeviceOrientation? recordingOrientation;
145147

148+
/// The properties of the camera device controlled by this controller.
149+
final CameraDescription description;
150+
146151
/// Creates a modified copy of the object.
147152
///
148153
/// Explicitly specified fields get the specified value, all other fields get
@@ -164,6 +169,7 @@ class CameraValue {
164169
Optional<DeviceOrientation>? lockedCaptureOrientation,
165170
Optional<DeviceOrientation>? recordingOrientation,
166171
bool? isPreviewPaused,
172+
CameraDescription? description,
167173
Optional<DeviceOrientation>? previewPauseOrientation,
168174
}) {
169175
return CameraValue(
@@ -188,6 +194,7 @@ class CameraValue {
188194
? this.recordingOrientation
189195
: recordingOrientation.orNull,
190196
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
197+
description: description ?? this.description,
191198
previewPauseOrientation: previewPauseOrientation == null
192199
? this.previewPauseOrientation
193200
: previewPauseOrientation.orNull,
@@ -211,7 +218,8 @@ class CameraValue {
211218
'lockedCaptureOrientation: $lockedCaptureOrientation, '
212219
'recordingOrientation: $recordingOrientation, '
213220
'isPreviewPaused: $isPreviewPaused, '
214-
'previewPausedOrientation: $previewPauseOrientation)';
221+
'previewPausedOrientation: $previewPauseOrientation, '
222+
'description: $description)';
215223
}
216224
}
217225

@@ -225,14 +233,14 @@ class CameraValue {
225233
class CameraController extends ValueNotifier<CameraValue> {
226234
/// Creates a new camera controller in an uninitialized state.
227235
CameraController(
228-
this.description,
236+
CameraDescription description,
229237
this.resolutionPreset, {
230238
this.enableAudio = true,
231239
this.imageFormatGroup,
232-
}) : super(const CameraValue.uninitialized());
240+
}) : super(CameraValue.uninitialized(description));
233241

234242
/// The properties of the camera device controlled by this controller.
235-
final CameraDescription description;
243+
CameraDescription get description => value.description;
236244

237245
/// The resolution this controller is targeting.
238246
///
@@ -274,7 +282,12 @@ class CameraController extends ValueNotifier<CameraValue> {
274282
/// Initializes the camera on the device.
275283
///
276284
/// Throws a [CameraException] if the initialization fails.
277-
Future<void> initialize() async {
285+
Future<void> initialize() => _initializeWithDescription(description);
286+
287+
/// Initializes the camera on the device with the specified description.
288+
///
289+
/// Throws a [CameraException] if the initialization fails.
290+
Future<void> _initializeWithDescription(CameraDescription description) async {
278291
if (_isDisposed) {
279292
throw CameraException(
280293
'Disposed CameraController',
@@ -313,6 +326,7 @@ class CameraController extends ValueNotifier<CameraValue> {
313326

314327
value = value.copyWith(
315328
isInitialized: true,
329+
description: description,
316330
previewSize: await initializeCompleter.future
317331
.then((CameraInitializedEvent event) => Size(
318332
event.previewWidth,
@@ -380,6 +394,18 @@ class CameraController extends ValueNotifier<CameraValue> {
380394
}
381395
}
382396

397+
/// Sets the description of the camera.
398+
///
399+
/// Throws a [CameraException] if setting the description fails.
400+
Future<void> setDescription(CameraDescription description) async {
401+
if (value.isRecordingVideo) {
402+
await CameraPlatform.instance.setDescriptionWhileRecording(description);
403+
value = value.copyWith(description: description);
404+
} else {
405+
await _initializeWithDescription(description);
406+
}
407+
}
408+
383409
/// Captures an image and returns the file where it was saved.
384410
///
385411
/// Throws a [CameraException] if the capture fails.

packages/camera/camera/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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.10.3+2
7+
version: 0.10.4
88

99
environment:
1010
sdk: ">=2.18.0 <4.0.0"
@@ -21,9 +21,9 @@ flutter:
2121
default_package: camera_web
2222

2323
dependencies:
24-
camera_android: ^0.10.1
25-
camera_avfoundation: ^0.9.9
26-
camera_platform_interface: ^2.3.2
24+
camera_android: ^0.10.5
25+
camera_avfoundation: ^0.9.13
26+
camera_platform_interface: ^2.4.0
2727
camera_web: ^0.3.1
2828
flutter:
2929
sdk: flutter

packages/camera/camera/test/camera_preview_test.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import 'package:flutter_test/flutter_test.dart';
1111

1212
class FakeController extends ValueNotifier<CameraValue>
1313
implements CameraController {
14-
FakeController() : super(const CameraValue.uninitialized());
14+
FakeController() : super(const CameraValue.uninitialized(fakeDescription));
15+
16+
static const CameraDescription fakeDescription = CameraDescription(
17+
name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
1518

1619
@override
1720
Future<void> dispose() async {
@@ -29,10 +32,6 @@ class FakeController extends ValueNotifier<CameraValue>
2932
@override
3033
void debugCheckIsDisposed() {}
3134

32-
@override
33-
CameraDescription get description => const CameraDescription(
34-
name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
35-
3635
@override
3736
bool get enableAudio => false;
3837

@@ -117,6 +116,12 @@ class FakeController extends ValueNotifier<CameraValue>
117116

118117
@override
119118
Future<void> resumePreview() async {}
119+
120+
@override
121+
Future<void> setDescription(CameraDescription description) async {}
122+
123+
@override
124+
CameraDescription get description => value.description;
120125
}
121126

122127
void main() {

packages/camera/camera/test/camera_value_test.dart

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import 'package:flutter/cupertino.dart';
1313
import 'package:flutter/services.dart';
1414
import 'package:flutter_test/flutter_test.dart';
1515

16+
import 'camera_preview_test.dart';
17+
1618
void main() {
1719
group('camera_value', () {
1820
test('Can be created', () {
@@ -32,6 +34,7 @@ void main() {
3234
recordingOrientation: DeviceOrientation.portraitUp,
3335
focusPointSupported: true,
3436
previewPauseOrientation: DeviceOrientation.portraitUp,
37+
description: FakeController.fakeDescription,
3538
);
3639

3740
expect(cameraValue, isA<CameraValue>());
@@ -54,7 +57,8 @@ void main() {
5457
});
5558

5659
test('Can be created as uninitialized', () {
57-
const CameraValue cameraValue = CameraValue.uninitialized();
60+
const CameraValue cameraValue =
61+
CameraValue.uninitialized(FakeController.fakeDescription);
5862

5963
expect(cameraValue, isA<CameraValue>());
6064
expect(cameraValue.isInitialized, isFalse);
@@ -76,7 +80,8 @@ void main() {
7680
});
7781

7882
test('Can be copied with isInitialized', () {
79-
const CameraValue cv = CameraValue.uninitialized();
83+
const CameraValue cv =
84+
CameraValue.uninitialized(FakeController.fakeDescription);
8085
final CameraValue cameraValue = cv.copyWith(isInitialized: true);
8186

8287
expect(cameraValue, isA<CameraValue>());
@@ -99,23 +104,26 @@ void main() {
99104
});
100105

101106
test('Has aspectRatio after setting size', () {
102-
const CameraValue cv = CameraValue.uninitialized();
107+
const CameraValue cv =
108+
CameraValue.uninitialized(FakeController.fakeDescription);
103109
final CameraValue cameraValue =
104110
cv.copyWith(isInitialized: true, previewSize: const Size(20, 10));
105111

106112
expect(cameraValue.aspectRatio, 2.0);
107113
});
108114

109115
test('hasError is true after setting errorDescription', () {
110-
const CameraValue cv = CameraValue.uninitialized();
116+
const CameraValue cv =
117+
CameraValue.uninitialized(FakeController.fakeDescription);
111118
final CameraValue cameraValue = cv.copyWith(errorDescription: 'error');
112119

113120
expect(cameraValue.hasError, isTrue);
114121
expect(cameraValue.errorDescription, 'error');
115122
});
116123

117124
test('Recording paused is false when not recording', () {
118-
const CameraValue cv = CameraValue.uninitialized();
125+
const CameraValue cv =
126+
CameraValue.uninitialized(FakeController.fakeDescription);
119127
final CameraValue cameraValue = cv.copyWith(
120128
isInitialized: true,
121129
isRecordingVideo: false,
@@ -126,25 +134,27 @@ void main() {
126134

127135
test('toString() works as expected', () {
128136
const CameraValue cameraValue = CameraValue(
129-
isInitialized: false,
130-
previewSize: Size(10, 10),
131-
isRecordingPaused: false,
132-
isRecordingVideo: false,
133-
isTakingPicture: false,
134-
isStreamingImages: false,
135-
flashMode: FlashMode.auto,
136-
exposureMode: ExposureMode.auto,
137-
focusMode: FocusMode.auto,
138-
exposurePointSupported: true,
139-
focusPointSupported: true,
140-
deviceOrientation: DeviceOrientation.portraitUp,
141-
lockedCaptureOrientation: DeviceOrientation.portraitUp,
142-
recordingOrientation: DeviceOrientation.portraitUp,
143-
isPreviewPaused: true,
144-
previewPauseOrientation: DeviceOrientation.portraitUp);
137+
isInitialized: false,
138+
previewSize: Size(10, 10),
139+
isRecordingPaused: false,
140+
isRecordingVideo: false,
141+
isTakingPicture: false,
142+
isStreamingImages: false,
143+
flashMode: FlashMode.auto,
144+
exposureMode: ExposureMode.auto,
145+
focusMode: FocusMode.auto,
146+
exposurePointSupported: true,
147+
focusPointSupported: true,
148+
deviceOrientation: DeviceOrientation.portraitUp,
149+
lockedCaptureOrientation: DeviceOrientation.portraitUp,
150+
recordingOrientation: DeviceOrientation.portraitUp,
151+
isPreviewPaused: true,
152+
previewPauseOrientation: DeviceOrientation.portraitUp,
153+
description: FakeController.fakeDescription,
154+
);
145155

146156
expect(cameraValue.toString(),
147-
'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)');
157+
'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))');
148158
});
149159
});
150160
}

0 commit comments

Comments
 (0)