Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 2 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.10.4

* Allows camera to be switched while video recording.
* Aligns Dart and Flutter SDK constraints.

## 0.10.3+2
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
onNewCameraSelected(cameraController.description);
_initializeCameraController(cameraController.description);
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,49 @@ void main() {
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();
sleep(const Duration(milliseconds: 500));
await controller.setDescription(cameras[1]);
sleep(const Duration(milliseconds: 500));

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();
sleep(const Duration(milliseconds: 500));
await controller.setDescription(cameras[1]);
sleep(const Duration(milliseconds: 500));

expect(controller.description, cameras[1]);
});

testWidgets(
'iOS image streaming with imageFormatGroup',
(WidgetTester tester) async {
Expand Down
23 changes: 9 additions & 14 deletions packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
onNewCameraSelected(cameraController.description);
_initializeCameraController(cameraController.description);
}
}
// #enddocregion AppLifecycle
Expand Down Expand Up @@ -597,10 +597,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
groupValue: controller?.description,
value: cameraDescription,
onChanged:
controller != null && controller!.value.isRecordingVideo
? null
: onChanged,
onChanged: onChanged,
),
),
);
Expand Down Expand Up @@ -633,17 +630,15 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
}

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,
Expand Down
38 changes: 32 additions & 6 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ class CameraValue {
required this.exposurePointSupported,
required this.focusPointSupported,
required this.deviceOrientation,
required this.description,
this.lockedCaptureOrientation,
this.recordingOrientation,
this.isPreviewPaused = false,
this.previewPauseOrientation,
}) : _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,
Expand All @@ -70,6 +71,7 @@ class CameraValue {
focusPointSupported: false,
deviceOrientation: DeviceOrientation.portraitUp,
isPreviewPaused: false,
description: description,
);

/// True after [CameraController.initialize] has completed successfully.
Expand Down Expand Up @@ -143,6 +145,9 @@ class CameraValue {
/// 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
Expand All @@ -164,6 +169,7 @@ class CameraValue {
Optional<DeviceOrientation>? lockedCaptureOrientation,
Optional<DeviceOrientation>? recordingOrientation,
bool? isPreviewPaused,
CameraDescription? description,
Optional<DeviceOrientation>? previewPauseOrientation,
}) {
return CameraValue(
Expand All @@ -188,6 +194,7 @@ class CameraValue {
? this.recordingOrientation
: recordingOrientation.orNull,
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
description: description ?? this.description,
previewPauseOrientation: previewPauseOrientation == null
? this.previewPauseOrientation
: previewPauseOrientation.orNull,
Expand All @@ -211,7 +218,8 @@ class CameraValue {
'lockedCaptureOrientation: $lockedCaptureOrientation, '
'recordingOrientation: $recordingOrientation, '
'isPreviewPaused: $isPreviewPaused, '
'previewPausedOrientation: $previewPauseOrientation)';
'previewPausedOrientation: $previewPauseOrientation, '
'description: $description)';
}
}

Expand All @@ -225,14 +233,14 @@ class CameraValue {
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.
///
Expand Down Expand Up @@ -274,7 +282,12 @@ class CameraController extends ValueNotifier<CameraValue> {
/// 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',
Expand Down Expand Up @@ -313,6 +326,7 @@ class CameraController extends ValueNotifier<CameraValue> {

value = value.copyWith(
isInitialized: true,
description: description,
previewSize: await initializeCompleter.future
.then((CameraInitializedEvent event) => Size(
event.previewWidth,
Expand Down Expand Up @@ -380,6 +394,18 @@ class CameraController extends ValueNotifier<CameraValue> {
}
}

/// 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.
Expand Down
8 changes: 4 additions & 4 deletions packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.10.3+2
version: 0.10.4

environment:
sdk: ">=2.17.0 <3.0.0"
Expand All @@ -21,9 +21,9 @@ flutter:
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
Expand Down
15 changes: 10 additions & 5 deletions packages/camera/camera/test/camera_preview_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import 'package:flutter_test/flutter_test.dart';

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 {
Expand All @@ -29,10 +32,6 @@ class FakeController extends ValueNotifier<CameraValue>
@override
void debugCheckIsDisposed() {}

@override
CameraDescription get description => const CameraDescription(
name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);

@override
bool get enableAudio => false;

Expand Down Expand Up @@ -117,6 +116,12 @@ class FakeController extends ValueNotifier<CameraValue>

@override
Future<void> resumePreview() async {}

@override
Future<void> setDescription(CameraDescription description) async {}

@override
CameraDescription get description => value.description;
}

void main() {
Expand Down
Loading