[camera]fix crash due to race condition in dispatch queue (#4619)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 0acd28b..cb560f5 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,6 +1,7 @@
-## NEXT
+## 0.9.4+7
-* Minor internal code cleanup.
+* Fixes a crash in iOS when passing null queue pointer into AVFoundation API due to race condition.
+* Minor iOS internal code cleanup.
## 0.9.4+6
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
index 32b770e..80f672b 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -21,6 +21,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
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 */; };
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 */; };
@@ -79,6 +80,7 @@
9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
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 = "<group>"; };
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>"; };
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>"; };
@@ -122,6 +124,7 @@
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */,
+ E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */,
);
path = RunnerTests;
sourceTree = "<group>";
@@ -390,6 +393,7 @@
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */,
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
+ E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */,
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */,
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */,
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */,
diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m
new file mode 100644
index 0000000..667a122
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m
@@ -0,0 +1,44 @@
+// 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 XCTest;
+
+@interface CameraCaptureSessionQueueRaceConditionTests : XCTestCase
+@end
+
+@implementation CameraCaptureSessionQueueRaceConditionTests
+
+- (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition {
+ CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil];
+
+ XCTestExpectation *disposeExpectation =
+ [self expectationWithDescription:@"dispose's result block must be called"];
+ XCTestExpectation *createExpectation =
+ [self expectationWithDescription:@"create's result block must be called"];
+ FlutterMethodCall *disposeCall = [FlutterMethodCall methodCallWithMethodName:@"dispose"
+ arguments:nil];
+ FlutterMethodCall *createCall = [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}];
+ // Mimic a dispose call followed by a create call, which can be triggered by slightly dragging the
+ // home bar, causing the app to be inactive, and immediately regain active.
+ [camera handleMethodCall:disposeCall
+ result:^(id _Nullable result) {
+ [disposeExpectation fulfill];
+ }];
+ [camera handleMethodCall:createCall
+ result:^(id _Nullable result) {
+ [createExpectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+ // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil
+ // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:`
+ // API will cause a crash.
+ XCTAssertNotNil(camera.captureSessionQueue,
+ @"captureSessionQueue must not be nil after create method. ");
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index f8574dc..5d70652 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -1322,9 +1322,7 @@
@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel;
@end
-@implementation CameraPlugin {
- dispatch_queue_t _captureSessionQueue;
-}
+@implementation CameraPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
@@ -1341,6 +1339,7 @@
NSAssert(self, @"super init cannot be nil");
_registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry];
_messenger = messenger;
+ _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL);
[self initDeviceEventMethodChannel];
[self startOrientationListener];
return self;
@@ -1385,10 +1384,6 @@
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
- if (_captureSessionQueue == nil) {
- _captureSessionQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL);
- }
-
// Invoke the plugin on another dispatch queue to avoid blocking the UI.
dispatch_async(_captureSessionQueue, ^{
FLTThreadSafeFlutterResult *threadSafeResult =
@@ -1507,7 +1502,6 @@
} else if ([@"dispose" isEqualToString:call.method]) {
[_registry unregisterTexture:cameraId];
[_camera close];
- _captureSessionQueue = nil;
[result sendSuccess];
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
[_camera setUpCaptureSessionForAudio];
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h
index afbf686..e36b4ab 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h
+++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h
@@ -10,6 +10,9 @@
/// Methods exposed for unit testing.
@interface CameraPlugin ()
+// All FLTCam's state access and capture session related operations should be on run on this queue.
+@property(nonatomic, strong) dispatch_queue_t captureSessionQueue;
+
/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing.
- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
messenger:(NSObject<FlutterBinaryMessenger> *)messenger
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 05c8b9d..e5a5f76 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+6
+version: 0.9.4+7
environment:
sdk: ">=2.14.0 <3.0.0"