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

Commit

Permalink
[camera]fix crash due to race condition in dispatch queue (#4619)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellohuanlin authored Jan 25, 2022
1 parent 4d8e98d commit 6524fb6
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 11 deletions.
5 changes: 3 additions & 2 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -122,6 +124,7 @@
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */,
E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */,
);
path = RunnerTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
10 changes: 2 additions & 8 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -1322,9 +1322,7 @@ @interface CameraPlugin ()
@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel;
@end

@implementation CameraPlugin {
dispatch_queue_t _captureSessionQueue;
}
@implementation CameraPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
Expand All @@ -1341,6 +1339,7 @@ - (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
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;
Expand Down Expand Up @@ -1385,10 +1384,6 @@ - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation {
}

- (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 =
Expand Down Expand Up @@ -1507,7 +1502,6 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
} else if ([@"dispose" isEqualToString:call.method]) {
[_registry unregisterTexture:cameraId];
[_camera close];
_captureSessionQueue = nil;
[result sendSuccess];
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
[_camera setUpCaptureSessionForAudio];
Expand Down
3 changes: 3 additions & 0 deletions packages/camera/camera/ios/Classes/CameraPlugin_Test.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
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"
Expand Down

0 comments on commit 6524fb6

Please sign in to comment.