Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[camera]call engine API in main thread to fix a crash
Browse files Browse the repository at this point in the history
  • Loading branch information
hellohuanlin committed Jan 11, 2022
1 parent 94e80fc commit 959eb1a
Show file tree
Hide file tree
Showing 15 changed files with 472 additions and 18 deletions.
6 changes: 5 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## 0.9.4+5
## 0.9.4+6

* Fixed a crash in iOS when using image stream due to calling Flutter engine API on non-main thread.

## 0.9.4+5

* Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception.
* Fixes integration tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; };
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; };
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; };
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -74,6 +77,9 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = "<group>"; };
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = "<group>"; };
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = "<group>"; };
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = "<group>"; };
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -107,6 +113,9 @@
03BB766C2665316900CE5A93 /* Info.plist */,
033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */,
03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */,
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */,
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */,
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */,
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */,
Expand Down Expand Up @@ -239,7 +248,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1100;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
03BB76672665316900CE5A93 = {
Expand Down Expand Up @@ -378,6 +387,9 @@
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */,
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */,
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */,
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 camera;
@import XCTest;
#import <OCMock/OCMock.h>

@interface ThreadSafeEventChannelTests : XCTestCase
@property(nonatomic, strong) FLTThreadSafeEventChannel *channel;
@property(nonatomic, strong) XCTestExpectation *mainThreadExpectation;
@end

@implementation ThreadSafeEventChannelTests

- (void)setUp {
[super setUp];
id mockEventChannel = OCMClassMock([FlutterEventChannel class]);

_mainThreadExpectation = [[XCTestExpectation alloc]
initWithDescription:@"setStreamHandler must be called in main thread"];
_channel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel];

OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
if (NSThread.isMainThread) {
[self.mainThreadExpectation fulfill];
}
});
}

- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread {
[self.channel setStreamHandler:nil
completion:^{
}];
[self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1];
}

- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread {
XCTestExpectation *mainThreadCompletionExpectation = [[XCTestExpectation alloc]
initWithDescription:@"setStreamHandler's completion block must be called in main thread"];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.channel setStreamHandler:nil
completion:^{
if (NSThread.isMainThread) {
[mainThreadCompletionExpectation fulfill];
}
}];
});
[self waitForExpectations:@[
self.mainThreadExpectation,
mainThreadCompletionExpectation,
]
timeout:1];
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 camera;
@import XCTest;
#import <OCMock/OCMock.h>

@interface ThreadSafeMethodChannelTests : XCTestCase
@property(nonatomic, strong) FLTThreadSafeMethodChannel *channel;
@property(nonatomic, strong) XCTestExpectation *mainThreadExpectation;
@end

@implementation ThreadSafeMethodChannelTests

- (void)setUp {
[super setUp];
id mockMethodChannel = OCMClassMock([FlutterMethodChannel class]);

_mainThreadExpectation =
[[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"];
_channel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel];

OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]])
.andDo(^(NSInvocation *invocation) {
if (NSThread.isMainThread) {
[self.mainThreadExpectation fulfill];
}
});
}

- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread {
[self.channel invokeMethod:@"foo" arguments:nil];

[self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1];
}

- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.channel invokeMethod:@"foo" arguments:nil];
});
[self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1];
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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 camera;
@import XCTest;
#import <OCMock/OCMock.h>

@interface ThreadSafeTextureRegistryTests : XCTestCase
@property(nonatomic, strong) FLTThreadSafeTextureRegistry *registry;
@property(nonatomic, strong) XCTestExpectation *registerTextureExpectation;
@property(nonatomic, strong) XCTestExpectation *registerTextureCompletionExpectation;
@property(nonatomic, strong) XCTestExpectation *unregisterTextureExpectation;
@property(nonatomic, strong) XCTestExpectation *textureFrameAvailableExpectation;

@end

@implementation ThreadSafeTextureRegistryTests

- (void)setUp {
[super setUp];
id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry));
_registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry];

_registerTextureExpectation = [[XCTestExpectation alloc]
initWithDescription:@"registerTexture must be called in main thread"];
_unregisterTextureExpectation = [[XCTestExpectation alloc]
initWithDescription:@"unregisterTexture must be called in main thread"];
_textureFrameAvailableExpectation = [[XCTestExpectation alloc]
initWithDescription:@"textureFrameAvailable must be called in main thread"];
_registerTextureCompletionExpectation = [[XCTestExpectation alloc]
initWithDescription:@"registerTexture's completion block must be called in main thread"];

OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
if (NSThread.isMainThread) {
[self.registerTextureExpectation fulfill];
}
});

OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) {
if (NSThread.isMainThread) {
[self.unregisterTextureExpectation fulfill];
}
});

OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) {
if (NSThread.isMainThread) {
[self.textureFrameAvailableExpectation fulfill];
}
});
}

