Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Predicate access denied to avoid deadlocks #235

Merged
merged 5 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading