diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a6eceb957b1..499d7bbf2284 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 + +* Fixed a crash in iOS when using image stream due to calling Flutter engine API on non-main thread. + +## 0.9.4+5 * Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. * Fixes integration tests. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index feb789f2ecba..ad0ece9adbd2 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -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 */ @@ -74,6 +77,9 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; + E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; + E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; @@ -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 */, @@ -239,7 +248,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 03BB76672665316900CE5A93 = { @@ -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; }; 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 1447e08231be..f4b3c1099001 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 @@ + +@interface ThreadSafeEventChannelTests : XCTestCase +@property(nonatomic, strong) FLTThreadSafeEventChannel *channel; +@property(nonatomic, strong) XCTestExpectation *mainThreadExpectation; +@end + +@implementation ThreadSafeEventChannelTests + +- (void)setUp { + [super setUp]; + id mockEventChannel = OCMClassMock([FlutterEventChannel class]); + + _mainThreadExpectation = [[XCTestExpectation alloc] + initWithDescription:@"setStreamHandler must be called in main thread"]; + _channel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; + + OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self.mainThreadExpectation fulfill]; + } + }); +} + +- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { + [self.channel setStreamHandler:nil + completion:^{ + }]; + [self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1]; +} + +- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread { + XCTestExpectation *mainThreadCompletionExpectation = [[XCTestExpectation alloc] + initWithDescription:@"setStreamHandler's completion block must be called in main thread"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.channel setStreamHandler:nil + completion:^{ + if (NSThread.isMainThread) { + [mainThreadCompletionExpectation fulfill]; + } + }]; + }); + [self waitForExpectations:@[ + self.mainThreadExpectation, + mainThreadCompletionExpectation, + ] + timeout:1]; +} + +@end 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 000000000000..601cec8f05d8 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +#import + +@interface ThreadSafeMethodChannelTests : XCTestCase +@property(nonatomic, strong) FLTThreadSafeMethodChannel *channel; +@property(nonatomic, strong) XCTestExpectation *mainThreadExpectation; +@end + +@implementation ThreadSafeMethodChannelTests + +- (void)setUp { + [super setUp]; + id mockMethodChannel = OCMClassMock([FlutterMethodChannel class]); + + _mainThreadExpectation = + [[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"]; + _channel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel]; + + OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self.mainThreadExpectation fulfill]; + } + }); +} + +- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread { + [self.channel invokeMethod:@"foo" arguments:nil]; + + [self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1]; +} + +- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.channel invokeMethod:@"foo" arguments:nil]; + }); + [self waitForExpectations:@[ self.mainThreadExpectation ] timeout:1]; +} + +@end 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 000000000000..26fe134486a1 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +#import + +@interface ThreadSafeTextureRegistryTests : XCTestCase +@property(nonatomic, strong) FLTThreadSafeTextureRegistry *registry; +@property(nonatomic, strong) XCTestExpectation *registerTextureExpectation; +@property(nonatomic, strong) XCTestExpectation *registerTextureCompletionExpectation; +@property(nonatomic, strong) XCTestExpectation *unregisterTextureExpectation; +@property(nonatomic, strong) XCTestExpectation *textureFrameAvailableExpectation; + +@end + +@implementation ThreadSafeTextureRegistryTests + +- (void)setUp { + [super setUp]; + id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); + _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry]; + + _registerTextureExpectation = [[XCTestExpectation alloc] + initWithDescription:@"registerTexture must be called in main thread"]; + _unregisterTextureExpectation = [[XCTestExpectation alloc] + initWithDescription:@"unregisterTexture must be called in main thread"]; + _textureFrameAvailableExpectation = [[XCTestExpectation alloc] + initWithDescription:@"textureFrameAvailable must be called in main thread"]; + _registerTextureCompletionExpectation = [[XCTestExpectation alloc] + initWithDescription:@"registerTexture's completion block must be called in main thread"]; + + OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self.registerTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self.unregisterTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self.textureFrameAvailableExpectation fulfill]; + } + }); +} + +- (void)testShouldStayOnMainThreadIfCalledFromMainThread { + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [self.registry registerTexture:anyTexture + completion:^(int64_t textureId) { + if (NSThread.isMainThread) { + [self.registerTextureCompletionExpectation fulfill]; + } + }]; + [self.registry textureFrameAvailable:0]; + [self.registry unregisterTexture:0]; + [self waitForExpectations:@[ + self.registerTextureExpectation, + self.unregisterTextureExpectation, + self.textureFrameAvailableExpectation, + self.registerTextureCompletionExpectation, + ] + timeout:1]; +} + +- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [self.registry registerTexture:anyTexture + completion:^(int64_t textureId) { + if (NSThread.isMainThread) { + [self.registerTextureCompletionExpectation fulfill]; + } + }]; + [self.registry textureFrameAvailable:0]; + [self.registry unregisterTexture:0]; + }); + [self waitForExpectations:@[ + self.registerTextureExpectation, + self.unregisterTextureExpectation, + self.textureFrameAvailableExpectation, + self.registerTextureCompletionExpectation, + ] + timeout:1]; +} + +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 2c12081da807..cecdb726095b 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,7 +10,10 @@ #import #import #import +#import "FLTThreadSafeEventChannel.h" #import "FLTThreadSafeFlutterResult.h" +#import "FLTThreadSafeMethodChannel.h" +#import "FLTThreadSafeTextureRegistry.h" @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @@ -305,7 +308,7 @@ @interface FLTCam : NSObject *)messen FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" binaryMessenger:messenger]; + FLTThreadSafeEventChannel *threadSafeEventChannel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; - [eventChannel setStreamHandler:_imageStreamHandler]; - - _isStreamingImages = YES; + [threadSafeEventChannel setStreamHandler:_imageStreamHandler + completion:^{ + dispatch_async(self->_dispatchQueue, ^{ + self->_isStreamingImages = YES; + }); + }]; } else { [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are already streaming!"]; @@ -1285,10 +1293,10 @@ - (void)setUpCaptureSessionForAudio { @end @interface CameraPlugin () -@property(readonly, nonatomic) NSObject *registry; +@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; +@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -1308,7 +1316,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)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 +1324,11 @@ - (instancetype)initWithRegistry:(NSObject *)registry } - (void)initDeviceEventMethodChannel { - _deviceEventMethodChannel = + FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" binaryMessenger:_messenger]; + _deviceEventMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; } - (void)startOrientationListener { @@ -1417,11 +1427,13 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call if (_camera) { [_camera close]; } - int64_t textureId = [self.registry registerTexture:cam]; _camera = cam; - [result sendSuccessWithData:@{ - @"cameraId" : @(textureId), - }]; + [self.registry registerTexture:cam + completion:^(int64_t textureId) { + [result sendSuccessWithData:@{ + @"cameraId" : @(textureId), + }]; + }]; } } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; @@ -1446,8 +1458,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", (unsigned long)cameraId] binaryMessenger:_messenger]; - _camera.methodChannel = methodChannel; - [methodChannel + FLTThreadSafeMethodChannel *threadSafeMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; + _camera.methodChannel = threadSafeMethodChannel; + [threadSafeMethodChannel invokeMethod:@"initialized" arguments:@{ @"previewWidth" : @(_camera.previewSize.width), diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h new file mode 100644 index 000000000000..dc6a414b35ea --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h @@ -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 + +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. + # Completion block runs on main thread. + */ +- (void)setStreamHandler:(nullable NSObject *)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 000000000000..1498457e7a11 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m @@ -0,0 +1,34 @@ +// 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" + +@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 *)handler + completion:(void (^)(void))completion { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.channel setStreamHandler:handler]; + completion(); + }); + } else { + [self.channel setStreamHandler:handler]; + completion(); + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h new file mode 100644 index 000000000000..db99d0f7f66a --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h @@ -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 + +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 diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m new file mode 100644 index 000000000000..8fdcfe7209a7 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m @@ -0,0 +1,31 @@ +// 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" + +@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 { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.channel invokeMethod:method arguments:arguments]; + }); + } else { + [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 000000000000..d20c9645c083 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h @@ -0,0 +1,53 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper for FlutterTextureRegistry that always sends events on 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 *)registry; + +/** + * Registers a `FlutterTexture` for usage in Flutter and returns an id that can be used to reference + * that texture when calling into Flutter with channels. Textures must be registered on the + * main thread. + * + * On success the completion block completes with the pointer to the registered texture, else with + * 0. + */ +- (void)registerTexture:(NSObject *)texture + completion:(void (^)(int64_t))completion; + +/** + * Notifies Flutter that the content of the previously registered texture has been updated. + * + * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. + * + * Runs on main thread. + */ +- (void)textureFrameAvailable:(int64_t)textureId; + +/** + * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures + * must be unregistered on the main thread. + * + * Runs on main thread. + * + * @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 000000000000..35ef79b79451 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m @@ -0,0 +1,52 @@ +// 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" + +@interface FLTThreadSafeTextureRegistry () +@property(nonatomic, strong) NSObject *registry; +@end + +@implementation FLTThreadSafeTextureRegistry + +- (instancetype)initWithTextureRegistry:(NSObject *)registry { + self = [super init]; + if (self) { + _registry = registry; + } + return self; +} + +- (void)registerTexture:(NSObject *)texture + completion:(void (^)(int64_t))completion { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion([self.registry registerTexture:texture]); + }); + } else { + completion([self.registry registerTexture:texture]); + } +} + +- (void)textureFrameAvailable:(int64_t)textureId { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.registry textureFrameAvailable:textureId]; + }); + } else { + [self.registry textureFrameAvailable:textureId]; + } +} + +- (void)unregisterTexture:(int64_t)textureId { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.registry unregisterTexture:textureId]; + }); + } else { + [self.registry unregisterTexture:textureId]; + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index b0fd493b24df..c55276e31147 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,7 +4,10 @@ #import #import +#import #import +#import +#import 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 58e1ca3ca98c..4c6c7e7e52d1 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing 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"