Skip to content

Commit

Permalink
🐛 Predicate access denied to avoid deadlocks (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexV525 authored Feb 26, 2024
1 parent b5fcda9 commit 3ffa737
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 53 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ See the [Migration Guide](guides/migration_guide.md) for breaking changes betwee

- Use `wechat_picker_library`.

### Fixes

- Predicate access denied to avoid deadlocks.

## 4.2.0-dev.3

### Improvements
Expand Down
138 changes: 85 additions & 53 deletions lib/src/states/camera_picker_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ const Duration _kDuration = Duration(milliseconds: 300);

class CameraPickerState extends State<CameraPicker>
with WidgetsBindingObserver {
/// The controller for the current camera.
/// 当前相机实例的控制器
CameraController get controller => innerController!;
CameraController? innerController;

/// Whether the access to the camera or the audio session
/// has been denied by the platform.
bool accessDenied = false;

/// Available cameras.
/// 可用的相机实例
late List<CameraDescription> cameras;

/// Whether the controller is handling method calls.
/// 相机控制器是否在处理方法调用
bool isControllerBusy = false;

/// A [Completer] lock to keep the initialization only runs once at a time.
Completer<void>? initializeLock;

/// The [Duration] for record detection. (200ms)
/// 检测是否开始录制的时长 (200毫秒)
final Duration recordDetectDuration = const Duration(milliseconds: 200);
Expand All @@ -47,19 +67,6 @@ class CameraPickerState extends State<CameraPicker>
final ValueNotifier<bool> isFocusPointDisplays = ValueNotifier<bool>(false);
final ValueNotifier<bool> isFocusPointFadeOut = ValueNotifier<bool>(false);

/// The controller for the current camera.
/// 当前相机实例的控制器
CameraController get controller => innerController!;
CameraController? innerController;

/// Available cameras.
/// 可用的相机实例
late List<CameraDescription> cameras;

/// Whether the controller is handling method calls.
/// 相机控制器是否在处理方法调用
bool isControllerBusy = false;

/// Current exposure offset.
/// 当前曝光值
final ValueNotifier<double> currentExposureOffset = ValueNotifier<double>(0);
Expand Down Expand Up @@ -253,8 +260,8 @@ class CameraPickerState extends State<CameraPicker>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final CameraController? c = innerController;
if (state == AppLifecycleState.resumed) {
initCameras(currentCamera);
if (state == AppLifecycleState.resumed && !accessDenied) {
initCameras(cameraDescription: currentCamera);
} else if (c == null || !c.value.isInitialized) {
// App state changed before we got the chance to initialize.
return;
Expand Down Expand Up @@ -319,34 +326,43 @@ class CameraPickerState extends State<CameraPicker>

/// Initialize cameras instances.
/// 初始化相机实例
Future<void> initCameras([CameraDescription? cameraDescription]) async {
// Save the current controller to a local variable.
final CameraController? c = innerController;
// Dispose at last to avoid disposed usage with assertions.
if (c != null) {
innerController = null;
await c.dispose();
}
// Then request a new frame to unbind the controller from elements.
safeSetState(() {
maxAvailableZoom = 1;
minAvailableZoom = 1;
currentZoom = 1;
baseZoom = 1;
// Meanwhile, cancel the existed exposure point and mode display.
exposurePointDisplayTimer?.cancel();
exposureModeDisplayTimer?.cancel();
exposureFadeOutTimer?.cancel();
isFocusPointDisplays.value = false;
isFocusPointFadeOut.value = false;
lastExposurePoint.value = null;
currentExposureOffset.value = 0;
currentExposureSliderOffset.value = 0;
lockedCaptureOrientation = pickerConfig.lockCaptureOrientation;
});
// **IMPORTANT**: Push methods into a post frame callback, which ensures the
// controller has already unbind from widgets.
ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) async {
Future<void> initCameras({
CameraDescription? cameraDescription,
bool ignoreLocks = false,
}) {
if (initializeLock != null && !ignoreLocks) {
return initializeLock!.future;
}
final lock = ignoreLocks ? initializeLock! : Completer<void>();
if (ignoreLocks) {
initializeLock = lock;
}
Future(() async {
// Save the current controller to a local variable.
final CameraController? c = innerController;
// Dispose at last to avoid disposed usage with assertions.
if (c != null) {
innerController = null;
await c.dispose();
}
// Then request a new frame to unbind the controller from elements.
safeSetState(() {
maxAvailableZoom = 1;
minAvailableZoom = 1;
currentZoom = 1;
baseZoom = 1;
// Meanwhile, cancel the existed exposure point and mode display.
exposurePointDisplayTimer?.cancel();
exposureModeDisplayTimer?.cancel();
exposureFadeOutTimer?.cancel();
isFocusPointDisplays.value = false;
isFocusPointFadeOut.value = false;
lastExposurePoint.value = null;
currentExposureOffset.value = 0;
currentExposureSliderOffset.value = 0;
lockedCaptureOrientation = pickerConfig.lockCaptureOrientation;
});
await Future.microtask(() {});
// When the [cameraDescription] is null, which means this is the first
// time initializing cameras, so available cameras should be fetched.
if (cameraDescription == null) {
Expand Down Expand Up @@ -388,12 +404,13 @@ class CameraPickerState extends State<CameraPicker>
enableAudio: enableAudio,
imageFormatGroup: pickerConfig.imageFormatGroup,
);

try {
final Stopwatch stopwatch = Stopwatch()..start();
await newController.initialize();
stopwatch.stop();
realDebugPrint("${stopwatch.elapsed} for controller's initialization.");
realDebugPrint(
"${stopwatch.elapsed} for controller's initialization.",
);
// Call recording preparation first.
if (shouldPrepareForVideoRecording) {
stopwatch
Expand Down Expand Up @@ -474,18 +491,33 @@ class CameraPickerState extends State<CameraPicker>
stopwatch.stop();
realDebugPrint("${stopwatch.elapsed} for config's update.");
innerController = newController;
lock.complete();
} catch (e, s) {
handleErrorWithHandler(e, s, pickerConfig.onError);
if (!retriedAfterInvalidInitialize) {
retriedAfterInvalidInitialize = true;
Future.delayed(Duration.zero, initCameras);
accessDenied = e is CameraException && e.code.contains('Access');
if (!accessDenied) {
if (!retriedAfterInvalidInitialize) {
retriedAfterInvalidInitialize = true;
Future.delayed(Duration.zero, () {
initCameras(
cameraDescription: cameraDescription,
ignoreLocks: true,
);
});
} else {
retriedAfterInvalidInitialize = false;
lock.completeError(e, s);
}
} else {
retriedAfterInvalidInitialize = false;
lock.completeError(e, s);
}
} finally {
safeSetState(() {});
}
});
return lock.future.catchError((e, s) {
handleErrorWithHandler(e, s, pickerConfig.onError);
}).whenComplete(() {
initializeLock = null;
safeSetState(() {});
});
}

/// Starts to listen on accelerometer events.
Expand Down Expand Up @@ -569,7 +601,7 @@ class CameraPickerState extends State<CameraPicker>
if (currentCameraIndex == cameras.length) {
currentCameraIndex = 0;
}
initCameras(currentCamera);
initCameras(cameraDescription: currentCamera);
}

/// Obtain the next camera description for semantics.
Expand Down

0 comments on commit 3ffa737

Please sign in to comment.