[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"