// Copyright 2013 The Flutter Authors // 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 cameraEventStreamController = StreamController.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 hostCameraHandlers = {}; // The stream to receive frames from the native code. StreamSubscription? _platformImageStreamSubscription; // The stream for vending frames to platform interface clients. StreamController? _frameStreamController; Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream.where((CameraEvent event) => event.cameraId == cameraId); @override Future> availableCameras() async { try { return (await _hostApi.getAvailableCameras()).map(cameraDescriptionFromPlatform).toList(); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } @override Future createCamera( CameraDescription cameraDescription, ResolutionPreset? resolutionPreset, { bool enableAudio = false, }) => createCameraWithSettings( cameraDescription, MediaSettings(resolutionPreset: resolutionPreset, enableAudio: enableAudio), ); @override Future 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 initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) async { hostCameraHandlers.putIfAbsent( cameraId, () => HostCameraMessageHandler(cameraId, cameraEventStreamController), ); final completer = Completer(); 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 dispose(int cameraId) async { final HostCameraMessageHandler? handler = hostCameraHandlers.remove(cameraId); handler?.dispose(); await _hostApi.dispose(cameraId); } @override Stream onCameraInitialized(int cameraId) { return _cameraEvents(cameraId).whereType(); } @override Stream onCameraResolutionChanged(int cameraId) { return _cameraEvents(cameraId).whereType(); } @override Stream onCameraClosing(int cameraId) { return _cameraEvents(cameraId).whereType(); } @override Stream onCameraError(int cameraId) { return _cameraEvents(cameraId).whereType(); } @override Stream onVideoRecordedEvent(int cameraId) { return _cameraEvents(cameraId).whereType(); } @override Stream onDeviceOrientationChanged() { return hostHandler.deviceEventStreamController.stream .whereType(); } @override Future lockCaptureOrientation(int cameraId, DeviceOrientation orientation) async { await _hostApi.lockCaptureOrientation(serializeDeviceOrientation(orientation)); } @override Future unlockCaptureOrientation(int cameraId) async { await _hostApi.unlockCaptureOrientation(); } @override Future takePicture(int cameraId) async { final String path = await _hostApi.takePicture(); return XFile(path); } @override Future prepareForVideoRecording() async { await _hostApi.prepareForVideoRecording(); } @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { // Ignore maxVideoDuration, as it is unimplemented and deprecated. return startVideoCapturing(VideoCaptureOptions(cameraId)); } @override Future 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 stopVideoRecording(int cameraId) async { final String path = await _hostApi.stopVideoRecording(); return XFile(path); } @override Future pauseVideoRecording(int cameraId) async { await _hostApi.pauseVideoRecording(); } @override Future resumeVideoRecording(int cameraId) async { await _hostApi.resumeVideoRecording(); } @override bool supportsImageStreaming() => true; @override Stream onStreamedFrameAvailable( int cameraId, { CameraImageStreamOptions? options, }) { _frameStreamController = _createStreamController(onListen: _onFrameStreamListen); return _frameStreamController!.stream; } StreamController _createStreamController({void Function()? onListen}) { return StreamController( onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); } void _onFrameStreamListen() { _startPlatformStream(); } Future _startPlatformStream() async { await _hostApi.startImageStream(); _startStreamListener(); } void _startStreamListener() { _platformImageStreamSubscription = imageDataStream().listen(( PlatformCameraImageData imageData, ) { try { _hostApi.receivedImageStreamData(); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } _frameStreamController!.add(cameraImageFromPlatformData(imageData)); }); } FutureOr _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 setFlashMode(int cameraId, FlashMode mode) async { await _hostApi.setFlashMode(_pigeonFlashMode(mode)); } @override Future setExposureMode(int cameraId, ExposureMode mode) async { await _hostApi.setExposureMode(_pigeonExposureMode(mode)); } @override Future setExposurePoint(int cameraId, Point? 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 getMinExposureOffset(int cameraId) async { return _hostApi.getMinExposureOffset(); } @override Future getMaxExposureOffset(int cameraId) async { return _hostApi.getMaxExposureOffset(); } @override Future getExposureOffsetStepSize(int cameraId) async { // iOS has no step size. return 0; } @override Future 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 setFocusMode(int cameraId, FocusMode mode) async { await _hostApi.setFocusMode(_pigeonFocusMode(mode)); } @override Future setFocusPoint(int cameraId, Point? 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 getMaxZoomLevel(int cameraId) async { return _hostApi.getMaxZoomLevel(); } @override Future getMinZoomLevel(int cameraId) async { return _hostApi.getMinZoomLevel(); } @override Future setZoomLevel(int cameraId, double zoom) async { try { await _hostApi.setZoomLevel(zoom); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } @override Future setVideoStabilizationMode(int cameraId, VideoStabilizationMode mode) async { try { final Map availableModes = await _getSupportedVideoStabilizationModeMap(cameraId); final PlatformVideoStabilizationMode? platformMode = availableModes[mode]; if (platformMode == null) { throw ArgumentError('Unavailable video stabilization mode.', 'mode'); } await _hostApi.setVideoStabilizationMode(platformMode); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } @override Future> getSupportedVideoStabilizationModes(int cameraId) async { return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys; } Future> _getSupportedVideoStabilizationModeMap(int cameraId) async { final ret = {}; for (final VideoStabilizationMode mode in VideoStabilizationMode.values) { final PlatformVideoStabilizationMode? platformMode = _pigeonVideoStabilizationMode(mode); if (platformMode != null) { final bool isSupported = await _hostApi.isVideoStabilizationModeSupported(platformMode); if (isSupported) { ret[mode] = platformMode; } } } return ret; } @override Future pausePreview(int cameraId) async { await _hostApi.pausePreview(); } @override Future resumePreview(int cameraId) async { await _hostApi.resumePreview(); } @override Future setDescriptionWhileRecording(CameraDescription description) async { await _hostApi.updateDescriptionWhileRecording(description.name); } @override Future setImageFileFormat(int cameraId, ImageFileFormat format) async { await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format)); } @override Future setJpegImageQuality(int cameraId, int quality) async { await _hostApi.setJpegImageQuality(quality); } @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 a [VideoStabilizationMode]'s Pigeon representation. PlatformVideoStabilizationMode? _pigeonVideoStabilizationMode( VideoStabilizationMode videoStabilizationMode, ) { switch (videoStabilizationMode) { case VideoStabilizationMode.off: return PlatformVideoStabilizationMode.off; case VideoStabilizationMode.level1: return PlatformVideoStabilizationMode.standard; case VideoStabilizationMode.level2: return PlatformVideoStabilizationMode.cinematic; case VideoStabilizationMode.level3: return PlatformVideoStabilizationMode.cinematicExtended; } // 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 null; } /// 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? 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 deviceEventStreamController = StreamController.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 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, ), ); } }