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

Commit

Permalink
[camera]fix threading issue with thread safe types to ensure dispatch…
Browse files Browse the repository at this point in the history
…ing to main thread before calling engine api
  • Loading branch information
hellohuanlin committed Dec 20, 2021
1 parent 6531740 commit 4f53674
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 10 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.4+6

* Fixed a crash in iOS when using image stream due to threading issue.

## 0.9.4+5

* Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception.
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,44 @@
// 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
@end

@implementation ThreadSafeEventChannelTests {
FLTThreadSafeEventChannel *_channel;
XCTestExpectation *_mainThreadExpectation;
}

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

_mainThreadExpectation =
[[XCTestExpectation alloc] initWithDescription:@"invokeMethod 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 {
[_channel setStreamHandler:nil];
[self waitForExpectations:@[ _mainThreadExpectation ] timeout:1];
}

- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self->_channel setStreamHandler:nil];
});
[self waitForExpectations:@[ _mainThreadExpectation ] timeout:1];
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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
@end

@implementation ThreadSafeMethodChannelTests {
FLTThreadSafeMethodChannel *_channel;
XCTestExpectation *_mainThreadExpectation;
}

- (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 {
[_channel invokeMethod:@"foo" arguments:nil];

[self waitForExpectations:@[ _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:@[ _mainThreadExpectation ] timeout:1];
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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
@end

@implementation ThreadSafeTextureRegistryTests {
FLTThreadSafeTextureRegistry *_registry;
XCTestExpectation *_registerTextureExpectation;
XCTestExpectation *_unregisterTextureExpectation;
XCTestExpectation *_textureFrameAvailableExpectation;
}

- (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"];

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));
[_registry registerTexture:anyTexture];
[_registry textureFrameAvailable:0];
[_registry unregisterTexture:0];
[self waitForExpectations:@[
_registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation
]
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];
[self->_registry textureFrameAvailable:0];
[self->_registry unregisterTexture:0];
});
[self waitForExpectations:@[
_registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation
]
timeout:1];
}

@end
25 changes: 17 additions & 8 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,9 +1118,11 @@ - (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];
[threadSafeEventChannel setStreamHandler:_imageStreamHandler];

_isStreamingImages = YES;
} else {
Expand Down Expand Up @@ -1285,10 +1290,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 +1313,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 @@ -1446,8 +1453,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
27 changes: 27 additions & 0 deletions packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 <Flutter/Flutter.h>

NS_ASSUME_NONNULL_BEGIN

/**
* Wrapper for FlutterEventChannel that always sends events on the main thread
*/
@interface FLTThreadSafeEventChannel : NSObject

/**
* Creates a FLTThreadSafeEventChannel by wrapping a FlutterEventChannel object.
* @param channel The FlutterEventChannel object to be wrapped.
*/
- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel;

/*
* Registers a handler for stream setup requests from the Flutter side on main thread.
*/
- (void)setStreamHandler:(nullable NSObject<FlutterStreamHandler> *)handler;

@end

NS_ASSUME_NONNULL_END
29 changes: 29 additions & 0 deletions packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 "FLTThreadSafeEventChannel.h"

@implementation FLTThreadSafeEventChannel {
FlutterEventChannel *_channel;
}

- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel {
self = [super init];
if (self) {
_channel = channel;
}
return self;
}

- (void)setStreamHandler:(NSObject<FlutterStreamHandler> *)handler {
if (!NSThread.isMainThread) {
dispatch_async(dispatch_get_main_queue(), ^{
[self->_channel setStreamHandler:handler];
});
} else {
[_channel setStreamHandler:handler];
}
}

@end
27 changes: 27 additions & 0 deletions packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 <Flutter/Flutter.h>

NS_ASSUME_NONNULL_BEGIN

/**
* Wrapper for FlutterMethodChannel that always invokes messages on the main thread
*/
@interface FLTThreadSafeMethodChannel : NSObject

/**
* Creates a FLTThreadSafeMethodChannel by wrapping a FlutterMethodChannel object.
* @param channel The FlutterMethodChannel object to be wrapped.
*/
- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel;

/**
* Invokes the specified flutter method with the specified arguments on main thread.
*/
- (void)invokeMethod:(NSString *)method arguments:(nullable id)arguments;

@end

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 4f53674

Please sign in to comment.