[camera]remove "selfRef" for SavePhotoDelegate and ensure thread safety (#4780)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d2b2f90..0089130 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@ +## 0.9.4+13 + +* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup. + ## 0.9.4+12 * Skips unnecessary AppDelegate setup for unit tests on iOS.
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index ac39de2..5f788fc 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 = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -23,11 +23,12 @@ 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 */; }; + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.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 */; }; E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E4327A36B9200699390 /* SampleBufferQueueTests.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 */ @@ -85,11 +86,12 @@ E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = "<group>"; }; E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = "<group>"; }; E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = "<group>"; }; + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = "<group>"; }; + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.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>"; }; E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = "<group>"; }; - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleBufferQueueTests.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>"; }; @@ -126,8 +128,9 @@ E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */, E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, @@ -396,12 +399,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */, 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m new file mode 100644 index 0000000..fdb2abd --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m
@@ -0,0 +1,114 @@ +// 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; +@import AVFoundation; +@import XCTest; +#import <OCMock/OCMock.h> + +@interface FLTCamPhotoCaptureTests : XCTestCase + +@end + +@implementation FLTCamPhotoCaptureTests + +- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError { + XCTestExpectation *errorExpectation = + [self expectationWithDescription: + @"Must send error to result if save photo delegate completes with error."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) { + [errorExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(nil, error); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath { + XCTestExpectation *pathExpectation = + [self expectationWithDescription: + @"Must send file path to result if save photo delegate completes with file path."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSString *filePath = @"test"; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) { + [pathExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(filePath, nil); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/// Creates an `FLTCam` that runs its operations on a given capture session queue. +- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { + id inputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) + .andReturn(inputMock); + + id sessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([sessionMock alloc]).andReturn(sessionMock); + OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + return [[FLTCam alloc] initWithCameraName:@"camera" + resolutionPreset:@"medium" + enableAudio:true + orientation:UIDeviceOrientationPortrait + captureSessionQueue:captureSessionQueue + error:nil]; +} + +@end
diff --git a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m similarity index 93% rename from packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m rename to packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m index 19cead9..ccc8de5 100644 --- a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m
@@ -8,11 +8,11 @@ @import XCTest; #import <OCMock/OCMock.h> -@interface SampleBufferQueueTests : XCTestCase +@interface FLTCamSampleBufferTests : XCTestCase @end -@implementation SampleBufferQueueTests +@implementation FLTCamSampleBufferTests - (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { id inputMock = OCMClassMock([AVCaptureDeviceInput class]);
diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index b6ea84d..9e8e244 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m
@@ -14,40 +14,44 @@ @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]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to capture photo."]; - [delegate handlePhotoCaptureResultWithError:error + NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(captureError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; + + [delegate handlePhotoCaptureResultWithError:captureError photoDataProvider:^NSData * { return nil; }]; - OCMVerify([mockResult sendError:error]); + [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); 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]; - }); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(ioError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. @@ -63,23 +67,25 @@ [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { - XCTestExpectation *resultExpectation = [self - expectationWithDescription:@"Must send file path to the result if success to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path 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]; - }); + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(filePath, path); + [completionExpectation 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:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) + OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) .andReturn(YES); [delegate handlePhotoCaptureResultWithError:nil @@ -94,16 +100,12 @@ [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."]; + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path 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: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. @@ -116,9 +118,14 @@ }) .andReturn(YES); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + [completionExpectation fulfill]; + }]; + [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { if (dispatch_get_specific(ioQueueSpecific)) {
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 97e9954..fcea190 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -13,6 +13,7 @@ #import "FLTThreadSafeFlutterResult.h" #import "FLTThreadSafeMethodChannel.h" #import "FLTThreadSafeTextureRegistry.h" +#import "QueueHelper.h" @interface CameraPlugin () @property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @@ -38,6 +39,9 @@ _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + [self initDeviceEventMethodChannel]; [self startOrientationListener]; return self;
diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 94f9850..31a9dec 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m
@@ -5,6 +5,7 @@ #import "FLTCam.h" #import "FLTCam_Test.h" #import "FLTSavePhotoDelegate.h" +#import "QueueHelper.h" @import CoreMotion; #import <libkern/OSAtomic.h> @@ -50,7 +51,6 @@ @property(nonatomic) FLTImageStreamHandler *imageStreamHandler; @property(readonly, nonatomic) AVCaptureSession *captureSession; -@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); @property(readonly, nonatomic) AVCaptureInput *captureVideoInput; @property(readonly) CVPixelBufferRef volatile latestPixelBuffer; @property(readonly, nonatomic) CGSize captureSize; @@ -76,8 +76,8 @@ @property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor; /// All FLTCam's state access and capture session related operations should be on run on this queue. @property(strong, nonatomic) dispatch_queue_t captureSessionQueue; -/// The queue on which captured photos (not videos) are wrote to disk. -/// Videos are wrote to disk by `videoAdaptor` on an internal queue managed by AVFoundation. +/// The queue on which captured photos (not videos) are written to disk. +/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation. @property(strong, nonatomic) dispatch_queue_t photoIOQueue; @property(assign, nonatomic) UIDeviceOrientation deviceOrientation; @end @@ -110,6 +110,7 @@ _lockedCaptureOrientation = UIDeviceOrientationUnknown; _deviceOrientation = orientation; _videoFormat = kCVPixelFormatType_32BGRA; + _inProgressSavePhotoDelegates = [NSMutableDictionary dictionary]; NSError *localError = nil; _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice @@ -220,11 +221,30 @@ return; } - [_capturePhotoOutput - capturePhotoWithSettings:settings - delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path - result:result - ioQueue:self.photoIOQueue]]; + FLTSavePhotoDelegate *savePhotoDelegate = [[FLTSavePhotoDelegate alloc] + initWithPath:path + ioQueue:self.photoIOQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + dispatch_async(self.captureSessionQueue, ^{ + // Dispatch back to capture session queue to delete reference. + // Retain cycle is broken after the dictionary entry is cleared. + // This is to keep the behavior with the previous `selfReference` approach in the + // FLTSavePhotoDelegate, where delegate is released only after capture completion. + [self.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)]; + }); + + if (error) { + [result sendError:error]; + } else { + NSAssert(path, @"Path must not be nil if no error."); + [result sendSuccessWithData:path]; + } + }]; + + NSAssert(dispatch_get_specific(FLTCaptureSessionQueueSpecific), + @"save photo delegate references must be updated on the capture session queue"); + self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate; + [self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate]; } - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
diff --git a/packages/camera/camera/ios/Classes/FLTCam_Test.h b/packages/camera/camera/ios/Classes/FLTCam_Test.h index 556578e..db885d0 100644 --- a/packages/camera/camera/ios/Classes/FLTCam_Test.h +++ b/packages/camera/camera/ios/Classes/FLTCam_Test.h
@@ -3,10 +3,23 @@ // found in the LICENSE file. #import "FLTCam.h" +#import "FLTSavePhotoDelegate.h" // APIs exposed for unit testing. @interface FLTCam () +/// The output for video capturing. @property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; +/// The output for photo capturing. Exposed setter for unit tests. +@property(strong, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); + +/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the +/// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the +/// FLTSavePhotoDelegate that handles the result of each photo capture operation. Note that photo +/// capture operations may overlap, so we have to keep track of multiple delegates in progress, +/// instead of just a single delegate reference. +@property(readonly, nonatomic) + NSMutableDictionary<NSNumber *, FLTSavePhotoDelegate *> *inProgressSavePhotoDelegates; + @end
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h index a773b46..40e4562 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h
@@ -4,34 +4,35 @@ @import AVFoundation; @import Foundation; -@import Flutter; #import "FLTThreadSafeFlutterResult.h" NS_ASSUME_NONNULL_BEGIN +/// The completion handler block for save photo operations. +/// Can be called from either main queue or IO queue. +/// If success, `error` will be present and `path` will be nil. Otherewise, `error` will be nil and +/// `path` will be present. +/// @param path the path for successfully saved photo file. +/// @param error photo capture error or IO error. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSString *_Nullable path, + NSError *_Nullable error); + /** Delegate object that handles photo capture results. */ @interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate> -/// 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. + * @param ioQueue the queue on which captured photos are written to disk. + * @param completionHandler The completion handler block for save photo operations. Can + * be called from either main queue or IO queue. */ - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue; + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler; @end NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index 8dadfec..ced3cb5 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m
@@ -3,37 +3,41 @@ // found in the LICENSE file. #import "FLTSavePhotoDelegate.h" +#import "FLTSavePhotoDelegate_Test.h" + +@interface FLTSavePhotoDelegate () +/// The file path for the captured photo. +@property(readonly, nonatomic) NSString *path; +/// The queue on which captured photos are written to disk. +@property(readonly, nonatomic) dispatch_queue_t ioQueue; +@end @implementation FLTSavePhotoDelegate - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue { + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _selfReference = self; - _result = result; _ioQueue = ioQueue; + _completionHandler = completionHandler; return self; } - (void)handlePhotoCaptureResultWithError:(NSError *)error photoDataProvider:(NSData * (^)(void))photoDataProvider { - self.selfReference = nil; if (error) { - [self.result sendError:error]; + self.completionHandler(nil, error); return; } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); NSError *ioError; if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { - [self.result sendSuccessWithData:self.path]; + self.completionHandler(self.path, nil); } else { - [self.result sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]; + self.completionHandler(nil, ioError); } }); }
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h index c0b77c7..2d0d4f9 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h
@@ -9,6 +9,11 @@ */ @interface FLTSavePhotoDelegate () +/// The completion handler block for capture and save photo operations. +/// Can be called from either main queue or IO queue. +/// Exposed for unit tests to manually trigger the completion. +@property(readonly, nonatomic) FLTSavePhotoDelegateCompletionHandler completionHandler; + /// Handler to write captured photo data into a file. /// @param error the capture error. /// @param photoDataProvider a closure that provides photo data.
diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index c254814..dc73732 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h
@@ -6,8 +6,18 @@ NS_ASSUME_NONNULL_BEGIN +/// Queue-specific context data to be associated with the capture session queue. +extern const char *FLTCaptureSessionQueueSpecific; + +/// A class that contains dispatch queue related helper functions. @interface QueueHelper : NSObject + +/// Ensures the given block to be run on the main queue. +/// If caller site is already on the main queue, the block will be run synchronously. Otherwise, the +/// block will be dispatched asynchronously to the main queue. +/// @param block the block to be run on the main queue. + (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 index 194dfa9..2cef7b6 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m
@@ -4,7 +4,10 @@ #import "QueueHelper.h" +const char *FLTCaptureSessionQueueSpecific = "capture_session_queue"; + @implementation QueueHelper + + (void)ensureToRunOnMainQueue:(void (^)(void))block { if (!NSThread.isMainThread) { dispatch_async(dispatch_get_main_queue(), block); @@ -12,4 +15,5 @@ block(); } } + @end
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7421a77..80d0393 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/main/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+12 +version: 0.9.4+13 environment: sdk: ">=2.14.0 <3.0.0"