From f14a94d6393aeef729c71f90e9b6c6495bf01a06 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 20 Jun 2026 01:07:20 +0200 Subject: [PATCH] fix camera initalization issue --- lib/main.dart | 9 ++ .../camera_preview_controller_view.dart | 1 + .../main_camera_controller.dart | 87 ++++++++++++------- .../permissions_view.dart | 60 ++++++++++--- .../views/camera/camera_qr_scanner.view.dart | 17 ++-- .../views/camera/camera_send_to.view.dart | 17 ++-- lib/src/visual/views/home.view.dart | 12 ++- pubspec.yaml | 2 +- 8 files changed, 148 insertions(+), 57 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 70fb9229..b2ca02c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mutex/mutex.dart'; @@ -68,6 +69,14 @@ Future twonlyMinimumInitialization() async { void main() async { final binding = SentryWidgetsFlutterBinding.ensureInitialized(); await AppEnvironment.init(); + + // Preload available cameras in the background to speed up camera tab startup + unawaited( + availableCameras().then((cameras) { + AppEnvironment.cameras = cameras; + }), + ); + final stopwatch = Stopwatch()..start(); unawaited(StartupGuard.markAppStartup()); diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index deb27d62..49810100 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -108,6 +108,7 @@ class _CameraPreviewControllerViewState ); } else { return PermissionHandlerView( + triggerPermissionRequest: widget.isVisible, onSuccess: () { setState(() { AppState.hasCameraPermissions = true; diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 3c12a7cd..8f722fc4 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -134,14 +134,25 @@ class MainCameraController { } Future selectCamera(int sCameraId, bool init) async { + if (initCameraStarted) return; initCameraStarted = true; final sessionId = ++_cameraSessionId; - await _pendingDisposal; - if (sessionId != _cameraSessionId) return; - if (AppEnvironment.cameras.isEmpty) { - AppEnvironment.cameras = await availableCameras(); + // Start checking microphone permission concurrently + final micPermissionFuture = Permission.microphone.isGranted; + + try { + await _pendingDisposal; if (sessionId != _cameraSessionId) return; + + if (AppEnvironment.cameras.isEmpty) { + AppEnvironment.cameras = await availableCameras(); + if (sessionId != _cameraSessionId) return; + } + } catch (e) { + Log.error('Error querying available cameras: $e'); + initCameraStarted = false; + return; } var cameraId = sCameraId; @@ -149,6 +160,7 @@ class MainCameraController { Log.warn( 'Trying to select a non existing camera $cameraId >= ${AppEnvironment.cameras.length}', ); + initCameraStarted = false; return; } @@ -159,12 +171,21 @@ class MainCameraController { break; } } + if (cameraId >= AppEnvironment.cameras.length) { + cameraId = sCameraId; + } } selectedCameraDetails.isZoomAble = false; - if (cameraController == null) { - final hasMic = await Permission.microphone.isGranted; + if (cameraController == null || !cameraController!.value.isInitialized) { + final controllerToDispose = cameraController; + cameraController = null; + if (controllerToDispose != null) { + unawaited(controllerToDispose.dispose()); + } + + final hasMic = await micPermissionFuture; if (sessionId != _cameraSessionId) return; cameraController = CameraController( @@ -178,56 +199,55 @@ class MainCameraController { try { _initializeFuture = cameraController?.initialize(); await _initializeFuture; - if (cameraController == null) return; - await cameraController?.startImageStream(_processCameraImage); - if (cameraController == null) return; - await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); - if (cameraController == null) return; + await cameraController!.startImageStream(_processCameraImage); + await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor); if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { - await cameraController?.setVideoStabilizationMode( + await cameraController!.setVideoStabilizationMode( VideoStabilizationMode.level1, ); } } catch (e) { + Log.error('Error initializing camera: $e'); + final controllerToDispose = cameraController; cameraController = null; + if (controllerToDispose != null) { + unawaited(controllerToDispose.dispose()); + } + initCameraStarted = false; return; } } else { try { if (!isVideoRecording) { - await cameraController?.stopImageStream(); + await cameraController!.stopImageStream(); } - if (cameraController == null) return; selectedCameraDetails.scaleFactor = 1; - await cameraController?.setZoomLevel(1); - if (cameraController == null) return; - await cameraController?.setDescription( + await cameraController!.setZoomLevel(1); + await cameraController!.setDescription( AppEnvironment.cameras[cameraId], ); - if (cameraController == null) return; if (!isVideoRecording) { - await cameraController?.startImageStream(_processCameraImage); + await cameraController!.startImageStream(_processCameraImage); } } catch (e) { - Log.info(e); + Log.error('Error switching camera description: $e'); + initCameraStarted = false; + return; } } try { - if (cameraController == null) return; - await cameraController?.lockCaptureOrientation( + await cameraController!.lockCaptureOrientation( DeviceOrientation.portraitUp, ); - if (cameraController == null) return; - await cameraController?.setFlashMode( + await cameraController!.setFlashMode( selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, ); - if (cameraController == null) return; - selectedCameraDetails.maxAvailableZoom = - await cameraController?.getMaxZoomLevel() ?? 1; - selectedCameraDetails.minAvailableZoom = - await cameraController?.getMinZoomLevel() ?? 1; + selectedCameraDetails.maxAvailableZoom = await cameraController! + .getMaxZoomLevel(); + selectedCameraDetails.minAvailableZoom = await cameraController! + .getMinZoomLevel(); selectedCameraDetails ..isZoomAble = selectedCameraDetails.maxAvailableZoom != @@ -240,10 +260,16 @@ class MainCameraController { isSelectingFaceFilters = false; setFilter(FaceFilterType.none); zoomButtonKey = GlobalKey(); + initCameraStarted = false; setState?.call(); } catch (e) { - Log.error(e); + Log.error('Error post-initializing camera: $e'); + final controllerToDispose = cameraController; cameraController = null; + if (controllerToDispose != null) { + unawaited(controllerToDispose.dispose()); + } + initCameraStarted = false; return; } } @@ -310,7 +336,6 @@ class MainCameraController { } final inputImage = _inputImageFromCameraImage(image); if (inputImage == null) return; - _processBarcode(inputImage); // check if front camera is selected if (cameraController?.description.lensDirection == CameraLensDirection.front) { diff --git a/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart b/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart index fa14088e..fe90a189 100644 --- a/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart @@ -2,12 +2,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; class PermissionHandlerView extends StatefulWidget { - const PermissionHandlerView({required this.onSuccess, super.key}); + const PermissionHandlerView({ + required this.onSuccess, + this.triggerPermissionRequest = true, + super.key, + }); final Function onSuccess; + final bool triggerPermissionRequest; @override PermissionHandlerViewState createState() => PermissionHandlerViewState(); @@ -17,10 +23,6 @@ Future checkPermissions() async { if (!await Permission.camera.isGranted) { return false; } - if (!await Permission.microphone.isGranted) { - // microphone is only needed when - return true; - } return true; } @@ -36,6 +38,32 @@ class PermissionHandlerViewState extends State _timer = Timer.periodic(const Duration(milliseconds: 500), (timer) async { await _checkAndTriggerSuccess(); }); + if (widget.triggerPermissionRequest) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _requestPermissions(); + }); + } + } + + @override + void didUpdateWidget(PermissionHandlerView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.triggerPermissionRequest && !oldWidget.triggerPermissionRequest) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _requestPermissions(); + }); + } + } + + Future _requestPermissions() async { + try { + await permissionServices(); + if (await checkPermissions()) { + _handleSuccess(); + } + } catch (e) { + Log.error(e); + } } @override @@ -54,21 +82,32 @@ class PermissionHandlerViewState extends State Future _checkAndTriggerSuccess() async { if (_isSuccessTriggered) return; + final route = ModalRoute.of(context); + if (route != null && !route.isCurrent) { + return; + } try { if (await checkPermissions()) { - _isSuccessTriggered = true; - _timer?.cancel(); - // ignore: avoid_dynamic_calls - widget.onSuccess(); + _handleSuccess(); } } catch (e) { Log.error(e); } } + void _handleSuccess() { + if (_isSuccessTriggered) return; + _isSuccessTriggered = true; + _timer?.cancel(); + unawaited(UserService.update((u) => u.requestedAudioPermission = true)); + // ignore: avoid_dynamic_calls + widget.onSuccess(); + } + Future> permissionServices() async { final statuses = await [ Permission.camera, + Permission.microphone, Permission.notification, ].request(); @@ -100,8 +139,7 @@ class PermissionHandlerViewState extends State try { await permissionServices(); if (await checkPermissions()) { - // ignore: avoid_dynamic_calls - widget.onSuccess(); + _handleSuccess(); } } catch (e) { Log.error(e); diff --git a/lib/src/visual/views/camera/camera_qr_scanner.view.dart b/lib/src/visual/views/camera/camera_qr_scanner.view.dart index 87614e4b..06aa8ee9 100644 --- a/lib/src/visual/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/visual/views/camera/camera_qr_scanner.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; @@ -19,7 +20,11 @@ class QrCodeScannerViewState extends State { _mainCameraController.setState = () { if (mounted) setState(() {}); }; - unawaited(_mainCameraController.selectCamera(0, true)); + Permission.camera.isGranted.then((hasPermission) { + if (hasPermission && mounted) { + unawaited(_mainCameraController.selectCamera(0, true)); + } + }); } @override @@ -44,10 +49,12 @@ class QrCodeScannerViewState extends State { onTapDown: _mainCameraController.onTapDown, ), ), - CameraPreviewControllerView( - mainController: _mainCameraController, - hideControllers: true, - isVisible: true, + Positioned.fill( + child: CameraPreviewControllerView( + mainController: _mainCameraController, + hideControllers: true, + isVisible: true, + ), ), ], ), diff --git a/lib/src/visual/views/camera/camera_send_to.view.dart b/lib/src/visual/views/camera/camera_send_to.view.dart index 5e063540..389c2423 100644 --- a/lib/src/visual/views/camera/camera_send_to.view.dart +++ b/lib/src/visual/views/camera/camera_send_to.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; @@ -21,7 +22,11 @@ class CameraSendToViewState extends State { _mainCameraController.setState = () { if (mounted) setState(() {}); }; - unawaited(_mainCameraController.selectCamera(0, true)); + Permission.camera.isGranted.then((hasPermission) { + if (hasPermission && mounted) { + unawaited(_mainCameraController.selectCamera(0, true)); + } + }); } @override @@ -46,10 +51,12 @@ class CameraSendToViewState extends State { onTapDown: _mainCameraController.onTapDown, ), ), - CameraPreviewControllerView( - mainController: _mainCameraController, - sendToGroup: widget.sendToGroup, - isVisible: true, + Positioned.fill( + child: CameraPreviewControllerView( + mainController: _mainCameraController, + sendToGroup: widget.sendToGroup, + isVisible: true, + ), ), ], ), diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index 248cc5ee..f6cfdcba 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/providers/routing.provider.dart'; @@ -102,8 +104,8 @@ class HomeViewState extends State with WidgetsBindingObserver { }); if (initialPage == 1) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isViewActive()) { + Permission.camera.isGranted.then((hasPermission) { + if (hasPermission && mounted) { unawaited(_mainCameraController.selectCamera(0, true)); } }); @@ -219,7 +221,8 @@ class HomeViewState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { - if (_offsetRatio < 1 && + if (AppState.hasCameraPermissions && + _offsetRatio < 1 && !_mainCameraController.isSharePreviewIsShown && _isViewActive() && _mainCameraController.cameraController == null && @@ -284,7 +287,8 @@ class HomeViewState extends State with WidgetsBindingObserver { }); } - if (_mainCameraController.cameraController == null && + if (AppState.hasCameraPermissions && + _mainCameraController.cameraController == null && !_mainCameraController.initCameraStarted && _offsetRatio < 1 && _isViewActive()) { diff --git a/pubspec.yaml b/pubspec.yaml index a9b6d85e..8db2d32a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.3.2+146 +version: 0.3.3+147 environment: sdk: ^3.11.0