blob: 90a124ebafd47ee37e9bd5c671329cc98c48c527 [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 "CameraPlugin.h"
#import "CameraPlugin_Test.h"
@import AVFoundation;
#import "CameraPermissionUtils.h"
#import "CameraProperties.h"
#import "FLTCam.h"
#import "FLTThreadSafeEventChannel.h"
#import "FLTThreadSafeTextureRegistry.h"
#import "QueueUtils.h"
#import "messages.g.h"
static FlutterError *FlutterErrorFromNSError(NSError *error) {
return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code]
message:error.localizedDescription
details:error.domain];
}
@interface CameraPlugin ()
@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry;
@property(readonly, nonatomic) NSObject<FlutterBinaryMessenger> *messenger;
@property(nonatomic) FCPCameraGlobalEventApi *globalEventAPI;
@end
@implementation CameraPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera_avfoundation"
binaryMessenger:[registrar messenger]];
CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures]
messenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
SetUpFCPCameraApi([registrar messenger], instance);
}
- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
messenger:(NSObject<FlutterBinaryMessenger> *)messenger {
return
[self initWithRegistry:registry
messenger:messenger
globalAPI:[[FCPCameraGlobalEventApi alloc] initWithBinaryMessenger:messenger]];
}
- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
messenger:(NSObject<FlutterBinaryMessenger> *)messenger
globalAPI:(FCPCameraGlobalEventApi *)globalAPI {
self = [super init];
NSAssert(self, @"super init cannot be nil");
_registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry];
_messenger = messenger;
_globalEventAPI = globalAPI;
_captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL);
dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific,
(void *)FLTCaptureSessionQueueSpecific, NULL);
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:[UIDevice currentDevice]];
return self;
}
- (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
[UIDevice.currentDevice endGeneratingDeviceOrientationNotifications];
}
- (void)orientationChanged:(NSNotification *)note {
UIDevice *device = note.object;
UIDeviceOrientation orientation = device.orientation;
if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown) {
// Do not change when oriented flat.
return;
}
__weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
// `FLTCam::setDeviceOrientation` must be called on capture session queue.
[weakSelf.camera setDeviceOrientation:orientation];
// `CameraPlugin::sendDeviceOrientation` can be called on any queue.
[weakSelf sendDeviceOrientation:orientation];
});
}
- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.globalEventAPI
deviceOrientationChangedOrientation:FCPGetPigeonDeviceOrientationForOrientation(orientation)
completion:^(FlutterError *error){
// Ignore errors; this is essentially a broadcast stream, and
// it's fine if the other end
// doesn't receive the message (e.g., if it doesn't currently
// have a listener set up).
}];
});
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
// Invoke the plugin on another dispatch queue to avoid blocking the UI.
__weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
[weakSelf handleMethodCallAsync:call result:result];
});
}
- (void)availableCamerasWithCompletion:
(nonnull void (^)(NSArray<FCPPlatformCameraDescription *> *_Nullable,
FlutterError *_Nullable))completion {
dispatch_async(self.captureSessionQueue, ^{
NSMutableArray *discoveryDevices =
[@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ]
mutableCopy];
if (@available(iOS 13.0, *)) {
[discoveryDevices addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera];
}
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:discoveryDevices
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
NSArray<AVCaptureDevice *> *devices = discoverySession.devices;
NSMutableArray<FCPPlatformCameraDescription *> *reply =
[[NSMutableArray alloc] initWithCapacity:devices.count];
for (AVCaptureDevice *device in devices) {
FCPPlatformCameraLensDirection lensFacing;
switch (device.position) {
case AVCaptureDevicePositionBack:
lensFacing = FCPPlatformCameraLensDirectionBack;
break;
case AVCaptureDevicePositionFront:
lensFacing = FCPPlatformCameraLensDirectionFront;
break;
case AVCaptureDevicePositionUnspecified:
lensFacing = FCPPlatformCameraLensDirectionExternal;
break;
}
[reply addObject:[FCPPlatformCameraDescription makeWithName:device.uniqueID
lensDirection:lensFacing]];
}
completion(reply, nil);
});
}
- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"create" isEqualToString:call.method]) {
[self handleCreateMethodCall:call result:result];
} else if ([@"startImageStream" isEqualToString:call.method]) {
[_camera startImageStreamWithMessenger:_messenger];
result(nil);
} else if ([@"stopImageStream" isEqualToString:call.method]) {
[_camera stopImageStream];
result(nil);
} else if ([@"receivedImageStreamData" isEqualToString:call.method]) {
[_camera receivedImageStreamData];
result(nil);
} else {
NSDictionary *argsMap = call.arguments;
NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue;
if ([@"initialize" isEqualToString:call.method]) {
NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]);
[_camera setVideoFormat:FLTGetVideoFormatFromString(videoFormatValue)];
__weak CameraPlugin *weakSelf = self;
_camera.onFrameAvailable = ^{
if (![weakSelf.camera isPreviewPaused]) {
[weakSelf.registry textureFrameAvailable:cameraId];
}
};
_camera.dartAPI = [[FCPCameraEventApi alloc]
initWithBinaryMessenger:_messenger
messageChannelSuffix:[NSString stringWithFormat:@"%ld", cameraId]];
[_camera reportInitializationState];
[self sendDeviceOrientation:[UIDevice currentDevice].orientation];
[_camera start];
result(nil);
} else if ([@"takePicture" isEqualToString:call.method]) {
[_camera captureToFile:result];
} else if ([@"dispose" isEqualToString:call.method]) {
[_registry unregisterTexture:cameraId];
[_camera close];
result(nil);
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
[self.camera setUpCaptureSessionForAudio];
result(nil);
} else if ([@"startVideoRecording" isEqualToString:call.method]) {
BOOL enableStream = [call.arguments[@"enableStream"] boolValue];
if (enableStream) {
[_camera startVideoRecordingWithResult:result messengerForStreaming:_messenger];
} else {
[_camera startVideoRecordingWithResult:result];
}
} else if ([@"stopVideoRecording" isEqualToString:call.method]) {
[_camera stopVideoRecordingWithResult:result];
} else if ([@"pauseVideoRecording" isEqualToString:call.method]) {
[_camera pauseVideoRecordingWithResult:result];
} else if ([@"resumeVideoRecording" isEqualToString:call.method]) {
[_camera resumeVideoRecordingWithResult:result];
} else if ([@"getMaxZoomLevel" isEqualToString:call.method]) {
[_camera getMaxZoomLevelWithResult:result];
} else if ([@"getMinZoomLevel" isEqualToString:call.method]) {
[_camera getMinZoomLevelWithResult:result];
} else if ([@"setZoomLevel" isEqualToString:call.method]) {
CGFloat zoom = ((NSNumber *)argsMap[@"zoom"]).floatValue;
[_camera setZoomLevel:zoom Result:result];
} else if ([@"setFlashMode" isEqualToString:call.method]) {
[_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]];
} else if ([@"setExposureMode" isEqualToString:call.method]) {
[_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]];
} else if ([@"setExposurePoint" isEqualToString:call.method]) {
BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue;
double x = 0.5;
double y = 0.5;
if (!reset) {
x = ((NSNumber *)call.arguments[@"x"]).doubleValue;
y = ((NSNumber *)call.arguments[@"y"]).doubleValue;
}
[_camera setExposurePointWithResult:result x:x y:y];
} else if ([@"getMinExposureOffset" isEqualToString:call.method]) {
result(@(_camera.captureDevice.minExposureTargetBias));
} else if ([@"getMaxExposureOffset" isEqualToString:call.method]) {
result(@(_camera.captureDevice.maxExposureTargetBias));
} else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) {
result(@(0.0));
} else if ([@"setExposureOffset" isEqualToString:call.method]) {
[_camera setExposureOffsetWithResult:result
offset:((NSNumber *)call.arguments[@"offset"]).doubleValue];
} else if ([@"lockCaptureOrientation" isEqualToString:call.method]) {
[_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]];
} else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) {
[_camera unlockCaptureOrientationWithResult:result];
} else if ([@"setFocusMode" isEqualToString:call.method]) {
[_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]];
} else if ([@"setFocusPoint" isEqualToString:call.method]) {
BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue;
double x = 0.5;
double y = 0.5;
if (!reset) {
x = ((NSNumber *)call.arguments[@"x"]).doubleValue;
y = ((NSNumber *)call.arguments[@"y"]).doubleValue;
}
[_camera setFocusPointWithResult:result x:x y:y];
} else if ([@"pausePreview" isEqualToString:call.method]) {
[_camera pausePreviewWithResult:result];
} else if ([@"resumePreview" isEqualToString:call.method]) {
[_camera resumePreviewWithResult:result];
} else if ([@"setDescriptionWhileRecording" isEqualToString:call.method]) {
[_camera setDescriptionWhileRecording:(call.arguments[@"cameraName"]) result:result];
} else if ([@"setImageFileFormat" isEqualToString:call.method]) {
NSString *fileFormat = call.arguments[@"fileFormat"];
[_camera setImageFileFormat:FCPGetFileFormatFromString(fileFormat)];
} else {
result(FlutterMethodNotImplemented);
}
}
}
- (void)handleCreateMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
// Create FLTCam only if granted camera access (and audio access if audio is enabled)
__weak typeof(self) weakSelf = self;
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
if (error) {
result(error);
} else {
// Request audio permission on `create` call with `enableAudio` argument instead of the
// `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is
// optional, and used as a workaround to fix a missing frame issue on iOS.
BOOL audioEnabled = [call.arguments[@"enableAudio"] boolValue];
if (audioEnabled) {
// Setup audio capture session only if granted audio access.
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
// cannot use the outter `strongSelf`
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
if (error) {
result(error);
} else {
[strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result];
}
});
} else {
[strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result];
}
}
});
}
// Returns number value if provided and positive, or nil.
// Used to parse values like framerates and bitrates, that are positive by nature.
// nil allows to ignore unsupported values.
+ (NSNumber *)positiveNumberValueOrNilForArgument:(NSString *)argument
fromMethod:(FlutterMethodCall *)flutterMethodCall
error:(NSError **)error {
id value = flutterMethodCall.arguments[argument];
if (!value || [value isEqual:[NSNull null]]) {
return nil;
}
if (![value isKindOfClass:[NSNumber class]]) {
if (error) {
*error = [NSError errorWithDomain:@"ArgumentError"
code:0
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"%@ should be a number", argument]
}];
}
return nil;
}
NSNumber *number = (NSNumber *)value;
if (isnan([number doubleValue])) {
if (error) {
*error = [NSError errorWithDomain:@"ArgumentError"
code:0
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"%@ should not be a nan", argument]
}];
}
return nil;
}
if ([number doubleValue] <= 0.0) {
if (error) {
*error = [NSError errorWithDomain:@"ArgumentError"
code:0
userInfo:@{
NSLocalizedDescriptionKey : [NSString
stringWithFormat:@"%@ should be a positive number", argument]
}];
}
return nil;
}
return number;
}
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FlutterResult)result {
__weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
NSString *cameraName = createMethodCall.arguments[@"cameraName"];
NSError *error;
NSNumber *framesPerSecond = [CameraPlugin positiveNumberValueOrNilForArgument:@"fps"
fromMethod:createMethodCall
error:&error];
if (error) {
result(FlutterErrorFromNSError(error));
return;
}
NSNumber *videoBitrate = [CameraPlugin positiveNumberValueOrNilForArgument:@"videoBitrate"
fromMethod:createMethodCall
error:&error];
if (error) {
result(FlutterErrorFromNSError(error));
return;
}
NSNumber *audioBitrate = [CameraPlugin positiveNumberValueOrNilForArgument:@"audioBitrate"
fromMethod:createMethodCall
error:&error];
if (error) {
result(FlutterErrorFromNSError(error));
return;
}
NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"];
NSNumber *enableAudio = createMethodCall.arguments[@"enableAudio"];
FLTCamMediaSettings *mediaSettings =
[[FLTCamMediaSettings alloc] initWithFramesPerSecond:framesPerSecond
videoBitrate:videoBitrate
audioBitrate:audioBitrate
enableAudio:[enableAudio boolValue]];
FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper =
[[FLTCamMediaSettingsAVWrapper alloc] init];
FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName
resolutionPreset:resolutionPreset
mediaSettings:mediaSettings
mediaSettingsAVWrapper:mediaSettingsAVWrapper
orientation:[[UIDevice currentDevice] orientation]
captureSessionQueue:strongSelf.captureSessionQueue
error:&error];
if (error) {
result(FlutterErrorFromNSError(error));
} else {
if (strongSelf.camera) {
[strongSelf.camera close];
}
strongSelf.camera = cam;
[strongSelf.registry registerTexture:cam
completion:^(int64_t textureId) {
result(@{
@"cameraId" : @(textureId),
});
}];
}
});
}
@end