fix camera initalization issue

This commit is contained in:
otsmr 2026-06-20 01:07:20 +02:00
parent c4fc14a909
commit f14a94d639
8 changed files with 148 additions and 57 deletions

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -68,6 +69,14 @@ Future<bool> twonlyMinimumInitialization() async {
void main() async { void main() async {
final binding = SentryWidgetsFlutterBinding.ensureInitialized(); final binding = SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init(); 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(); final stopwatch = Stopwatch()..start();
unawaited(StartupGuard.markAppStartup()); unawaited(StartupGuard.markAppStartup());

View file

@ -108,6 +108,7 @@ class _CameraPreviewControllerViewState
); );
} else { } else {
return PermissionHandlerView( return PermissionHandlerView(
triggerPermissionRequest: widget.isVisible,
onSuccess: () { onSuccess: () {
setState(() { setState(() {
AppState.hasCameraPermissions = true; AppState.hasCameraPermissions = true;

View file

@ -134,14 +134,25 @@ class MainCameraController {
} }
Future<void> selectCamera(int sCameraId, bool init) async { Future<void> selectCamera(int sCameraId, bool init) async {
if (initCameraStarted) return;
initCameraStarted = true; initCameraStarted = true;
final sessionId = ++_cameraSessionId; final sessionId = ++_cameraSessionId;
await _pendingDisposal;
if (sessionId != _cameraSessionId) return;
if (AppEnvironment.cameras.isEmpty) { // Start checking microphone permission concurrently
AppEnvironment.cameras = await availableCameras(); final micPermissionFuture = Permission.microphone.isGranted;
try {
await _pendingDisposal;
if (sessionId != _cameraSessionId) return; 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; var cameraId = sCameraId;
@ -149,6 +160,7 @@ class MainCameraController {
Log.warn( Log.warn(
'Trying to select a non existing camera $cameraId >= ${AppEnvironment.cameras.length}', 'Trying to select a non existing camera $cameraId >= ${AppEnvironment.cameras.length}',
); );
initCameraStarted = false;
return; return;
} }
@ -159,12 +171,21 @@ class MainCameraController {
break; break;
} }
} }
if (cameraId >= AppEnvironment.cameras.length) {
cameraId = sCameraId;
}
} }
selectedCameraDetails.isZoomAble = false; selectedCameraDetails.isZoomAble = false;
if (cameraController == null) { if (cameraController == null || !cameraController!.value.isInitialized) {
final hasMic = await Permission.microphone.isGranted; final controllerToDispose = cameraController;
cameraController = null;
if (controllerToDispose != null) {
unawaited(controllerToDispose.dispose());
}
final hasMic = await micPermissionFuture;
if (sessionId != _cameraSessionId) return; if (sessionId != _cameraSessionId) return;
cameraController = CameraController( cameraController = CameraController(
@ -178,56 +199,55 @@ class MainCameraController {
try { try {
_initializeFuture = cameraController?.initialize(); _initializeFuture = cameraController?.initialize();
await _initializeFuture; await _initializeFuture;
if (cameraController == null) return; await cameraController!.startImageStream(_processCameraImage);
await cameraController?.startImageStream(_processCameraImage); await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor);
if (cameraController == null) return;
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
if (cameraController == null) return;
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
await cameraController?.setVideoStabilizationMode( await cameraController!.setVideoStabilizationMode(
VideoStabilizationMode.level1, VideoStabilizationMode.level1,
); );
} }
} catch (e) { } catch (e) {
Log.error('Error initializing camera: $e');
final controllerToDispose = cameraController;
cameraController = null; cameraController = null;
if (controllerToDispose != null) {
unawaited(controllerToDispose.dispose());
}
initCameraStarted = false;
return; return;
} }
} else { } else {
try { try {
if (!isVideoRecording) { if (!isVideoRecording) {
await cameraController?.stopImageStream(); await cameraController!.stopImageStream();
} }
if (cameraController == null) return;
selectedCameraDetails.scaleFactor = 1; selectedCameraDetails.scaleFactor = 1;
await cameraController?.setZoomLevel(1); await cameraController!.setZoomLevel(1);
if (cameraController == null) return; await cameraController!.setDescription(
await cameraController?.setDescription(
AppEnvironment.cameras[cameraId], AppEnvironment.cameras[cameraId],
); );
if (cameraController == null) return;
if (!isVideoRecording) { if (!isVideoRecording) {
await cameraController?.startImageStream(_processCameraImage); await cameraController!.startImageStream(_processCameraImage);
} }
} catch (e) { } catch (e) {
Log.info(e); Log.error('Error switching camera description: $e');
initCameraStarted = false;
return;
} }
} }
try { try {
if (cameraController == null) return; await cameraController!.lockCaptureOrientation(
await cameraController?.lockCaptureOrientation(
DeviceOrientation.portraitUp, DeviceOrientation.portraitUp,
); );
if (cameraController == null) return; await cameraController!.setFlashMode(
await cameraController?.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
); );
if (cameraController == null) return; selectedCameraDetails.maxAvailableZoom = await cameraController!
selectedCameraDetails.maxAvailableZoom = .getMaxZoomLevel();
await cameraController?.getMaxZoomLevel() ?? 1; selectedCameraDetails.minAvailableZoom = await cameraController!
selectedCameraDetails.minAvailableZoom = .getMinZoomLevel();
await cameraController?.getMinZoomLevel() ?? 1;
selectedCameraDetails selectedCameraDetails
..isZoomAble = ..isZoomAble =
selectedCameraDetails.maxAvailableZoom != selectedCameraDetails.maxAvailableZoom !=
@ -240,10 +260,16 @@ class MainCameraController {
isSelectingFaceFilters = false; isSelectingFaceFilters = false;
setFilter(FaceFilterType.none); setFilter(FaceFilterType.none);
zoomButtonKey = GlobalKey(); zoomButtonKey = GlobalKey();
initCameraStarted = false;
setState?.call(); setState?.call();
} catch (e) { } catch (e) {
Log.error(e); Log.error('Error post-initializing camera: $e');
final controllerToDispose = cameraController;
cameraController = null; cameraController = null;
if (controllerToDispose != null) {
unawaited(controllerToDispose.dispose());
}
initCameraStarted = false;
return; return;
} }
} }
@ -310,7 +336,6 @@ class MainCameraController {
} }
final inputImage = _inputImageFromCameraImage(image); final inputImage = _inputImageFromCameraImage(image);
if (inputImage == null) return; if (inputImage == null) return;
_processBarcode(inputImage);
// check if front camera is selected // check if front camera is selected
if (cameraController?.description.lensDirection == if (cameraController?.description.lensDirection ==
CameraLensDirection.front) { CameraLensDirection.front) {

View file

@ -2,12 +2,18 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class PermissionHandlerView extends StatefulWidget { 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 Function onSuccess;
final bool triggerPermissionRequest;
@override @override
PermissionHandlerViewState createState() => PermissionHandlerViewState(); PermissionHandlerViewState createState() => PermissionHandlerViewState();
@ -17,10 +23,6 @@ Future<bool> checkPermissions() async {
if (!await Permission.camera.isGranted) { if (!await Permission.camera.isGranted) {
return false; return false;
} }
if (!await Permission.microphone.isGranted) {
// microphone is only needed when
return true;
}
return true; return true;
} }
@ -36,6 +38,32 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
_timer = Timer.periodic(const Duration(milliseconds: 500), (timer) async { _timer = Timer.periodic(const Duration(milliseconds: 500), (timer) async {
await _checkAndTriggerSuccess(); 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<void> _requestPermissions() async {
try {
await permissionServices();
if (await checkPermissions()) {
_handleSuccess();
}
} catch (e) {
Log.error(e);
}
} }
@override @override
@ -54,21 +82,32 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
Future<void> _checkAndTriggerSuccess() async { Future<void> _checkAndTriggerSuccess() async {
if (_isSuccessTriggered) return; if (_isSuccessTriggered) return;
final route = ModalRoute.of(context);
if (route != null && !route.isCurrent) {
return;
}
try { try {
if (await checkPermissions()) { if (await checkPermissions()) {
_isSuccessTriggered = true; _handleSuccess();
_timer?.cancel();
// ignore: avoid_dynamic_calls
widget.onSuccess();
} }
} catch (e) { } catch (e) {
Log.error(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<Map<Permission, PermissionStatus>> permissionServices() async { Future<Map<Permission, PermissionStatus>> permissionServices() async {
final statuses = await [ final statuses = await [
Permission.camera, Permission.camera,
Permission.microphone,
Permission.notification, Permission.notification,
].request(); ].request();
@ -100,8 +139,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
try { try {
await permissionServices(); await permissionServices();
if (await checkPermissions()) { if (await checkPermissions()) {
// ignore: avoid_dynamic_calls _handleSuccess();
widget.onSuccess();
} }
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; 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.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/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
@ -19,7 +20,11 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
_mainCameraController.setState = () { _mainCameraController.setState = () {
if (mounted) setState(() {}); if (mounted) setState(() {});
}; };
unawaited(_mainCameraController.selectCamera(0, true)); Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true));
}
});
} }
@override @override
@ -44,10 +49,12 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
onTapDown: _mainCameraController.onTapDown, onTapDown: _mainCameraController.onTapDown,
), ),
), ),
CameraPreviewControllerView( Positioned.fill(
mainController: _mainCameraController, child: CameraPreviewControllerView(
hideControllers: true, mainController: _mainCameraController,
isVisible: true, hideControllers: true,
isVisible: true,
),
), ),
], ],
), ),

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/src/database/twonly.db.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.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/camera_preview_controller_view.dart';
@ -21,7 +22,11 @@ class CameraSendToViewState extends State<CameraSendToView> {
_mainCameraController.setState = () { _mainCameraController.setState = () {
if (mounted) setState(() {}); if (mounted) setState(() {});
}; };
unawaited(_mainCameraController.selectCamera(0, true)); Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true));
}
});
} }
@override @override
@ -46,10 +51,12 @@ class CameraSendToViewState extends State<CameraSendToView> {
onTapDown: _mainCameraController.onTapDown, onTapDown: _mainCameraController.onTapDown,
), ),
), ),
CameraPreviewControllerView( Positioned.fill(
mainController: _mainCameraController, child: CameraPreviewControllerView(
sendToGroup: widget.sendToGroup, mainController: _mainCameraController,
isVisible: true, sendToGroup: widget.sendToGroup,
isVisible: true,
),
), ),
], ],
), ),

View file

@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/routing.provider.dart';
@ -102,8 +104,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
}); });
if (initialPage == 1) { if (initialPage == 1) {
WidgetsBinding.instance.addPostFrameCallback((_) { Permission.camera.isGranted.then((hasPermission) {
if (_isViewActive()) { if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
} }
}); });
@ -219,7 +221,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
if (_offsetRatio < 1 && if (AppState.hasCameraPermissions &&
_offsetRatio < 1 &&
!_mainCameraController.isSharePreviewIsShown && !_mainCameraController.isSharePreviewIsShown &&
_isViewActive() && _isViewActive() &&
_mainCameraController.cameraController == null && _mainCameraController.cameraController == null &&
@ -284,7 +287,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
}); });
} }
if (_mainCameraController.cameraController == null && if (AppState.hasCameraPermissions &&
_mainCameraController.cameraController == null &&
!_mainCameraController.initCameraStarted && !_mainCameraController.initCameraStarted &&
_offsetRatio < 1 && _offsetRatio < 1 &&
_isViewActive()) { _isViewActive()) {

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.3.2+146 version: 0.3.3+147
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0