- (void)testShouldStayOnMainThreadIfCalledFromMainThread {
NSObject<FlutterTexture> *anyTexture = OCMProtocolMock(@protocol(FlutterTexture));
[self.registry registerTexture:anyTexture
completion:^(int64_t textureId) {
if (NSThread.isMainThread) {
[self.registerTextureCompletionExpectation fulfill];
}
}];
[self.registry textureFrameAvailable:0];
[self.registry unregisterTexture:0];
[self waitForExpectations:@[
self.registerTextureExpectation,
self.unregisterTextureExpectation,
self.textureFrameAvailableExpectation,
self.registerTextureCompletionExpectation,
]
timeout:1];
}

- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSObject<FlutterTexture> *anyTexture = OCMProtocolMock(@protocol(FlutterTexture));
[self.registry registerTexture:anyTexture
completion:^(int64_t textureId) {
if (NSThread.isMainThread) {
[self.registerTextureCompletionExpectation fulfill];
}
}];
[self.registry textureFrameAvailable:0];
[self.registry unregisterTexture:0];
});
[self waitForExpectations:@[
self.registerTextureExpectation,
self.unregisterTextureExpectation,
self.textureFrameAvailableExpectation,
self.registerTextureCompletionExpectation,
]
timeout:1];
}

@end
42 changes: 28 additions & 14 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
#import <CoreMotion/CoreMotion.h>
#import <libkern/OSAtomic.h>
#import <uuid/uuid.h>
#import "FLTThreadSafeEventChannel.h"
#import "FLTThreadSafeFlutterResult.h"
#import "FLTThreadSafeMethodChannel.h"
#import "FLTThreadSafeTextureRegistry.h"

@interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
@property(readonly, nonatomic) NSString *path;
Expand Down Expand Up @@ -305,7 +308,7 @@ @interface FLTCam : NSObject <FlutterTexture,
@property(nonatomic, copy) void (^onFrameAvailable)(void);
@property BOOL enableAudio;
@property(nonatomic) FLTImageStreamHandler *imageStreamHandler;
@property(nonatomic) FlutterMethodChannel *methodChannel;
@property(nonatomic) FLTThreadSafeMethodChannel *methodChannel;
@property(readonly, nonatomic) AVCaptureSession *captureSession;
@property(readonly, nonatomic) AVCaptureDevice *captureDevice;
@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10));
Expand Down Expand Up @@ -1115,11 +1118,16 @@ - (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messen
FlutterEventChannel *eventChannel =
[FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream"
binaryMessenger:messenger];
FLTThreadSafeEventChannel *threadSafeEventChannel =
[[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel];

_imageStreamHandler = [[FLTImageStreamHandler alloc] init];
[eventChannel setStreamHandler:_imageStreamHandler];

_isStreamingImages = YES;
[threadSafeEventChannel setStreamHandler:_imageStreamHandler
completion:^{
dispatch_async(self->_dispatchQueue, ^{
self->_isStreamingImages = YES;
});
}];
} else {
[_methodChannel invokeMethod:errorMethod
arguments:@"Images from camera are already streaming!"];
Expand Down Expand Up @@ -1285,10 +1293,10 @@ - (void)setUpCaptureSessionForAudio {
@end

@interface CameraPlugin ()
@property(readonly, nonatomic) NSObject<FlutterTextureRegistry> *registry;
@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry;
@property(readonly, nonatomic) NSObject<FlutterBinaryMessenger> *messenger;
@property(readonly, nonatomic) FLTCam *camera;
@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel;
@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel;
@end

@implementation CameraPlugin {
Expand All @@ -1308,17 +1316,19 @@ - (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
messenger:(NSObject<FlutterBinaryMessenger> *)messenger {
self = [super init];
NSAssert(self, @"super init cannot be nil");
_registry = registry;
_registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry];
_messenger = messenger;
[self initDeviceEventMethodChannel];
[self startOrientationListener];
return self;
}

- (void)initDeviceEventMethodChannel {
_deviceEventMethodChannel =
FlutterMethodChannel *methodChannel =
[FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device"
binaryMessenger:_messenger];
_deviceEventMethodChannel =
[[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel];
}

- (void)startOrientationListener {
Expand Down Expand Up @@ -1417,11 +1427,13 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
if (_camera) {
[_camera close];
}
int64_t textureId = [self.registry registerTexture:cam];
_camera = cam;
[result sendSuccessWithData:@{
@"cameraId" : @(textureId),
}];
[self.registry registerTexture:cam
completion:^(int64_t textureId) {
[result sendSuccessWithData:@{
@"cameraId" : @(textureId),
}];
}];
}
} else if ([@"startImageStream" isEqualToString:call.method]) {
[_camera startImageStreamWithMessenger:_messenger];
Expand All @@ -1446,8 +1458,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu",
(unsigned long)cameraId]
binaryMessenger:_messenger];
_camera.methodChannel = methodChannel;
[methodChannel
FLTThreadSafeMethodChannel *threadSafeMethodChannel =
[[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel];
_camera.methodChannel = threadSafeMethodChannel;
[threadSafeMethodChannel
invokeMethod:@"initialized"
arguments:@{
@"previewWidth" : @(_camera.previewSize.width),
Expand Down
Loading

0 comments on commit 959eb1a

Please sign in to comment.