Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camera] Initial iOS Pigeon conversion #6553

Merged
merged 3 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
Contributor 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
Contributor 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
Contributor 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
35 changes: 20 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,10 @@ - (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 {
dispatch_async(self.captureSessionQueue, ^{
NSMutableArray *discoveryDevices =
[@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ]
mutableCopy];
Expand All @@ -117,29 +121,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