[camera]call engine API in main thread to fix a crash (#4661)
* [camera]call engine API in main thread to fix a crash
* [camera]addess a few comments by moving XCTestExpectations to each test cases, and update wrappers comment
* [camera]remove setUp function in tests, and update comments for thread safe wrappers
* [camera]handle event channel's threading properly
* [camera]address various nits, mainly the test expectation refactor and QueueHelper refactor
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 1a6eceb..e9db5de 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,4 +1,8 @@
-## 0.9.4+5
+## 0.9.4+6
+
+* Fixes 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.
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
index feb789f..32b770e 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -20,6 +20,10 @@
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 */; };
+ E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; };
+ 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 */
@@ -74,6 +78,10 @@
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>"; };
+ E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; 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>"; };
@@ -107,6 +115,10 @@
03BB766C2665316900CE5A93 /* Info.plist */,
033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */,
03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */,
+ E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */,
+ E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */,
+ E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */,
+ E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */,
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */,
@@ -239,7 +251,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1100;
+ LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
03BB76672665316900CE5A93 = {
@@ -378,6 +390,10 @@
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 */,
+ E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 1447e08..f4b3c10 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
- LastUpgradeVersion = "1100"
+ LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m
new file mode 100644
index 0000000..c5f377f
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m
@@ -0,0 +1,38 @@
+// 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;
+
+@interface QueueHelperTests : XCTestCase
+
+@end
+
+@implementation QueueHelperTests
+
+- (void)testShouldStayOnMainQueueIfCalledFromMainQueue {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Block must be run on the main queue."];
+ [QueueHelper ensureToRunOnMainQueue:^{
+ if (NSThread.isMainThread) {
+ [expectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Block must be run on the main queue."];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [QueueHelper ensureToRunOnMainQueue:^{
+ if (NSThread.isMainThread) {
+ [expectation fulfill];
+ }
+ }];
+ });
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+@end
diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m
new file mode 100644
index 0000000..dd7ca39
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m
@@ -0,0 +1,66 @@
+// 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
+
+- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread {
+ FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]);
+ FLTThreadSafeEventChannel *threadSafeEventChannel =
+ [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel];
+
+ XCTestExpectation *mainThreadExpectation =
+ [self expectationWithDescription:@"setStreamHandler must be called on the main thread"];
+ XCTestExpectation *mainThreadCompletionExpectation =
+ [self expectationWithDescription:
+ @"setStreamHandler's completion block must be called on the main thread"];
+ OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [mainThreadExpectation fulfill];
+ }
+ });
+
+ [threadSafeEventChannel setStreamHandler:nil
+ completion:^{
+ if (NSThread.isMainThread) {
+ [mainThreadCompletionExpectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread {
+ FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]);
+ FLTThreadSafeEventChannel *threadSafeEventChannel =
+ [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel];
+
+ XCTestExpectation *mainThreadExpectation =
+ [self expectationWithDescription:@"setStreamHandler must be called on the main thread"];
+ XCTestExpectation *mainThreadCompletionExpectation =
+ [self expectationWithDescription:
+ @"setStreamHandler's completion block must be called on the main thread"];
+ OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [mainThreadExpectation fulfill];
+ }
+ });
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [threadSafeEventChannel setStreamHandler:nil
+ completion:^{
+ if (NSThread.isMainThread) {
+ [mainThreadCompletionExpectation fulfill];
+ }
+ }];
+ });
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+@end
diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m
index 8cd4b8b..a01b531 100644
--- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m
@@ -10,8 +10,7 @@
@implementation ThreadSafeFlutterResultTests
- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread {
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -23,12 +22,11 @@
[threadSafeFlutterResult sendSuccess];
});
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testSyncSendSuccess_ShouldCallResultOnMainThread {
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -36,12 +34,11 @@
[expectation fulfill];
}];
[threadSafeFlutterResult sendSuccess];
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult {
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -53,15 +50,14 @@
[threadSafeFlutterResult sendNotImplemented];
});
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult {
NSString* errorCode = @"errorCode";
NSString* errorMessage = @"message";
NSString* errorDetails = @"error details";
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -77,13 +73,12 @@
[threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails];
});
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testSendNSError_ShouldSendErrorToFlutterResult {
NSError* originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil];
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -99,13 +94,12 @@
[threadSafeFlutterResult sendError:originalError];
});
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testSendResult_ShouldSendResultToFlutterResult {
NSString* resultData = @"resultData";
- XCTestExpectation* expectation =
- [[XCTestExpectation alloc] initWithDescription:@"Result finished"];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"Result finished"];
FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
@@ -117,6 +111,6 @@
[threadSafeFlutterResult sendSuccessWithData:resultData];
});
- [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
}
@end
diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m
new file mode 100644
index 0000000..5075be7
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m
@@ -0,0 +1,54 @@
+// 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
+
+- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread {
+ FlutterMethodChannel *mockMethodChannel = OCMClassMock([FlutterMethodChannel class]);
+ FLTThreadSafeMethodChannel *threadSafeMethodChannel =
+ [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel];
+
+ XCTestExpectation *mainThreadExpectation =
+ [self expectationWithDescription:@"invokeMethod must be called on the main thread"];
+
+ OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]])
+ .andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [mainThreadExpectation fulfill];
+ }
+ });
+
+ [threadSafeMethodChannel invokeMethod:@"foo" arguments:nil];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread {
+ FlutterMethodChannel *mockMethodChannel = OCMClassMock([FlutterMethodChannel class]);
+ FLTThreadSafeMethodChannel *threadSafeMethodChannel =
+ [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel];
+
+ XCTestExpectation *mainThreadExpectation =
+ [self expectationWithDescription:@"invokeMethod must be called on the main thread"];
+
+ OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]])
+ .andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [mainThreadExpectation fulfill];
+ }
+ });
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [threadSafeMethodChannel invokeMethod:@"foo" arguments:nil];
+ });
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+@end
diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m
new file mode 100644
index 0000000..067ebab
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m
@@ -0,0 +1,108 @@
+// 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
+
+- (void)testShouldStayOnMainThreadIfCalledFromMainThread {
+ NSObject<FlutterTextureRegistry> *mockTextureRegistry =
+ OCMProtocolMock(@protocol(FlutterTextureRegistry));
+ FLTThreadSafeTextureRegistry *threadSafeTextureRegistry =
+ [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry];
+
+ XCTestExpectation *registerTextureExpectation =
+ [self expectationWithDescription:@"registerTexture must be called on the main thread"];
+ XCTestExpectation *unregisterTextureExpectation =
+ [self expectationWithDescription:@"unregisterTexture must be called on the main thread"];
+ XCTestExpectation *textureFrameAvailableExpectation =
+ [self expectationWithDescription:@"textureFrameAvailable must be called on the main thread"];
+ XCTestExpectation *registerTextureCompletionExpectation =
+ [self expectationWithDescription:
+ @"registerTexture's completion block must be called on the main thread"];
+
+ OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [registerTextureExpectation fulfill];
+ }
+ });
+
+ OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [unregisterTextureExpectation fulfill];
+ }
+ });
+
+ OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [textureFrameAvailableExpectation fulfill];
+ }
+ });
+
+ NSObject<FlutterTexture> *anyTexture = OCMProtocolMock(@protocol(FlutterTexture));
+ [threadSafeTextureRegistry registerTexture:anyTexture
+ completion:^(int64_t textureId) {
+ if (NSThread.isMainThread) {
+ [registerTextureCompletionExpectation fulfill];
+ }
+ }];
+ [threadSafeTextureRegistry textureFrameAvailable:0];
+ [threadSafeTextureRegistry unregisterTexture:0];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread {
+ NSObject<FlutterTextureRegistry> *mockTextureRegistry =
+ OCMProtocolMock(@protocol(FlutterTextureRegistry));
+ FLTThreadSafeTextureRegistry *threadSafeTextureRegistry =
+ [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry];
+
+ XCTestExpectation *registerTextureExpectation =
+ [self expectationWithDescription:@"registerTexture must be called on the main thread"];
+ XCTestExpectation *unregisterTextureExpectation =
+ [self expectationWithDescription:@"unregisterTexture must be called on the main thread"];
+ XCTestExpectation *textureFrameAvailableExpectation =
+ [self expectationWithDescription:@"textureFrameAvailable must be called on the main thread"];
+ XCTestExpectation *registerTextureCompletionExpectation =
+ [self expectationWithDescription:
+ @"registerTexture's completion block must be called on the main thread"];
+
+ OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [registerTextureExpectation fulfill];
+ }
+ });
+
+ OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [unregisterTextureExpectation fulfill];
+ }
+ });
+
+ OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) {
+ if (NSThread.isMainThread) {
+ [textureFrameAvailableExpectation fulfill];
+ }
+ });
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ NSObject<FlutterTexture> *anyTexture = OCMProtocolMock(@protocol(FlutterTexture));
+ [threadSafeTextureRegistry registerTexture:anyTexture
+ completion:^(int64_t textureId) {
+ if (NSThread.isMainThread) {
+ [registerTextureCompletionExpectation fulfill];
+ }
+ }];
+ [threadSafeTextureRegistry textureFrameAvailable:0];
+ [threadSafeTextureRegistry unregisterTexture:0];
+ });
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index 2c12081..09b5f0f 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -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;
@@ -18,19 +21,34 @@
@end
@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler>
+// The queue on which `eventSink` property should be accessed
+@property(nonatomic, strong) dispatch_queue_t dispatchQueue;
+// `eventSink` property should be accessed on `dispatchQueue`.
+// The block itself should be invoked on the main queue.
@property FlutterEventSink eventSink;
@end
@implementation FLTImageStreamHandler
+- (instancetype)initWithDispatchQueue:(dispatch_queue_t)dispatchQueue {
+ self = [super init];
+ NSAssert(self, @"super init cannot be nil");
+ _dispatchQueue = dispatchQueue;
+ return self;
+}
+
- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments {
- _eventSink = nil;
+ dispatch_async(self.dispatchQueue, ^{
+ self.eventSink = nil;
+ });
return nil;
}
- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(nonnull FlutterEventSink)events {
- _eventSink = events;
+ dispatch_async(self.dispatchQueue, ^{
+ self.eventSink = events;
+ });
return nil;
}
@end
@@ -305,7 +323,7 @@
@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));
@@ -605,8 +623,10 @@
return;
}
if (_isStreamingImages) {
- if (_imageStreamHandler.eventSink) {
+ FlutterEventSink eventSink = _imageStreamHandler.eventSink;
+ if (eventSink) {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
+ // Must lock base address before accessing the pixel data
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer);
@@ -651,6 +671,9 @@
[planes addObject:planeBuffer];
}
+ // Before accessing pixel data, we should lock the base address, and unlock it afterwards.
+ // Done accessing the `pixelBuffer` at this point.
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary];
imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth];
@@ -663,9 +686,9 @@
imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration];
imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]];
- _imageStreamHandler.eventSink(imageBuffer);
-
- CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
+ dispatch_async(dispatch_get_main_queue(), ^{
+ eventSink(imageBuffer);
+ });
}
}
if (_isRecording && !_isRecordingPaused) {
@@ -1115,11 +1138,16 @@
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;
+ _imageStreamHandler = [[FLTImageStreamHandler alloc] initWithDispatchQueue:_dispatchQueue];
+ [threadSafeEventChannel setStreamHandler:_imageStreamHandler
+ completion:^{
+ dispatch_async(self->_dispatchQueue, ^{
+ self.isStreamingImages = YES;
+ });
+ }];
} else {
[_methodChannel invokeMethod:errorMethod
arguments:@"Images from camera are already streaming!"];
@@ -1285,10 +1313,10 @@
@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 {
@@ -1308,7 +1336,7 @@
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];
@@ -1316,9 +1344,11 @@
}
- (void)initDeviceEventMethodChannel {
- _deviceEventMethodChannel =
+ FlutterMethodChannel *methodChannel =
[FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device"
binaryMessenger:_messenger];
+ _deviceEventMethodChannel =
+ [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel];
}
- (void)startOrientationListener {
@@ -1417,11 +1447,13 @@
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];
@@ -1446,8 +1478,10 @@
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),
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h
new file mode 100644
index 0000000..ddfa754
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h
@@ -0,0 +1,30 @@
+// 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
+
+/**
+ * A thread safe wrapper for FlutterEventChannel that can be called from any thread, by dispatching
+ * its underlying engine calls to 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 on the main thread for stream setup requests from the Flutter side.
+ # The completion block runs on the main thread.
+ */
+- (void)setStreamHandler:(nullable NSObject<FlutterStreamHandler> *)handler
+ completion:(void (^)(void))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m
new file mode 100644
index 0000000..02a36f1
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m
@@ -0,0 +1,30 @@
+// 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"
+#import "QueueHelper.h"
+
+@interface FLTThreadSafeEventChannel ()
+@property(nonatomic, strong) FlutterEventChannel *channel;
+@end
+
+@implementation FLTThreadSafeEventChannel
+
+- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel {
+ self = [super init];
+ if (self) {
+ _channel = channel;
+ }
+ return self;
+}
+
+- (void)setStreamHandler:(NSObject<FlutterStreamHandler> *)handler
+ completion:(void (^)(void))completion {
+ [QueueHelper ensureToRunOnMainQueue:^{
+ [self.channel setStreamHandler:handler];
+ completion();
+ }];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h
index f290ca0..787be4c 100644
--- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h
@@ -5,7 +5,8 @@
#import <Flutter/Flutter.h>
/**
- * Wrapper for FlutterResult that always delivers the result on the main thread.
+ * A thread safe wrapper for FlutterResult that can be called from any thread, by dispatching its
+ * underlying engine calls to the main thread.
*/
@interface FLTThreadSafeFlutterResult : NSObject
@@ -21,31 +22,31 @@
- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result;
/**
- * Sends a successful result without any data.
+ * Sends a successful result on the main thread without any data.
*/
- (void)sendSuccess;
/**
- * Sends a successful result with data.
+ * Sends a successful result on the main thread with data.
* @param data Result data that is send to the Flutter Dart side.
*/
- (void)sendSuccessWithData:(nonnull id)data;
/**
- * Sends an NSError as result
+ * Sends an NSError as result on the main thread.
* @param error Error that will be send as FlutterError.
*/
- (void)sendError:(nonnull NSError*)error;
/**
- * Sends a FlutterError as result.
+ * Sends a FlutterError as result on the main thread.
*/
- (void)sendErrorWithCode:(nonnull NSString*)code
message:(nullable NSString*)message
details:(nullable id)details;
/**
- * Sends FlutterMethodNotImplemented as result.
+ * Sends FlutterMethodNotImplemented as result on the main thread.
*/
- (void)sendNotImplemented;
@end
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m
index caa4788..2e426cc 100644
--- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m
@@ -4,6 +4,7 @@
#import "FLTThreadSafeFlutterResult.h"
#import <Foundation/Foundation.h>
+#import "QueueHelper.h"
@implementation FLTThreadSafeFlutterResult {
}
@@ -46,13 +47,9 @@
* Sends result to flutterResult on the main thread.
*/
- (void)send:(id _Nullable)result {
- if (!NSThread.isMainThread) {
- dispatch_async(dispatch_get_main_queue(), ^{
- self->_flutterResult(result);
- });
- } else {
- _flutterResult(result);
- }
+ [QueueHelper ensureToRunOnMainQueue:^{
+ self.flutterResult(result);
+ }];
}
@end
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h
new file mode 100644
index 0000000..0f6611d
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h
@@ -0,0 +1,28 @@
+// 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
+
+/**
+ * A thread safe wrapper for FlutterMethodChannel that can be called from any thread, by dispatching
+ * its underlying engine calls to 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 on the main thread with the specified arguments.
+ */
+- (void)invokeMethod:(NSString *)method arguments:(nullable id)arguments;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m
new file mode 100644
index 0000000..ad4da87
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m
@@ -0,0 +1,28 @@
+// 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 "FLTThreadSafeMethodChannel.h"
+#import "QueueHelper.h"
+
+@interface FLTThreadSafeMethodChannel ()
+@property(nonatomic, strong) FlutterMethodChannel *channel;
+@end
+
+@implementation FLTThreadSafeMethodChannel
+
+- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel {
+ self = [super init];
+ if (self) {
+ _channel = channel;
+ }
+ return self;
+}
+
+- (void)invokeMethod:(NSString *)method arguments:(id)arguments {
+ [QueueHelper ensureToRunOnMainQueue:^{
+ [self.channel invokeMethod:method arguments:arguments];
+ }];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h
new file mode 100644
index 0000000..030e2db
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h
@@ -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 <Flutter/Flutter.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A thread safe wrapper for FlutterTextureRegistry that can be called from any thread, by
+ * dispatching its underlying engine calls to the main thread.
+ */
+@interface FLTThreadSafeTextureRegistry : NSObject
+
+/**
+ * Creates a FLTThreadSafeTextureRegistry by wrapping an object conforming to
+ * FlutterTextureRegistry.
+ * @param registry The FlutterTextureRegistry object to be wrapped.
+ */
+- (instancetype)initWithTextureRegistry:(NSObject<FlutterTextureRegistry> *)registry;
+
+/**
+ * Registers a `FlutterTexture` on the main thread for usage in Flutter and returns an id that can
+ * be used to reference that texture when calling into Flutter with channels.
+ *
+ * On success the completion block completes with the pointer to the registered texture, else with
+ * 0. The completion block runs on the main thread.
+ */
+- (void)registerTexture:(NSObject<FlutterTexture> *)texture
+ completion:(void (^)(int64_t))completion;
+
+/**
+ * Notifies the Flutter engine on the main thread that the given texture has been updated.
+ */
+- (void)textureFrameAvailable:(int64_t)textureId;
+
+/**
+ * Notifies the Flutter engine on the main thread to unregister a `FlutterTexture` that has been
+ * previously registered with `registerTexture:`.
+ * @param textureId The result that was previously returned from `registerTexture:`.
+ */
+- (void)unregisterTexture:(int64_t)textureId;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m
new file mode 100644
index 0000000..5eb2443
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m
@@ -0,0 +1,41 @@
+// 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 "FLTThreadSafeTextureRegistry.h"
+#import "QueueHelper.h"
+
+@interface FLTThreadSafeTextureRegistry ()
+@property(nonatomic, strong) NSObject<FlutterTextureRegistry> *registry;
+@end
+
+@implementation FLTThreadSafeTextureRegistry
+
+- (instancetype)initWithTextureRegistry:(NSObject<FlutterTextureRegistry> *)registry {
+ self = [super init];
+ if (self) {
+ _registry = registry;
+ }
+ return self;
+}
+
+- (void)registerTexture:(NSObject<FlutterTexture> *)texture
+ completion:(void (^)(int64_t))completion {
+ [QueueHelper ensureToRunOnMainQueue:^{
+ completion([self.registry registerTexture:texture]);
+ }];
+}
+
+- (void)textureFrameAvailable:(int64_t)textureId {
+ [QueueHelper ensureToRunOnMainQueue:^{
+ [self.registry textureFrameAvailable:textureId];
+ }];
+}
+
+- (void)unregisterTexture:(int64_t)textureId {
+ [QueueHelper ensureToRunOnMainQueue:^{
+ [self.registry unregisterTexture:textureId];
+ }];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h
new file mode 100644
index 0000000..c254814
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/QueueHelper.h
@@ -0,0 +1,13 @@
+// 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface QueueHelper : NSObject
++ (void)ensureToRunOnMainQueue:(void (^)(void))block;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m
new file mode 100644
index 0000000..194dfa9
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/QueueHelper.m
@@ -0,0 +1,15 @@
+// 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 "QueueHelper.h"
+
+@implementation QueueHelper
++ (void)ensureToRunOnMainQueue:(void (^)(void))block {
+ if (!NSThread.isMainThread) {
+ dispatch_async(dispatch_get_main_queue(), block);
+ } else {
+ block();
+ }
+}
+@end
diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h
index b0fd493..428b125 100644
--- a/packages/camera/camera/ios/Classes/camera-umbrella.h
+++ b/packages/camera/camera/ios/Classes/camera-umbrella.h
@@ -4,7 +4,11 @@
#import <Foundation/Foundation.h>
#import <camera/CameraPlugin.h>
+#import <camera/FLTThreadSafeEventChannel.h>
#import <camera/FLTThreadSafeFlutterResult.h>
+#import <camera/FLTThreadSafeMethodChannel.h>
+#import <camera/FLTThreadSafeTextureRegistry.h>
+#import <camera/QueueHelper.h>
FOUNDATION_EXPORT double cameraVersionNumber;
FOUNDATION_EXPORT const unsigned char cameraVersionString[];
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 58e1ca3..4c6c7e7 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
Dart.
repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.4+5
+version: 0.9.4+6
environment:
sdk: ">=2.14.0 <3.0.0"