blob: 18debf68854792ec4f0899bc8e3ef171313cfe8e [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/widgets.dart';
import 'package:stream_transform/stream_transform.dart';
import 'camera.dart';
import 'camera_info.dart';
import 'camera_selector.dart';
import 'camerax_library.g.dart';
import 'preview.dart';
import 'process_camera_provider.dart';
import 'surface.dart';
import 'system_services.dart';
import 'use_case.dart';
/// The Android implementation of [CameraPlatform] that uses the CameraX library.
class AndroidCameraCameraX extends CameraPlatform {
/// Registers this class as the default instance of [CameraPlatform].
static void registerWith() {
CameraPlatform.instance = AndroidCameraCameraX();
}
/// The [ProcessCameraProvider] instance used to access camera functionality.
@visibleForTesting
ProcessCameraProvider? processCameraProvider;
/// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is
/// bound to the lifecycle of the camera it manages.
@visibleForTesting
Camera? camera;
/// The [Preview] instance that can be configured to present a live camera preview.
@visibleForTesting
Preview? preview;
/// Whether or not the [preview] is currently bound to the lifecycle that the
/// [processCameraProvider] tracks.
@visibleForTesting
bool previewIsBound = false;
bool _previewIsPaused = false;
/// The [CameraSelector] used to configure the [processCameraProvider] to use
/// the desired camera.
@visibleForTesting
CameraSelector? cameraSelector;
/// The controller we need to broadcast the different camera events.
///
/// It is a `broadcast` because multiple controllers will connect to
/// different stream views of this Controller.
/// This is only exposed for test purposes. It shouldn't be used by clients of
/// the plugin as it may break or change at any time.
@visibleForTesting
final StreamController<CameraEvent> cameraEventStreamController =
StreamController<CameraEvent>.broadcast();
/// The stream of camera events.
Stream<CameraEvent> _cameraEvents(int cameraId) =>
cameraEventStreamController.stream
.where((CameraEvent event) => event.cameraId == cameraId);
/// Returns list of all available cameras and their descriptions.
@override
Future<List<CameraDescription>> availableCameras() async {
final List<CameraDescription> cameraDescriptions = <CameraDescription>[];
processCameraProvider ??= await ProcessCameraProvider.getInstance();
final List<CameraInfo> cameraInfos =
await processCameraProvider!.getAvailableCameraInfos();
CameraLensDirection? cameraLensDirection;
int cameraCount = 0;
int? cameraSensorOrientation;
String? cameraName;
for (final CameraInfo cameraInfo in cameraInfos) {
// Determine the lens direction by filtering the CameraInfo
// TODO(gmackall): replace this with call to CameraInfo.getLensFacing when changes containing that method are available
if ((await createCameraSelector(CameraSelector.lensFacingBack)
.filter(<CameraInfo>[cameraInfo]))
.isNotEmpty) {
cameraLensDirection = CameraLensDirection.back;
} else if ((await createCameraSelector(CameraSelector.lensFacingFront)
.filter(<CameraInfo>[cameraInfo]))
.isNotEmpty) {
cameraLensDirection = CameraLensDirection.front;
} else {
//Skip this CameraInfo as its lens direction is unknown
continue;
}
cameraSensorOrientation = await cameraInfo.getSensorRotationDegrees();
cameraName = 'Camera $cameraCount';
cameraCount++;
cameraDescriptions.add(CameraDescription(
name: cameraName,
lensDirection: cameraLensDirection,
sensorOrientation: cameraSensorOrientation));
}
return cameraDescriptions;
}
/// Creates an uninitialized camera instance and returns the camera ID.
///
/// In the CameraX library, cameras are accessed by combining [UseCase]s
/// to an instance of a [ProcessCameraProvider]. Thus, to create an
/// unitialized camera instance, this method retrieves a
/// [ProcessCameraProvider] instance.
///
/// To return the camera ID, which is equivalent to the ID of the surface texture
/// that a camera preview can be drawn to, a [Preview] instance is configured
/// and bound to the [ProcessCameraProvider] instance.
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
// Must obtain proper permissions before attempting to access a camera.
await requestCameraPermissions(enableAudio);
// Save CameraSelector that matches cameraDescription.
final int cameraSelectorLensDirection =
_getCameraSelectorLensDirection(cameraDescription.lensDirection);
final bool cameraIsFrontFacing =
cameraSelectorLensDirection == CameraSelector.lensFacingFront;
cameraSelector = createCameraSelector(cameraSelectorLensDirection);
// Start listening for device orientation changes preceding camera creation.
startListeningForDeviceOrientationChange(
cameraIsFrontFacing, cameraDescription.sensorOrientation);
// Retrieve a ProcessCameraProvider instance.
processCameraProvider ??= await ProcessCameraProvider.getInstance();
// Configure Preview instance and bind to ProcessCameraProvider.
final int targetRotation =
_getTargetRotation(cameraDescription.sensorOrientation);
final ResolutionInfo? targetResolution =
_getTargetResolutionForPreview(resolutionPreset);
preview = createPreview(targetRotation, targetResolution);
previewIsBound = false;
_previewIsPaused = false;
final int flutterSurfaceTextureId = await preview!.setSurfaceProvider();
return flutterSurfaceTextureId;
}
/// Initializes the camera on the device.
///
/// Since initialization of a camera does not directly map as an operation to
/// the CameraX library, this method just retrieves information about the
/// camera and sends a [CameraInitializedEvent].
///
/// [imageFormatGroup] is used to specify the image formatting used.
/// On Android this defaults to ImageFormat.YUV_420_888 and applies only to
/// the image stream.
@override
Future<void> initializeCamera(
int cameraId, {
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
}) async {
// TODO(camsim99): Use imageFormatGroup to configure ImageAnalysis use case
// for image streaming.
// https://github.com/flutter/flutter/issues/120463
// Configure CameraInitializedEvent to send as representation of a
// configured camera:
// Retrieve preview resolution.
assert(
preview != null,
'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"',
);
await _bindPreviewToLifecycle();
final ResolutionInfo previewResolutionInfo =
await preview!.getResolutionInfo();
_unbindPreviewFromLifecycle();
// Retrieve exposure and focus mode configurations:
// TODO(camsim99): Implement support for retrieving exposure mode configuration.
// https://github.com/flutter/flutter/issues/120468
const ExposureMode exposureMode = ExposureMode.auto;
const bool exposurePointSupported = false;
// TODO(camsim99): Implement support for retrieving focus mode configuration.
// https://github.com/flutter/flutter/issues/120467
const FocusMode focusMode = FocusMode.auto;
const bool focusPointSupported = false;
cameraEventStreamController.add(CameraInitializedEvent(
cameraId,
previewResolutionInfo.width.toDouble(),
previewResolutionInfo.height.toDouble(),
exposureMode,
exposurePointSupported,
focusMode,
focusPointSupported));
}
/// Releases the resources of the accessed camera.
///
/// [cameraId] not used.
@override
Future<void> dispose(int cameraId) async {
preview?.releaseFlutterSurfaceTexture();
processCameraProvider?.unbindAll();
}
/// The camera has been initialized.
@override
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraInitializedEvent>();
}
/// The camera experienced an error.
@override
Stream<CameraErrorEvent> onCameraError(int cameraId) {
return SystemServices.cameraErrorStreamController.stream
.map<CameraErrorEvent>((String errorDescription) {
return CameraErrorEvent(cameraId, errorDescription);
});
}
/// The ui orientation changed.
@override
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
return SystemServices.deviceOrientationChangedStreamController.stream;
}
/// Pause the active preview on the current frame for the selected camera.
///
/// [cameraId] not used.
@override
Future<void> pausePreview(int cameraId) async {
_unbindPreviewFromLifecycle();
_previewIsPaused = true;
}
/// Resume the paused preview for the selected camera.
///
/// [cameraId] not used.
@override
Future<void> resumePreview(int cameraId) async {
await _bindPreviewToLifecycle();
_previewIsPaused = false;
}
/// Returns a widget showing a live camera preview.
@override
Widget buildPreview(int cameraId) {
return FutureBuilder<void>(
future: _bindPreviewToLifecycle(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
// Do nothing while waiting for preview to be bound to lifecyle.
return const SizedBox.shrink();
case ConnectionState.done:
return Texture(textureId: cameraId);
}
});
}
// Methods for binding UseCases to the lifecycle of the camera controlled
// by a ProcessCameraProvider instance:
/// Binds [preview] instance to the camera lifecycle controlled by the
/// [processCameraProvider].
Future<void> _bindPreviewToLifecycle() async {
assert(processCameraProvider != null);
assert(cameraSelector != null);
if (previewIsBound || _previewIsPaused) {
// Only bind if preview is not already bound or intentionally paused.
return;
}
camera = await processCameraProvider!
.bindToLifecycle(cameraSelector!, <UseCase>[preview!]);
previewIsBound = true;
}
/// Unbinds [preview] instance to camera lifecycle controlled by the
/// [processCameraProvider].
void _unbindPreviewFromLifecycle() {
if (preview == null || !previewIsBound) {
return;
}
assert(processCameraProvider != null);
processCameraProvider!.unbind(<UseCase>[preview!]);
previewIsBound = false;
}
// Methods for mapping Flutter camera constants to CameraX constants:
/// Returns [CameraSelector] lens direction that maps to specified
/// [CameraLensDirection].
int _getCameraSelectorLensDirection(CameraLensDirection lensDirection) {
switch (lensDirection) {
case CameraLensDirection.front:
return CameraSelector.lensFacingFront;
case CameraLensDirection.back:
return CameraSelector.lensFacingBack;
case CameraLensDirection.external:
return CameraSelector.lensFacingExternal;
}
}
/// Returns [Surface] target rotation constant that maps to specified sensor
/// orientation.
int _getTargetRotation(int sensorOrientation) {
switch (sensorOrientation) {
case 90:
return Surface.ROTATION_90;
case 180:
return Surface.ROTATION_180;
case 270:
return Surface.ROTATION_270;
case 0:
return Surface.ROTATION_0;
default:
throw ArgumentError(
'"$sensorOrientation" is not a valid sensor orientation value');
}
}
/// Returns [ResolutionInfo] that maps to the specified resolution preset for
/// a camera preview.
ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) {
// TODO(camsim99): Implement resolution configuration.
// https://github.com/flutter/flutter/issues/120462
return null;
}
// Methods for calls that need to be tested:
/// Requests camera permissions.
@visibleForTesting
Future<void> requestCameraPermissions(bool enableAudio) async {
await SystemServices.requestCameraPermissions(enableAudio);
}
/// Subscribes the plugin as a listener to changes in device orientation.
@visibleForTesting
void startListeningForDeviceOrientationChange(
bool cameraIsFrontFacing, int sensorOrientation) {
SystemServices.startListeningForDeviceOrientationChange(
cameraIsFrontFacing, sensorOrientation);
}
/// Returns a [CameraSelector] based on the specified camera lens direction.
@visibleForTesting
CameraSelector createCameraSelector(int cameraSelectorLensDirection) {
switch (cameraSelectorLensDirection) {
case CameraSelector.lensFacingFront:
return CameraSelector.getDefaultFrontCamera();
case CameraSelector.lensFacingBack:
return CameraSelector.getDefaultBackCamera();
default:
return CameraSelector(lensFacing: cameraSelectorLensDirection);
}
}
/// Returns a [Preview] configured with the specified target rotation and
/// resolution.
@visibleForTesting
Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) {
return Preview(
targetRotation: targetRotation, targetResolution: targetResolution);
}
}