Skip to content

Commit

Permalink
[camera] Initial iOS Pigeon conversion (#6553)
Browse files Browse the repository at this point in the history
Converts one platform channel method to Pigeon, setting up all the Pigeon plumbing and test scaffolding.

The Camera API surface is relatively large, so this lays a foundation for incremental conversion, minimizing the mixing of Pigeon setup with the individual method conversions.

Part of flutter/flutter#117905
  • Loading branch information
stuartmorgan authored Apr 17, 2024
1 parent 87ae14f commit ba1e24b
Show file tree
Hide file tree
Showing 15 changed files with 557 additions and 133 deletions.
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"];

// 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];

// Verify the result
NSDictionary *dictionaryResult = (NSDictionary *)resultValue;
if (@available(iOS 13.0, *)) {
XCTAssertTrue([dictionaryResult count] == 4);
XCTAssertEqual(resultValue.count, 4);
} 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

0 comments on commit ba1e24b

Please sign in to comment.