blob: 5a7a79c8d96c630cdab9005f872faf9cdb0f3461 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:quiver/core.dart';
/// The state of a [CameraController].
class CameraValue {
/// Creates a new camera controller state.
const CameraValue({
required this.isInitialized,
this.previewSize,
required this.isRecordingVideo,
required this.isTakingPicture,
required this.isStreamingImages,
required this.isRecordingPaused,
required this.flashMode,
required this.exposureMode,
required this.focusMode,
required this.deviceOrientation,
this.lockedCaptureOrientation,
this.recordingOrientation,
this.isPreviewPaused = false,
this.previewPauseOrientation,
});
/// Creates a new camera controller state for an uninitialized controller.
const CameraValue.uninitialized()
: this(
isInitialized: false,
isRecordingVideo: false,
isTakingPicture: false,
isStreamingImages: false,
isRecordingPaused: false,
flashMode: FlashMode.auto,
exposureMode: ExposureMode.auto,
focusMode: FocusMode.auto,
deviceOrientation: DeviceOrientation.portraitUp,
isPreviewPaused: false,
);
/// True after [CameraController.initialize] has completed successfully.
final bool isInitialized;
/// True when a picture capture request has been sent but as not yet returned.
final bool isTakingPicture;
/// True when the camera is recording (not the same as previewing).
final bool isRecordingVideo;
/// True when images from the camera are being streamed.
final bool isStreamingImages;
/// True when video recording is paused.
final bool isRecordingPaused;
/// True when the preview widget has been paused manually.
final bool isPreviewPaused;
/// Set to the orientation the preview was paused in, if it is currently paused.
final DeviceOrientation? previewPauseOrientation;
/// The size of the preview in pixels.
///
/// Is `null` until [isInitialized] is `true`.
final Size? previewSize;
/// The flash mode the camera is currently set to.
final FlashMode flashMode;
/// The exposure mode the camera is currently set to.
final ExposureMode exposureMode;
/// The focus mode the camera is currently set to.
final FocusMode focusMode;
/// The current device UI orientation.
final DeviceOrientation deviceOrientation;
/// The currently locked capture orientation.
final DeviceOrientation? lockedCaptureOrientation;
/// Whether the capture orientation is currently locked.
bool get isCaptureOrientationLocked => lockedCaptureOrientation != null;
/// The orientation of the currently running video recording.
final DeviceOrientation? recordingOrientation;
/// Creates a modified copy of the object.
///
/// Explicitly specified fields get the specified value, all other fields get
/// the same value of the current object.
CameraValue copyWith({
bool? isInitialized,
bool? isRecordingVideo,
bool? isTakingPicture,
bool? isStreamingImages,
Size? previewSize,
bool? isRecordingPaused,
FlashMode? flashMode,
ExposureMode? exposureMode,
FocusMode? focusMode,
bool? exposurePointSupported,
bool? focusPointSupported,
DeviceOrientation? deviceOrientation,
Optional<DeviceOrientation>? lockedCaptureOrientation,
Optional<DeviceOrientation>? recordingOrientation,
bool? isPreviewPaused,
Optional<DeviceOrientation>? previewPauseOrientation,
}) {
return CameraValue(
isInitialized: isInitialized ?? this.isInitialized,
previewSize: previewSize ?? this.previewSize,
isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo,
isTakingPicture: isTakingPicture ?? this.isTakingPicture,
isStreamingImages: isStreamingImages ?? this.isStreamingImages,
isRecordingPaused: isRecordingPaused ?? this.isRecordingPaused,
flashMode: flashMode ?? this.flashMode,
exposureMode: exposureMode ?? this.exposureMode,
focusMode: focusMode ?? this.focusMode,
deviceOrientation: deviceOrientation ?? this.deviceOrientation,
lockedCaptureOrientation: lockedCaptureOrientation == null
? this.lockedCaptureOrientation
: lockedCaptureOrientation.orNull,
recordingOrientation: recordingOrientation == null
? this.recordingOrientation
: recordingOrientation.orNull,
isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
previewPauseOrientation: previewPauseOrientation == null
? this.previewPauseOrientation
: previewPauseOrientation.orNull,
);
}
@override
String toString() {
return '${objectRuntimeType(this, 'CameraValue')}('
'isRecordingVideo: $isRecordingVideo, '
'isInitialized: $isInitialized, '
'previewSize: $previewSize, '
'isStreamingImages: $isStreamingImages, '
'flashMode: $flashMode, '
'exposureMode: $exposureMode, '
'focusMode: $focusMode, '
'deviceOrientation: $deviceOrientation, '
'lockedCaptureOrientation: $lockedCaptureOrientation, '
'recordingOrientation: $recordingOrientation, '
'isPreviewPaused: $isPreviewPaused, '
'previewPausedOrientation: $previewPauseOrientation)';
}
}
/// Controls a device camera.
///
/// This is a stripped-down version of the app-facing controller to serve as a
/// utility for the example and integration tests. It wraps only the calls that
/// have state associated with them, to consolidate tracking of camera state
/// outside of the overall example code.
class CameraController extends ValueNotifier<CameraValue> {
/// Creates a new camera controller in an uninitialized state.
CameraController(
this.description,
this.resolutionPreset, {
this.enableAudio = true,
this.imageFormatGroup,
}) : super(const CameraValue.uninitialized());
/// The properties of the camera device controlled by this controller.
final CameraDescription description;
/// The resolution this controller is targeting.
///
/// This resolution preset is not guaranteed to be available on the device,
/// if unavailable a lower resolution will be used.
///
/// See also: [ResolutionPreset].
final ResolutionPreset resolutionPreset;
/// Whether to include audio when recording a video.
final bool enableAudio;
/// The [ImageFormatGroup] describes the output of the raw image format.
///
/// When null the imageFormat will fallback to the platforms default.
final ImageFormatGroup? imageFormatGroup;
late int _cameraId;
bool _isDisposed = false;
StreamSubscription<CameraImageData>? _imageStreamSubscription;
FutureOr<bool>? _initCalled;
StreamSubscription<DeviceOrientationChangedEvent>?
_deviceOrientationSubscription;
/// The camera identifier with which the controller is associated.
int get cameraId => _cameraId;
/// Initializes the camera on the device.
Future<void> initialize() async {
final Completer<CameraInitializedEvent> _initializeCompleter =
Completer<CameraInitializedEvent>();
_deviceOrientationSubscription = CameraPlatform.instance
.onDeviceOrientationChanged()
.listen((DeviceOrientationChangedEvent event) {
value = value.copyWith(
deviceOrientation: event.orientation,
);
});
_cameraId = await CameraPlatform.instance.createCamera(
description,
resolutionPreset,
enableAudio: enableAudio,
);
CameraPlatform.instance
.onCameraInitialized(_cameraId)
.first
.then((CameraInitializedEvent event) {
_initializeCompleter.complete(event);
});
await CameraPlatform.instance.initializeCamera(
_cameraId,
imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown,
);
value = value.copyWith(
isInitialized: true,
previewSize: await _initializeCompleter.future
.then((CameraInitializedEvent event) => Size(
event.previewWidth,
event.previewHeight,
)),
exposureMode: await _initializeCompleter.future
.then((CameraInitializedEvent event) => event.exposureMode),
focusMode: await _initializeCompleter.future
.then((CameraInitializedEvent event) => event.focusMode),
exposurePointSupported: await _initializeCompleter.future
.then((CameraInitializedEvent event) => event.exposurePointSupported),
focusPointSupported: await _initializeCompleter.future
.then((CameraInitializedEvent event) => event.focusPointSupported),
);
_initCalled = true;
}
/// Prepare the capture session for video recording.
Future<void> prepareForVideoRecording() async {
await CameraPlatform.instance.prepareForVideoRecording();
}
/// Pauses the current camera preview
Future<void> pausePreview() async {
await CameraPlatform.instance.pausePreview(_cameraId);
value = value.copyWith(
isPreviewPaused: true,
previewPauseOrientation: Optional<DeviceOrientation>.of(
value.lockedCaptureOrientation ?? value.deviceOrientation));
}
/// Resumes the current camera preview
Future<void> resumePreview() async {
await CameraPlatform.instance.resumePreview(_cameraId);
value = value.copyWith(
isPreviewPaused: false,
previewPauseOrientation: const Optional<DeviceOrientation>.absent());
}
/// Captures an image and returns the file where it was saved.
///
/// Throws a [CameraException] if the capture fails.
Future<XFile> takePicture() async {
value = value.copyWith(isTakingPicture: true);
final XFile file = await CameraPlatform.instance.takePicture(_cameraId);
value = value.copyWith(isTakingPicture: false);
return file;
}
/// Start streaming images from platform camera.
Future<void> startImageStream(
Function(CameraImageData image) onAvailable) async {
_imageStreamSubscription = CameraPlatform.instance
.onStreamedFrameAvailable(_cameraId)
.listen((CameraImageData imageData) {
onAvailable(imageData);
});
value = value.copyWith(isStreamingImages: true);
}
/// Stop streaming images from platform camera.
Future<void> stopImageStream() async {
value = value.copyWith(isStreamingImages: false);
await _imageStreamSubscription?.cancel();
_imageStreamSubscription = null;
}
/// Start a video recording.
///
/// The video is returned as a [XFile] after calling [stopVideoRecording].
/// Throws a [CameraException] if the capture fails.
Future<void> startVideoRecording() async {
await CameraPlatform.instance.startVideoRecording(_cameraId);
value = value.copyWith(
isRecordingVideo: true,
isRecordingPaused: false,
recordingOrientation: Optional<DeviceOrientation>.of(
value.lockedCaptureOrientation ?? value.deviceOrientation));
}
/// Stops the video recording and returns the file where it was saved.
///
/// Throws a [CameraException] if the capture failed.
Future<XFile> stopVideoRecording() async {
final XFile file =
await CameraPlatform.instance.stopVideoRecording(_cameraId);
value = value.copyWith(
isRecordingVideo: false,
recordingOrientation: const Optional<DeviceOrientation>.absent(),
);
return file;
}
/// Pause video recording.
///
/// This feature is only available on iOS and Android sdk 24+.
Future<void> pauseVideoRecording() async {
await CameraPlatform.instance.pauseVideoRecording(_cameraId);
value = value.copyWith(isRecordingPaused: true);
}
/// Resume video recording after pausing.
///
/// This feature is only available on iOS and Android sdk 24+.
Future<void> resumeVideoRecording() async {
await CameraPlatform.instance.resumeVideoRecording(_cameraId);
value = value.copyWith(isRecordingPaused: false);
}
/// Returns a widget showing a live camera preview.
Widget buildPreview() {
return CameraPlatform.instance.buildPreview(_cameraId);
}
/// Sets the flash mode for taking pictures.
Future<void> setFlashMode(FlashMode mode) async {
await CameraPlatform.instance.setFlashMode(_cameraId, mode);
value = value.copyWith(flashMode: mode);
}
/// Sets the exposure mode for taking pictures.
Future<void> setExposureMode(ExposureMode mode) async {
await CameraPlatform.instance.setExposureMode(_cameraId, mode);
value = value.copyWith(exposureMode: mode);
}
/// Sets the exposure offset for the selected camera.
Future<double> setExposureOffset(double offset) async {
// Check if offset is in range
final List<double> range = await Future.wait(<Future<double>>[
CameraPlatform.instance.getMinExposureOffset(_cameraId),
CameraPlatform.instance.getMaxExposureOffset(_cameraId)
]);
// Round to the closest step if needed
final double stepSize =
await CameraPlatform.instance.getExposureOffsetStepSize(_cameraId);
if (stepSize > 0) {
final double inv = 1.0 / stepSize;
double roundedOffset = (offset * inv).roundToDouble() / inv;
if (roundedOffset > range[1]) {
roundedOffset = (offset * inv).floorToDouble() / inv;
} else if (roundedOffset < range[0]) {
roundedOffset = (offset * inv).ceilToDouble() / inv;
}
offset = roundedOffset;
}
return CameraPlatform.instance.setExposureOffset(_cameraId, offset);
}
/// Locks the capture orientation.
///
/// If [orientation] is omitted, the current device orientation is used.
Future<void> lockCaptureOrientation() async {
await CameraPlatform.instance
.lockCaptureOrientation(_cameraId, value.deviceOrientation);
value = value.copyWith(
lockedCaptureOrientation:
Optional<DeviceOrientation>.of(value.deviceOrientation));
}
/// Unlocks the capture orientation.
Future<void> unlockCaptureOrientation() async {
await CameraPlatform.instance.unlockCaptureOrientation(_cameraId);
value = value.copyWith(
lockedCaptureOrientation: const Optional<DeviceOrientation>.absent());
}
/// Sets the focus mode for taking pictures.
Future<void> setFocusMode(FocusMode mode) async {
await CameraPlatform.instance.setFocusMode(_cameraId, mode);
value = value.copyWith(focusMode: mode);
}
/// Releases the resources of this camera.
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
_deviceOrientationSubscription?.cancel();
_isDisposed = true;
super.dispose();
if (_initCalled != null) {
await _initCalled;
await CameraPlatform.instance.dispose(_cameraId);
}
}
@override
void removeListener(VoidCallback listener) {
// Prevent ValueListenableBuilder in CameraPreview widget from causing an
// exception to be thrown by attempting to remove its own listener after
// the controller has already been disposed.
if (!_isDisposed) {
super.removeListener(listener);
}
}
}