Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.15+2

* Converts camera query to Pigeon.

## 0.9.15+1

* Simplifies internal handling of method channel responses.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ @implementation AvailableCamerasTest

- (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone {
CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change to the utility method allows using waitForExpectationsWithTimeout: instead of having to list every expectation.


// iPhone 13 Cameras:
AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]);
Expand Down Expand Up @@ -55,29 +54,26 @@ - (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone {
}
OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]);

// Set up method call
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"availableCameras"
arguments:nil];

__block id resultValue;
[camera handleMethodCallAsync:call
result:^(id _Nullable result) {
resultValue = result;
[expectation fulfill];
}];
__block NSArray<FCPPlatformCameraDescription *> *resultValue;
[camera
availableCamerasWithCompletion:^(NSArray<FCPPlatformCameraDescription *> *_Nullable result,
FlutterError *_Nullable error) {
XCTAssertNil(error);
resultValue = result;
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:30 handler:nil];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we weren't actually using the expectation before, but now it's necessary because the queue dispatch isn't being bypassed in the test like it was before, so the block above is no longer run synchronously in the test.


// Verify the result
NSDictionary *dictionaryResult = (NSDictionary *)resultValue;
if (@available(iOS 13.0, *)) {
XCTAssertTrue([dictionaryResult count] == 4);
XCTAssertEqual(resultValue.count, 4);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a quality of life improvement for when the test fails. (I had a failure initially because I didn't add the wait, so resultValue was nil, and I got a failure with no actual info.)

} else {
XCTAssertTrue([dictionaryResult count] == 3);
XCTAssertEqual(resultValue.count, 3);
}
}
- (void)testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone {
CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"];

// iPhone 8 Cameras:
AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]);
Expand Down Expand Up @@ -105,20 +101,19 @@ - (void)testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone {
[cameras addObjectsFromArray:@[ wideAngleCamera, frontFacingCamera ]];
OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]);

// Set up method call
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"availableCameras"
arguments:nil];

__block id resultValue;
[camera handleMethodCallAsync:call
result:^(id _Nullable result) {
resultValue = result;
[expectation fulfill];
}];
__block NSArray<FCPPlatformCameraDescription *> *resultValue;
[camera
availableCamerasWithCompletion:^(NSArray<FCPPlatformCameraDescription *> *_Nullable result,
FlutterError *_Nullable error) {
XCTAssertNil(error);
resultValue = result;
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:30 handler:nil];

// Verify the result
NSDictionary *dictionaryResult = (NSDictionary *)resultValue;
XCTAssertTrue([dictionaryResult count] == 2);
XCTAssertEqual(resultValue.count, 2);
;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@

#import <Flutter/Flutter.h>

@interface CameraPlugin : NSObject <FlutterPlugin>
#import "messages.g.h"

@interface CameraPlugin : NSObject <FlutterPlugin, FCPCameraApi>
@end
37 changes: 22 additions & 15 deletions packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "FLTThreadSafeMethodChannel.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]
Expand All @@ -35,6 +36,7 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures]
messenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
SetUpFCPCameraApi([registrar messenger], instance);
}

- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
Expand Down Expand Up @@ -104,8 +106,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
});
}

- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"availableCameras" isEqualToString:call.method]) {
- (void)availableCamerasWithCompletion:
(nonnull void (^)(NSArray<FCPPlatformCameraDescription *> *_Nullable,
FlutterError *_Nullable))completion {
// This doesn't interact with FLTCam, so can use an arbitrary thread rather than
// captureSessionQueue. It should still not be done on the main thread, however.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will unfortunately need to have the thread dispatch in every individual method in the Pigeon version (instead of handleMethodCall doing it for everything at once). Almost everything needs to use captureSessionQueue, since FLTCam sometimes needs to redispatch to that queue in internal callbacks to interact with instance state, which means we can't just use task queues.

(Also IIRC task queues aren't implemented for macOS, and we want to enable this plugin for macOS at some point.)

It'll just be a couple of lines in each method though, so it shouldn't be too bad. I think we can simplify when we migrate to Swift.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's still better to use captureSessionQueue for all session configurations, in case Apple made any assumptions in its internal implementation (e.g. I wouldn't be surprised if multiple capture session related API access some common internal states).

(AVCaptureDeviceDiscoverySession's API doc doesn't say anything, but Apple's sample project uses this convention)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It hadn't occurred to me that in theory the unique device IDs could be thread-specific; that would be weird, but not implausible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I bet we should be fine either way, but just wanna be extra safe here.

NSMutableArray *discoveryDevices =
[@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ]
mutableCopy];
Expand All @@ -117,29 +123,30 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
NSArray<AVCaptureDevice *> *devices = discoverySession.devices;
NSMutableArray<NSDictionary<NSString *, NSObject *> *> *reply =
NSMutableArray<FCPPlatformCameraDescription *> *reply =
[[NSMutableArray alloc] initWithCapacity:devices.count];
for (AVCaptureDevice *device in devices) {
NSString *lensFacing;
switch ([device position]) {
FCPPlatformCameraLensDirection lensFacing;
switch (device.position) {
case AVCaptureDevicePositionBack:
lensFacing = @"back";
lensFacing = FCPPlatformCameraLensDirectionBack;
break;
case AVCaptureDevicePositionFront:
lensFacing = @"front";
lensFacing = FCPPlatformCameraLensDirectionFront;
break;
case AVCaptureDevicePositionUnspecified:
lensFacing = @"external";
lensFacing = FCPPlatformCameraLensDirectionExternal;
break;
}
[reply addObject:@{
@"name" : [device uniqueID],
@"lensFacing" : lensFacing,
@"sensorOrientation" : @90,
}];
[reply addObject:[FCPPlatformCameraDescription makeWithName:device.uniqueID
lensDirection:lensFacing]];
}
result(reply);
} else if ([@"create" isEqualToString:call.method]) {
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];
Expand Down
60 changes: 60 additions & 0 deletions packages/camera/camera_avfoundation/ios/Classes/messages.g.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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.
// Autogenerated from Pigeon (v18.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#import <Foundation/Foundation.h>

@protocol FlutterBinaryMessenger;
@protocol FlutterMessageCodec;
@class FlutterError;
@class FlutterStandardTypedData;

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, FCPPlatformCameraLensDirection) {
/// Front facing camera (a user looking at the screen is seen by the camera).
FCPPlatformCameraLensDirectionFront = 0,
/// Back facing camera (a user looking at the screen is not seen by the camera).
FCPPlatformCameraLensDirectionBack = 1,
/// External camera which may not be mounted to the device.
FCPPlatformCameraLensDirectionExternal = 2,
};

/// Wrapper for FCPPlatformCameraLensDirection to allow for nullability.
@interface FCPPlatformCameraLensDirectionBox : NSObject
@property(nonatomic, assign) FCPPlatformCameraLensDirection value;
- (instancetype)initWithValue:(FCPPlatformCameraLensDirection)value;
@end

@class FCPPlatformCameraDescription;

@interface FCPPlatformCameraDescription : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)makeWithName:(NSString *)name
lensDirection:(FCPPlatformCameraLensDirection)lensDirection;
/// The name of the camera device.
@property(nonatomic, copy) NSString *name;
/// The direction the camera is facing.
@property(nonatomic, assign) FCPPlatformCameraLensDirection lensDirection;
@end

/// The codec used by FCPCameraApi.
NSObject<FlutterMessageCodec> *FCPCameraApiGetCodec(void);

@protocol FCPCameraApi
/// Returns the list of available cameras.
- (void)availableCamerasWithCompletion:(void (^)(NSArray<FCPPlatformCameraDescription *> *_Nullable,
FlutterError *_Nullable))completion;
@end

extern void SetUpFCPCameraApi(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<FCPCameraApi> *_Nullable api);

extern void SetUpFCPCameraApiWithSuffix(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<FCPCameraApi> *_Nullable api,
NSString *messageChannelSuffix);

NS_ASSUME_NONNULL_END
155 changes: 155 additions & 0 deletions packages/camera/camera_avfoundation/ios/Classes/messages.g.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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.
// Autogenerated from Pigeon (v18.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#import "messages.g.h"

#if TARGET_OS_OSX
#import <FlutterMacOS/FlutterMacOS.h>
#else
#import <Flutter/Flutter.h>
#endif

#if !__has_feature(objc_arc)
#error File requires ARC to be enabled.
#endif

static NSArray *wrapResult(id result, FlutterError *error) {
if (error) {
return @[
error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null]
];
}
return @[ result ?: [NSNull null] ];
}

static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
id result = array[key];
return (result == [NSNull null]) ? nil : result;
}

