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