Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 11f590d

Browse files
committed
[camera]remove self ref pointer for save photo delegate, and make sure thread safety"
1 parent 9d32c83 commit 11f590d

File tree

11 files changed

+307
-79
lines changed

11 files changed

+307
-79
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.4+12
2+
3+
* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup.
4+
15
## 0.9.4+11
26

37
* Manages iOS camera's orientation-related states on a background queue to prevent potential race conditions.

packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 50;
6+
objectVersion = 46;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -23,11 +23,12 @@
2323
E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; };
2424
E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; };
2525
E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; };
26+
E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; };
27+
E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; };
2628
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; };
2729
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; };
2830
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; };
2931
E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; };
30-
E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */; };
3132
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
3233
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; };
3334
/* End PBXBuildFile section */
@@ -85,11 +86,12 @@
8586
E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = "<group>"; };
8687
E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = "<group>"; };
8788
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = "<group>"; };
89+
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = "<group>"; };
90+
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = "<group>"; };
8891
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = "<group>"; };
8992
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = "<group>"; };
9093
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = "<group>"; };
9194
E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = "<group>"; };
92-
E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleBufferQueueTests.m; sourceTree = "<group>"; };
9395
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
9496
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = "<group>"; };
9597
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = "<group>"; };
@@ -126,8 +128,9 @@
126128
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */,
127129
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */,
128130
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */,
129-
E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */,
130131
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */,
132+
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */,
133+
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */,
131134
E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */,
132135
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
133136
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
@@ -396,12 +399,13 @@
396399
isa = PBXSourcesBuildPhase;
397400
buildActionMask = 2147483647;
398401
files = (
399-
E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */,
400402
03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */,
401403
033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */,
404+
E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */,
402405
E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */,
403406
03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
404407
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
408+
E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */,
405409
E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */,
406410
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */,
407411
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import camera;
6+
@import camera.Test;
7+
@import AVFoundation;
8+
@import XCTest;
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FLTCamPhotoCaptureTests : XCTestCase
12+
13+
@end
14+
15+
@implementation FLTCamPhotoCaptureTests
16+
17+
- (void)testCaptureToFile_savePhotoDelegateReferencesMustBeAccessedOnCaptureSessionQueue {
18+
XCTestExpectation *setReferenceExpectation =
19+
[self expectationWithDescription:
20+
@"SavePhotoDelegate references must be set on capture session queue."];
21+
XCTestExpectation *clearReferenceExpectation =
22+
[self expectationWithDescription:
23+
@"SavePhotoDelegate references must be cleared on capture session queue."];
24+
25+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
26+
const char *captureSessionQueueSpecific = "capture_session_queue";
27+
dispatch_queue_set_specific(captureSessionQueue, captureSessionQueueSpecific,
28+
(void *)captureSessionQueueSpecific, NULL);
29+
FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue];
30+
31+
// settings.uniqueID is used as the key for `inProgressSavePhotoDelegates` dictionary
32+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
33+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
34+
OCMStub([mockSettings photoSettings]).andReturn(settings);
35+
36+
// We need to make sure the delegate reference is actually saved in a real dictionary, so that we
37+
// can call its completion handler later. Must use a partial mock, in order to forward invocation
38+
// to the real object.
39+
id mockDelegates = OCMPartialMock([NSMutableDictionary dictionary]);
40+
OCMStub([mockDelegates setObject:OCMOCK_ANY forKeyedSubscript:OCMOCK_ANY])
41+
.andDo(^(NSInvocation *invocation) {
42+
if (dispatch_get_specific(captureSessionQueueSpecific)) {
43+
FLTSavePhotoDelegate *delegate;
44+
// Index 0 and 1 are `self` and `_cmd`.
45+
[invocation getArgument:&delegate atIndex:2];
46+
if (delegate) {
47+
[setReferenceExpectation fulfill];
48+
} else {
49+
[clearReferenceExpectation fulfill];
50+
}
51+
}
52+
})
53+
.andForwardToRealObject();
54+
cam.inProgressSavePhotoDelegates = mockDelegates;
55+
56+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
57+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
58+
.andDo(^(NSInvocation *invocation) {
59+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
60+
XCTAssertNotNil(delegate, @"Delegate reference must be saved to the dictionary.");
61+
// Completion runs on IO queue.
62+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
63+
dispatch_async(ioQueue, ^{
64+
NSString *filePath = @"test";
65+
delegate.completionHandler(nil, filePath);
66+
});
67+
});
68+
cam.capturePhotoOutput = mockOutput;
69+
70+
// `FLTCam::captureToFile` runs on capture session queue.
71+
dispatch_async(captureSessionQueue, ^{
72+
[cam captureToFile:OCMClassMock([FLTThreadSafeFlutterResult class])];
73+
});
74+
75+
[self waitForExpectationsWithTimeout:1 handler:nil];
76+
}
77+
78+
- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError {
79+
XCTestExpectation *errorExpectation =
80+
[self expectationWithDescription:
81+
@"Must send error to result if save photo delegate completes with error."];
82+
83+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
84+
FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue];
85+
86+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
87+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
88+
OCMStub([mockSettings photoSettings]).andReturn(settings);
89+
90+
NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
91+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
92+
OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) {
93+
[errorExpectation fulfill];
94+
});
95+
96+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
97+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
98+
.andDo(^(NSInvocation *invocation) {
99+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
100+
// Completion runs on IO queue.
101+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
102+
dispatch_async(ioQueue, ^{
103+
delegate.completionHandler(error, nil);
104+
});
105+
});
106+
cam.capturePhotoOutput = mockOutput;
107+
108+
// `FLTCam::captureToFile` runs on capture session queue.
109+
dispatch_async(captureSessionQueue, ^{
110+
[cam captureToFile:mockResult];
111+
});
112+
113+
[self waitForExpectationsWithTimeout:1 handler:nil];
114+
}
115+
116+
- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath {
117+
XCTestExpectation *pathExpectation =
118+
[self expectationWithDescription:
119+
@"Must send file path to result if save photo delegate completes with file path."];
120+
121+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
122+
FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue];
123+
124+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
125+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
126+
OCMStub([mockSettings photoSettings]).andReturn(settings);
127+
128+
NSString *filePath = @"test";
129+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
130+
OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) {
131+
[pathExpectation fulfill];
132+
});
133+
134+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
135+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
136+
.andDo(^(NSInvocation *invocation) {
137+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
138+
// Completion runs on IO queue.
139+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
140+
dispatch_async(ioQueue, ^{
141+
delegate.completionHandler(nil, filePath);
142+
});
143+
});
144+
cam.capturePhotoOutput = mockOutput;
145+
146+
// `FLTCam::captureToFile` runs on capture session queue.
147+
dispatch_async(captureSessionQueue, ^{
148+
[cam captureToFile:mockResult];
149+
});
150+
[self waitForExpectationsWithTimeout:1 handler:nil];
151+
}
152+
153+
/// Creates an `FLTCam` that runs its operations on a given capture session queue.
154+
- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue {
155+
id inputMock = OCMClassMock([AVCaptureDeviceInput class]);
156+
OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]])
157+
.andReturn(inputMock);
158+
159+
id sessionMock = OCMClassMock([AVCaptureSession class]);
160+
OCMStub([sessionMock alloc]).andReturn(sessionMock);
161+
OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op
162+
OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES);
163+
164+
return [[FLTCam alloc] initWithCameraName:@"camera"
165+
resolutionPreset:@"medium"
166+
enableAudio:true
167+
orientation:UIDeviceOrientationPortrait
168+
captureSessionQueue:captureSessionQueue
169+
error:nil];
170+
}
171+
172+
@end

packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m renamed to packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
@import XCTest;
99
#import <OCMock/OCMock.h>
1010

11-
@interface SampleBufferQueueTests : XCTestCase
11+
@interface FLTCamSampleBufferTests : XCTestCase
1212

1313
@end
1414

15-
@implementation SampleBufferQueueTests
15+
@implementation FLTCamSampleBufferTests
1616

1717
- (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue {
1818
id inputMock = OCMClassMock([AVCaptureDeviceInput class]);

0 commit comments

Comments
 (0)