@implementation FCPPlatformCameraLensDirectionBox
- (instancetype)initWithValue:(FCPPlatformCameraLensDirection)value {
self = [super init];
if (self) {
_value = value;
}
return self;
}
@end

@interface FCPPlatformCameraDescription ()
+ (FCPPlatformCameraDescription *)fromList:(NSArray *)list;
+ (nullable FCPPlatformCameraDescription *)nullableFromList:(NSArray *)list;
- (NSArray *)toList;
@end

@implementation FCPPlatformCameraDescription
+ (instancetype)makeWithName:(NSString *)name
lensDirection:(FCPPlatformCameraLensDirection)lensDirection {
FCPPlatformCameraDescription *pigeonResult = [[FCPPlatformCameraDescription alloc] init];
pigeonResult.name = name;
pigeonResult.lensDirection = lensDirection;
return pigeonResult;
}
+ (FCPPlatformCameraDescription *)fromList:(NSArray *)list {
FCPPlatformCameraDescription *pigeonResult = [[FCPPlatformCameraDescription alloc] init];
pigeonResult.name = GetNullableObjectAtIndex(list, 0);
pigeonResult.lensDirection = [GetNullableObjectAtIndex(list, 1) integerValue];
return pigeonResult;
}
+ (nullable FCPPlatformCameraDescription *)nullableFromList:(NSArray *)list {
return (list) ? [FCPPlatformCameraDescription fromList:list] : nil;
}
- (NSArray *)toList {
return @[
self.name ?: [NSNull null],
@(self.lensDirection),
];
}
@end

@interface FCPCameraApiCodecReader : FlutterStandardReader
@end
@implementation FCPCameraApiCodecReader
- (nullable id)readValueOfType:(UInt8)type {
switch (type) {
case 128:
return [FCPPlatformCameraDescription fromList:[self readValue]];
default:
return [super readValueOfType:type];
}
}
@end

@interface FCPCameraApiCodecWriter : FlutterStandardWriter
@end
@implementation FCPCameraApiCodecWriter
- (void)writeValue:(id)value {
if ([value isKindOfClass:[FCPPlatformCameraDescription class]]) {
[self writeByte:128];
[self writeValue:[value toList]];
} else {
[super writeValue:value];
}
}
@end

@interface FCPCameraApiCodecReaderWriter : FlutterStandardReaderWriter
@end
@implementation FCPCameraApiCodecReaderWriter
- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
return [[FCPCameraApiCodecWriter alloc] initWithData:data];
}
- (FlutterStandardReader *)readerWithData:(NSData *)data {
return [[FCPCameraApiCodecReader alloc] initWithData:data];
}
@end

NSObject<FlutterMessageCodec> *FCPCameraApiGetCodec(void) {
static FlutterStandardMessageCodec *sSharedObject = nil;
static dispatch_once_t sPred = 0;
dispatch_once(&sPred, ^{
FCPCameraApiCodecReaderWriter *readerWriter = [[FCPCameraApiCodecReaderWriter alloc] init];
sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
});
return sSharedObject;
}

void SetUpFCPCameraApi(id<FlutterBinaryMessenger> binaryMessenger, NSObject<FCPCameraApi> *api) {
SetUpFCPCameraApiWithSuffix(binaryMessenger, api, @"");
}

void SetUpFCPCameraApiWithSuffix(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<FCPCameraApi> *api, NSString *messageChannelSuffix) {
messageChannelSuffix = messageChannelSuffix.length > 0
? [NSString stringWithFormat:@".%@", messageChannelSuffix]
: @"";
/// Returns the list of available cameras.
{
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
initWithName:[NSString stringWithFormat:@"%@%@",
@"dev.flutter.pigeon.camera_avfoundation."
@"CameraApi.getAvailableCameras",
messageChannelSuffix]
binaryMessenger:binaryMessenger
codec:FCPCameraApiGetCodec()];
if (api) {
NSCAssert(
[api respondsToSelector:@selector(availableCamerasWithCompletion:)],
@"FCPCameraApi api (%@) doesn't respond to @selector(availableCamerasWithCompletion:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
[api availableCamerasWithCompletion:^(
NSArray<FCPPlatformCameraDescription *> *_Nullable output,
FlutterError *_Nullable error) {
callback(wrapResult(output, error));
}];
}];
} else {
[channel setMessageHandler:nil];
}
}
}
Loading