From 2939d95628810f3790621a584b74c876980f6dd6 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 12:53:58 -0800 Subject: [PATCH 1/8] [camera]photo file io on background queue --- packages/camera/camera/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../RunnerTests/FLTSavePhotoDelegateTests.m | 135 ++++++++++++++++++ packages/camera/camera/ios/Classes/FLTCam.m | 93 ++++++------ packages/camera/camera/pubspec.yaml | 2 +- 5 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 9b32af241032..8a320f646542 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+10 + +* iOS performance improvement by writing captured photo file on a background IO queue. + ## 0.9.4+9 * iOS performance improvement by moving sample buffer handling from the main queue to a background session queue. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 65a6bbda3261..53f22bc60f9d 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; }; E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; + E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.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 */; }; @@ -83,6 +84,7 @@ 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 = ""; }; E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = ""; }; E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; + E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; 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 = ""; }; @@ -125,6 +127,7 @@ E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */, + E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, @@ -399,6 +402,7 @@ E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m new file mode 100644 index 000000000000..c91e63d43ca5 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -0,0 +1,135 @@ +// +// FLTSavePhotoDelegateTests.m +// RunnerTests +// +// Created by Huan Lin on 1/31/22. +// Copyright © 2022 The Flutter Authors. All rights reserved. +// + +@import camera; +@import camera.Test; +@import AVFoundation; +@import XCTest; +#import + +@interface FLTSavePhotoDelegate : NSObject +@property(readonly, nonatomic) NSString *path; +- initWithPath:(NSString *)path + result:(FLTThreadSafeFlutterResult *)result + ioQueue:(dispatch_queue_t)ioQueue; +- (void)handlePhotoCaptureResultWithError:(nullable NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider; +@end + +@interface FLTSavePhotoDelegateTests : XCTestCase + +@end + +@implementation FLTSavePhotoDelegateTests + +- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture { + NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" + result:mockResult + ioQueue:ioQueue]; + + [delegate handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return nil; + }]; + OCMVerify(times(1), [mockResult sendError:error]); +} + +- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]) + .andDo(^(NSInvocation *invocation) { + [resultExpectation fulfill]; + }); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" + result:mockResult + ioQueue:ioQueue]; + + // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. + id mockData = OCMPartialMock([NSData data]); + OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]).andReturn(NO); + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + return mockData; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { + XCTestExpectation *resultExpectation = [self + expectationWithDescription:@"Must send file path to the result if success to write file."]; + + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" + result:mockResult + ioQueue:ioQueue]; + OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) { + [resultExpectation fulfill]; + }); + + // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. + id mockData = OCMPartialMock([NSData data]); + OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]).andReturn(YES); + + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + return mockData; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue { + XCTestExpectation *dataProviderQueueExpectation = + [self expectationWithDescription:@"Data provider must run on io queue."]; + XCTestExpectation *writeFileQueueExpectation = + [self expectationWithDescription:@"File writing must run on io queue"]; + XCTestExpectation *resultExpectation = [self + expectationWithDescription:@"Must send file path to the result if success to write file."]; + + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + const char *ioQueueSpecific = "io_queue_specific"; + dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendSuccessWithData:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + [resultExpectation fulfill]; + }); + + // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. + id mockData = OCMPartialMock([NSData data]); + OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + if (dispatch_get_specific(ioQueueSpecific)) { + [writeFileQueueExpectation fulfill]; + } + }) + .andReturn(YES); + + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" + result:mockResult + ioQueue:ioQueue]; + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + if (dispatch_get_specific(ioQueueSpecific)) { + [dataProviderQueueExpectation fulfill]; + } + return mockData; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 82ac6714d689..f184aa37ea3b 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -44,66 +44,69 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; +/// The queue on which captured photos are wrote to disk. +@property(strong, nonatomic) dispatch_queue_t ioQueue; +/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. +@property(strong, nonatomic) FLTSavePhotoDelegate *selfReference; @end -@implementation FLTSavePhotoDelegate { - /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. - FLTSavePhotoDelegate *selfReference; -} +@implementation FLTSavePhotoDelegate -- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result { +- initWithPath:(NSString *)path + result:(FLTThreadSafeFlutterResult *)result + ioQueue:(dispatch_queue_t)ioQueue { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - selfReference = self; + _selfReference = self; _result = result; + _ioQueue = ioQueue; return self; } +- (void)handlePhotoCaptureResultWithError:(NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider { + self.selfReference = nil; + if (error) { + [self.result sendError:error]; + return; + } + dispatch_async(self.ioQueue, ^{ + NSData *data = photoDataProvider(); + bool success = [data writeToFile:self.path atomically:YES]; + + if (!success) { + [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; + return; + } + [self.result sendSuccessWithData:self.path]; + }); +} + - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error API_AVAILABLE(ios(10)) { - selfReference = nil; - if (error) { - [_result sendError:error]; - return; - } - - NSData *data = [AVCapturePhotoOutput - JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer - previewPhotoSampleBuffer:previewPhotoSampleBuffer]; - - // TODO(sigurdm): Consider writing file asynchronously. - bool success = [data writeToFile:_path atomically:YES]; - - if (!success) { - [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; - return; - } - [_result sendSuccessWithData:_path]; + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [AVCapturePhotoOutput + JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer + previewPhotoSampleBuffer: + previewPhotoSampleBuffer]; + }]; } - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error API_AVAILABLE(ios(11.0)) { - selfReference = nil; - if (error) { - [_result sendError:error]; - return; - } - - NSData *photoData = [photo fileDataRepresentation]; - - bool success = [photoData writeToFile:_path atomically:YES]; - if (!success) { - [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; - return; - } - [_result sendSuccessWithData:_path]; + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [photo fileDataRepresentation]; + }]; } + @end @interface FLTCam () =2.14.0 <3.0.0" From 429381a53fafec88fcb1eb63e997ac4a3a507d7e Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 13:05:45 -0800 Subject: [PATCH 2/8] [camera]update license --- .../ios/RunnerTests/FLTSavePhotoDelegateTests.m | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index c91e63d43ca5..9676804dd439 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -1,10 +1,6 @@ -// -// FLTSavePhotoDelegateTests.m -// RunnerTests -// -// Created by Huan Lin on 1/31/22. -// Copyright © 2022 The Flutter Authors. All rights reserved. -// +// 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 camera.Test; From 57f3c8455e014ea4ba329001cb2e52aeccffb7f3 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 13:35:04 -0800 Subject: [PATCH 3/8] [camera]update readme --- packages/camera/camera/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 8a320f646542..f846774ffe0a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.9.4+10 -* iOS performance improvement by writing captured photo file on a background IO queue. +* iOS performance improvement by moving file writing from the main queue to a background IO queue. ## 0.9.4+9 From 7911ecdc6703c97267cc18c547dbe90601779119 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 16:39:12 -0800 Subject: [PATCH 4/8] [camera]move photo delegate out of fltcam and expose test header to test module --- .../RunnerTests/FLTSavePhotoDelegateTests.m | 9 --- .../camera/ios/Classes/CameraPlugin.modulemap | 1 + packages/camera/camera/ios/Classes/FLTCam.m | 69 +------------------ .../camera/ios/Classes/FLTSavePhotoDelegate.h | 37 ++++++++++ .../camera/ios/Classes/FLTSavePhotoDelegate.m | 64 +++++++++++++++++ .../ios/Classes/FLTSavePhotoDelegate_Test.h | 17 +++++ 6 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h create mode 100644 packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m create mode 100644 packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index 9676804dd439..5b2f7335966b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -8,15 +8,6 @@ @import XCTest; #import -@interface FLTSavePhotoDelegate : NSObject -@property(readonly, nonatomic) NSString *path; -- initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue; -- (void)handlePhotoCaptureResultWithError:(nullable NSError *)error - photoDataProvider:(NSData * (^)(void))photoDataProvider; -@end - @interface FLTSavePhotoDelegateTests : XCTestCase @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap index a695728fc87a..c10fb76556ba 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap +++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap @@ -9,5 +9,6 @@ framework module camera { header "CameraProperties.h" header "FLTCam.h" header "FLTCam_Test.h" + header "FLTSavePhotoDelegate_Test.h" } } diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index f184aa37ea3b..94f985066675 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -4,6 +4,7 @@ #import "FLTCam.h" #import "FLTCam_Test.h" +#import "FLTSavePhotoDelegate.h" @import CoreMotion; #import @@ -41,74 +42,6 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments } @end -@interface FLTSavePhotoDelegate : NSObject -@property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; -/// The queue on which captured photos are wrote to disk. -@property(strong, nonatomic) dispatch_queue_t ioQueue; -/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. -@property(strong, nonatomic) FLTSavePhotoDelegate *selfReference; -@end - -@implementation FLTSavePhotoDelegate - -- initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _path = path; - _selfReference = self; - _result = result; - _ioQueue = ioQueue; - return self; -} - -- (void)handlePhotoCaptureResultWithError:(NSError *)error - photoDataProvider:(NSData * (^)(void))photoDataProvider { - self.selfReference = nil; - if (error) { - [self.result sendError:error]; - return; - } - dispatch_async(self.ioQueue, ^{ - NSData *data = photoDataProvider(); - bool success = [data writeToFile:self.path atomically:YES]; - - if (!success) { - [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; - return; - } - [self.result sendSuccessWithData:self.path]; - }); -} - -- (void)captureOutput:(AVCapturePhotoOutput *)output - didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer - previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer - resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings - bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings - error:(NSError *)error API_AVAILABLE(ios(10)) { - [self handlePhotoCaptureResultWithError:error - photoDataProvider:^NSData * { - return [AVCapturePhotoOutput - JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer - previewPhotoSampleBuffer: - previewPhotoSampleBuffer]; - }]; -} - -- (void)captureOutput:(AVCapturePhotoOutput *)output - didFinishProcessingPhoto:(AVCapturePhoto *)photo - error:(NSError *)error API_AVAILABLE(ios(11.0)) { - [self handlePhotoCaptureResultWithError:error - photoDataProvider:^NSData * { - return [photo fileDataRepresentation]; - }]; -} - -@end - @interface FLTCam () diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h new file mode 100644 index 000000000000..a773b4613931 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h @@ -0,0 +1,37 @@ +// 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 AVFoundation; +@import Foundation; +@import Flutter; + +#import "FLTThreadSafeFlutterResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + Delegate object that handles photo capture results. + */ +@interface FLTSavePhotoDelegate : NSObject +/// The file path for the captured photo. +@property(readonly, nonatomic) NSString *path; +/// The thread safe flutter result wrapper to report the result. +@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; +/// The queue on which captured photos are wrote to disk. +@property(strong, nonatomic) dispatch_queue_t ioQueue; +/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. +@property(strong, nonatomic, nullable) FLTSavePhotoDelegate *selfReference; + +/** + * Initialize a photo capture delegate. + * @param path the path for captured photo file. + * @param result the thread safe flutter result wrapper to report the result. + * @param ioQueue the queue on which captured photos are wrote to disk. + */ +- (instancetype)initWithPath:(NSString *)path + result:(FLTThreadSafeFlutterResult *)result + ioQueue:(dispatch_queue_t)ioQueue; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m new file mode 100644 index 000000000000..782332003df7 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -0,0 +1,64 @@ +// 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 "FLTSavePhotoDelegate.h" + +@implementation FLTSavePhotoDelegate + +- (instancetype)initWithPath:(NSString *)path + result:(FLTThreadSafeFlutterResult *)result + ioQueue:(dispatch_queue_t)ioQueue { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _path = path; + _selfReference = self; + _result = result; + _ioQueue = ioQueue; + return self; +} + +- (void)handlePhotoCaptureResultWithError:(NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider { + self.selfReference = nil; + if (error) { + [self.result sendError:error]; + return; + } + dispatch_async(self.ioQueue, ^{ + NSData *data = photoDataProvider(); + bool success = [data writeToFile:self.path atomically:YES]; + + if (!success) { + [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; + return; + } + [self.result sendSuccessWithData:self.path]; + }); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer + previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer + resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings + bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings + error:(NSError *)error API_AVAILABLE(ios(10)) { + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [AVCapturePhotoOutput + JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer + previewPhotoSampleBuffer: + previewPhotoSampleBuffer]; + }]; +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhoto:(AVCapturePhoto *)photo + error:(NSError *)error API_AVAILABLE(ios(11.0)) { + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [photo fileDataRepresentation]; + }]; +} + +@end diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h new file mode 100644 index 000000000000..c0b77c7bac83 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h @@ -0,0 +1,17 @@ +// 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 "FLTSavePhotoDelegate.h" + +/** + API exposed for unit tests. + */ +@interface FLTSavePhotoDelegate () + +/// Handler to write captured photo data into a file. +/// @param error the capture error. +/// @param photoDataProvider a closure that provides photo data. +- (void)handlePhotoCaptureResultWithError:(NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider; +@end From b0e11396ef47d1d35bd9564c401450128b6af47e Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 17:47:54 -0800 Subject: [PATCH 5/8] [camera]address some nits --- .../ios/RunnerTests/FLTSavePhotoDelegateTests.m | 10 +++++----- .../camera/camera/ios/Classes/FLTSavePhotoDelegate.m | 8 +++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index 5b2f7335966b..61d641953475 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -26,7 +26,7 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture { photoDataProvider:^NSData * { return nil; }]; - OCMVerify(times(1), [mockResult sendError:error]); + OCMVerify([mockResult sendError:error]); } - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { @@ -45,7 +45,7 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]).andReturn(NO); + OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]).andReturn(NO); [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { return mockData; @@ -69,7 +69,7 @@ - (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]).andReturn(YES); + OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]).andReturn(YES); [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { @@ -90,14 +90,14 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue const char *ioQueueSpecific = "io_queue_specific"; dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - OCMStub([mockResult sendSuccessWithData:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { [resultExpectation fulfill]; }); // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:[OCMArg any] atomically:[OCMArg any]]) + OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]) .andDo(^(NSInvocation *invocation) { if (dispatch_get_specific(ioQueueSpecific)) { [writeFileQueueExpectation fulfill]; diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index 782332003df7..d41b2bbf62cf 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -27,13 +27,11 @@ - (void)handlePhotoCaptureResultWithError:(NSError *)error } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); - bool success = [data writeToFile:self.path atomically:YES]; - - if (!success) { + if ([data writeToFile:self.path atomically:YES]) { + [self.result sendSuccessWithData:self.path]; + } else { [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; - return; } - [self.result sendSuccessWithData:self.path]; }); } From 87ecfb30cc0db9ab467221189eca403773ef246c Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 17:50:54 -0800 Subject: [PATCH 6/8] [camera]remove thread safe wrappers from umbrella header --- .../camera/example/ios/Runner.xcodeproj/project.pbxproj | 2 +- packages/camera/camera/ios/Classes/CameraPlugin.modulemap | 5 +++++ packages/camera/camera/ios/Classes/camera-umbrella.h | 5 ----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 53f22bc60f9d..ac39de2e3f84 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap index c10fb76556ba..529c6580e908 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap +++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap @@ -10,5 +10,10 @@ framework module camera { header "FLTCam.h" header "FLTCam_Test.h" header "FLTSavePhotoDelegate_Test.h" + header "FLTThreadSafeEventChannel.h" + header "FLTThreadSafeFlutterResult.h" + header "FLTThreadSafeMethodChannel.h" + header "FLTThreadSafeTextureRegistry.h" + header "QueueHelper.h" } } diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index 428b125d3a43..5c39401e6261 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,11 +4,6 @@ #import #import -#import -#import -#import -#import -#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[]; From 4174a54a61d82d9762df78e005dbce7ee7d8e9ad Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 17:58:37 -0800 Subject: [PATCH 7/8] [camera]passing file writing error details to flutter result --- packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index d41b2bbf62cf..8dadfec7fecd 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -27,10 +27,13 @@ - (void)handlePhotoCaptureResultWithError:(NSError *)error } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); - if ([data writeToFile:self.path atomically:YES]) { + NSError *ioError; + if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { [self.result sendSuccessWithData:self.path]; } else { - [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; + [self.result sendErrorWithCode:@"IOError" + message:@"Unable to write file" + details:ioError.localizedDescription]; } }); } From 86569d7e79b032f657419c048a06b698a04d4392 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 1 Feb 2022 18:10:13 -0800 Subject: [PATCH 8/8] [camera]fix unit tests after nit --- .../RunnerTests/FLTSavePhotoDelegateTests.m | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index 61d641953475..b6ea84da449c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -34,7 +34,14 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - OCMStub([mockResult sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]) + + NSError *ioError = [NSError errorWithDomain:@"IOError" + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; + + OCMStub([mockResult sendErrorWithCode:@"IOError" + message:@"Unable to write file" + details:ioError.localizedDescription]) .andDo(^(NSInvocation *invocation) { [resultExpectation fulfill]; }); @@ -45,7 +52,10 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]).andReturn(NO); + OCMStub([mockData writeToFile:OCMOCK_ANY + options:NSDataWritingAtomic + error:[OCMArg setTo:ioError]]) + .andReturn(NO); [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { return mockData; @@ -69,7 +79,8 @@ - (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]).andReturn(YES); + OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) + .andReturn(YES); [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { @@ -97,7 +108,7 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:OCMOCK_ANY atomically:OCMOCK_ANY]) + OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) .andDo(^(NSInvocation *invocation) { if (dispatch_get_specific(ioQueueSpecific)) { [writeFileQueueExpectation fulfill];