blob: 129dda5f7fcd4fb5024436444bc3fa1f89f45a9f [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 'dart:math';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_transform/stream_transform.dart';
import 'messages.g.dart';
import 'type_conversion.dart';
import 'utils.dart';
/// An iOS implementation of [CameraPlatform] based on AVFoundation.
class AVFoundationCamera extends CameraPlatform {
/// Creates a new AVFoundation-based [CameraPlatform] implementation instance.
AVFoundationCamera({@visibleForTesting CameraApi? api})
: _hostApi = api ?? CameraApi();
/// Registers this class as the default instance of [CameraPlatform].
static void registerWith() {
CameraPlatform.instance = AVFoundationCamera();
}
/// Interface for calling host-side code.
final CameraApi _hostApi;
/// The controller we need to broadcast the different events coming
/// from handleMethodCall, specific to 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 handler for device-level messages that should be rebroadcast to
/// clients as [DeviceEvent]s.
@visibleForTesting
late final HostDeviceMessageHandler hostHandler = () {
// Set up the method handler lazily.
return HostDeviceMessageHandler();
}();
/// The per-camera handlers for messages that should be rebroadcast to
/// clients as [CameraEvent]s.
@visibleForTesting
final Map<int, HostCameraMessageHandler> hostCameraHandlers =
<int, HostCameraMessageHandler>{};
// The stream to receive frames from the native code.
StreamSubscription<dynamic>? _platformImageStreamSubscription;
// The stream for vending frames to platform interface clients.
StreamController<CameraImageData>? _frameStreamController;
Stream<CameraEvent> _cameraEvents(int cameraId) => cameraEventStreamController
.stream
.where((CameraEvent event) => event.cameraId == cameraId);
@override
Future<List<CameraDescription>> availableCameras() async {
try {
return (await _hostApi.getAvailableCameras())
.map(cameraDescriptionFromPlatform)
.toList();
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) => createCameraWithSettings(
cameraDescription,
MediaSettings(resolutionPreset: resolutionPreset, enableAudio: enableAudio),
);
@override
Future<int> createCameraWithSettings(
CameraDescription cameraDescription,
MediaSettings? mediaSettings,
) async {
try {
return await _hostApi.create(
cameraDescription.name,
PlatformMediaSettings(
resolutionPreset: _pigeonResolutionPreset(
mediaSettings?.resolutionPreset,
),
framesPerSecond: mediaSettings?.fps,
videoBitrate: mediaSettings?.videoBitrate,
audioBitrate: mediaSettings?.audioBitrate,
enableAudio: mediaSettings?.enableAudio ?? true,
),
);
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
@override
Future<void> initializeCamera(
int cameraId, {
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
}) async {
hostCameraHandlers.putIfAbsent(
cameraId,
() => HostCameraMessageHandler(cameraId, cameraEventStreamController),
);
final Completer<void> completer = Completer<void>();
unawaited(
onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) {
completer.complete();
}),
);
try {
await _hostApi.initialize(cameraId, _pigeonImageFormat(imageFormatGroup));
} on PlatformException catch (e, s) {
completer.completeError(CameraException(e.code, e.message), s);
}
return completer.future;
}
@override
Future<void> dispose(int cameraId) async {
final HostCameraMessageHandler? handler = hostCameraHandlers.remove(
cameraId,
);
handler?.dispose();
await _hostApi.dispose(cameraId);
}
@override
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraInitializedEvent>();
}
@override
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraResolutionChangedEvent>();
}
@override
Stream<CameraClosingEvent> onCameraClosing(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraClosingEvent>();
}
@override
Stream<CameraErrorEvent> onCameraError(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraErrorEvent>();
}
@override
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
return _cameraEvents(cameraId).whereType<VideoRecordedEvent>();
}
@override
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
return hostHandler.deviceEventStreamController.stream
.whereType<DeviceOrientationChangedEvent>();
}
@override
Future<void> lockCaptureOrientation(
int cameraId,
DeviceOrientation orientation,
) async {
await _hostApi.lockCaptureOrientation(
serializeDeviceOrientation(orientation),
);
}
@override
Future<void> unlockCaptureOrientation(int cameraId) async {
await _hostApi.unlockCaptureOrientation();
}
@override
Future<XFile> takePicture(int cameraId) async {
final String path = await _hostApi.takePicture();
return XFile(path);
}
@override
Future<void> prepareForVideoRecording() async {
await _hostApi.prepareForVideoRecording();
}
@override
Future<void> startVideoRecording(
int cameraId, {
Duration? maxVideoDuration,
}) async {
// Ignore maxVideoDuration, as it is unimplemented and deprecated.
return startVideoCapturing(VideoCaptureOptions(cameraId));
}
@override
Future<void> startVideoCapturing(VideoCaptureOptions options) async {
// Max video duration is currently not supported.
await _hostApi.startVideoRecording(options.streamCallback != null);
if (options.streamCallback != null) {
_frameStreamController = _createStreamController();
_frameStreamController!.stream.listen(options.streamCallback);
_startStreamListener();
}
}
@override
Future<XFile> stopVideoRecording(int cameraId) async {
final String path = await _hostApi.stopVideoRecording();
return XFile(path);
}
@override
Future<void> pauseVideoRecording(int cameraId) async {
await _hostApi.pauseVideoRecording();
}
@override
Future<void> resumeVideoRecording(int cameraId) async {
await _hostApi.resumeVideoRecording();
}
@override
bool supportsImageStreaming() => true;
@override
Stream<CameraImageData> onStreamedFrameAvailable(
int cameraId, {
CameraImageStreamOptions? options,
}) {
_frameStreamController = _createStreamController(
onListen: _onFrameStreamListen,
);
return _frameStreamController!.stream;
}
StreamController<CameraImageData> _createStreamController({
void Function()? onListen,
}) {
return StreamController<CameraImageData>(
onListen: onListen ?? () {},
onPause: _onFrameStreamPauseResume,
onResume: _onFrameStreamPauseResume,
onCancel: _onFrameStreamCancel,
);
}
void _onFrameStreamListen() {
_startPlatformStream();
}
Future<void> _startPlatformStream() async {
await _hostApi.startImageStream();
_startStreamListener();
}
void _startStreamListener() {
const EventChannel cameraEventChannel = EventChannel(
'plugins.flutter.io/camera_avfoundation/imageStream',
);
_platformImageStreamSubscription = cameraEventChannel
.receiveBroadcastStream()
.listen((dynamic imageData) {
try {
_hostApi.receivedImageStreamData();
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
_frameStreamController!.add(
cameraImageFromPlatformData(imageData as Map<dynamic, dynamic>),
);
});
}
FutureOr<void> _onFrameStreamCancel() async {
await _hostApi.stopImageStream();
await _platformImageStreamSubscription?.cancel();
_platformImageStreamSubscription = null;
_frameStreamController = null;
}
void _onFrameStreamPauseResume() {
throw CameraException(
'InvalidCall',
'Pause and resume are not supported for onStreamedFrameAvailable',
);
}
@override
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
await _hostApi.setFlashMode(_pigeonFlashMode(mode));
}
@override
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
await _hostApi.setExposureMode(_pigeonExposureMode(mode));
}
@override
Future<void> setExposurePoint(int cameraId, Point<double>? point) async {
assert(point == null || point.x >= 0 && point.x <= 1);
assert(point == null || point.y >= 0 && point.y <= 1);
await _hostApi.setExposurePoint(_pigeonPoint(point));
}
@override
Future<double> getMinExposureOffset(int cameraId) async {
return _hostApi.getMinExposureOffset();
}
@override
Future<double> getMaxExposureOffset(int cameraId) async {
return _hostApi.getMaxExposureOffset();
}
@override
Future<double> getExposureOffsetStepSize(int cameraId) async {
// iOS has no step size.
return 0;
}
@override
Future<double> setExposureOffset(int cameraId, double offset) async {
await _hostApi.setExposureOffset(offset);
// The platform API allows for implementations that have to adjust the
// target offset and return the actual offset used, but there is never
// adjustment in this implementation.
return offset;
}
@override
Future<void> setFocusMode(int cameraId, FocusMode mode) async {
await _hostApi.setFocusMode(_pigeonFocusMode(mode));
}
@override
Future<void> setFocusPoint(int cameraId, Point<double>? point) async {
assert(point == null || point.x >= 0 && point.x <= 1);
assert(point == null || point.y >= 0 && point.y <= 1);
await _hostApi.setFocusPoint(_pigeonPoint(point));
}
@override
Future<double> getMaxZoomLevel(int cameraId) async {
return _hostApi.getMaxZoomLevel();
}
@override
Future<double> getMinZoomLevel(int cameraId) async {
return _hostApi.getMinZoomLevel();
}
@override
Future<void> setZoomLevel(int cameraId, double zoom) async {
try {
await _hostApi.setZoomLevel(zoom);
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
@override
Future<void> pausePreview(int cameraId) async {
await _hostApi.pausePreview();
}
@override
Future<void> resumePreview(int cameraId) async {
await _hostApi.resumePreview();
}
@override
Future<void> setDescriptionWhileRecording(
CameraDescription description,
) async {
await _hostApi.updateDescriptionWhileRecording(description.name);
}
@override
Future<void> setImageFileFormat(int cameraId, ImageFileFormat format) async {
await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format));
}
@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
}
/// Returns an [FocusMode]'s Pigeon representation.
PlatformFocusMode _pigeonFocusMode(FocusMode mode) {
switch (mode) {
case FocusMode.locked:
return PlatformFocusMode.locked;
case FocusMode.auto:
return PlatformFocusMode.auto;
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// ignore: dead_code
return PlatformFocusMode.auto;
}
/// Returns an [ExposureMode]'s Pigeon representation.
PlatformExposureMode _pigeonExposureMode(ExposureMode mode) {
switch (mode) {
case ExposureMode.locked:
return PlatformExposureMode.locked;
case ExposureMode.auto:
return PlatformExposureMode.auto;
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// ignore: dead_code
return PlatformExposureMode.auto;
}
/// Returns a [FlashMode]'s Pigeon representation.
PlatformFlashMode _pigeonFlashMode(FlashMode flashMode) {
switch (flashMode) {
case FlashMode.off:
return PlatformFlashMode.off;
case FlashMode.auto:
return PlatformFlashMode.auto;
case FlashMode.always:
return PlatformFlashMode.always;
case FlashMode.torch:
return PlatformFlashMode.torch;
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// ignore: dead_code
return PlatformFlashMode.off;
}
/// Returns a [ResolutionPreset]'s Pigeon representation.
PlatformResolutionPreset _pigeonResolutionPreset(
ResolutionPreset? resolutionPreset,
) {
if (resolutionPreset == null) {
// Provide a default if one isn't provided, since the native side needs
// to set something.
return PlatformResolutionPreset.high;
}
switch (resolutionPreset) {
case ResolutionPreset.max:
return PlatformResolutionPreset.max;
case ResolutionPreset.ultraHigh:
return PlatformResolutionPreset.ultraHigh;
case ResolutionPreset.veryHigh:
return PlatformResolutionPreset.veryHigh;
case ResolutionPreset.high:
return PlatformResolutionPreset.high;
case ResolutionPreset.medium:
return PlatformResolutionPreset.medium;
case ResolutionPreset.low:
return PlatformResolutionPreset.low;
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// ignore: dead_code
return PlatformResolutionPreset.max;
}
/// Returns an [ImageFormatGroup]'s Pigeon representation.
PlatformImageFormatGroup _pigeonImageFormat(ImageFormatGroup format) {
switch (format) {
// "unknown" is used to indicate the default.
case ImageFormatGroup.unknown:
case ImageFormatGroup.bgra8888:
return PlatformImageFormatGroup.bgra8888;
case ImageFormatGroup.yuv420:
return PlatformImageFormatGroup.yuv420;
case ImageFormatGroup.jpeg:
case ImageFormatGroup.nv21:
// Fall through.
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of
// doing fallback, when a specific unsupported format is requested. This
// would require a breaking change at this layer and the app-facing layer.
return PlatformImageFormatGroup.bgra8888;
}
/// Returns an [ImageFileFormat]'s Pigeon representation.
PlatformImageFileFormat _pigeonImageFileFormat(ImageFileFormat format) {
switch (format) {
case ImageFileFormat.heif:
return PlatformImageFileFormat.heif;
case ImageFileFormat.jpeg:
return PlatformImageFileFormat.jpeg;
}
// The enum comes from a different package, which could get a new value at
// any time, so provide a fallback that ensures this won't break when used
// with a version that contains new values. This is deliberately outside
// the switch rather than a `default` so that the linter will flag the
// switch as needing an update.
// TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of
// doing fallback, when a specific unsupported format is requested. This
// would require a breaking change at this layer and the app-facing layer.
// ignore: dead_code
return PlatformImageFileFormat.jpeg;
}
/// Returns a [Point]s Pigeon representation.
PlatformPoint? _pigeonPoint(Point<double>? point) {
if (point == null) {
return null;
}
return PlatformPoint(x: point.x, y: point.y);
}
}
/// Callback handler for device-level events from the platform host.
@visibleForTesting
class HostDeviceMessageHandler implements CameraGlobalEventApi {
/// Creates a new handler and registers it to listen to its platform channel.
HostDeviceMessageHandler() {
CameraGlobalEventApi.setUp(this);
}
/// The controller used to broadcast general device events coming from the
/// host platform.
///
/// It is a `broadcast` because multiple controllers will connect to
/// different stream views of this Controller.
final StreamController<DeviceEvent> deviceEventStreamController =
StreamController<DeviceEvent>.broadcast();
@override
void deviceOrientationChanged(PlatformDeviceOrientation orientation) {
deviceEventStreamController.add(
DeviceOrientationChangedEvent(deviceOrientationFromPlatform(orientation)),
);
}
}
/// Callback handler for camera-level events from the platform host.
@visibleForTesting
class HostCameraMessageHandler implements CameraEventApi {
/// Creates a new handler that listens for events from camera [cameraId], and
/// broadcasts them to [streamController].
HostCameraMessageHandler(this.cameraId, this.streamController) {
CameraEventApi.setUp(this, messageChannelSuffix: cameraId.toString());
}
/// Removes the handler for native messages.
void dispose() {
CameraEventApi.setUp(null, messageChannelSuffix: cameraId.toString());
}
/// The camera ID this handler listens for events from.
final int cameraId;
/// The controller used to broadcast camera events coming from the
/// host platform.
final StreamController<CameraEvent> streamController;
@override
void error(String message) {
streamController.add(CameraErrorEvent(cameraId, message));
}
@override
void initialized(PlatformCameraState initialState) {
streamController.add(
CameraInitializedEvent(
cameraId,
initialState.previewSize.width,
initialState.previewSize.height,
exposureModeFromPlatform(initialState.exposureMode),
initialState.exposurePointSupported,
focusModeFromPlatform(initialState.focusMode),
initialState.focusPointSupported,
),
);
}
}