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 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart';
@ -68,6 +69,14 @@ Future<bool> 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());

View file

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

View file

@ -134,8 +134,14 @@ class MainCameraController {
}
Future<void> selectCamera(int sCameraId, bool init) async {
if (initCameraStarted) return;
initCameraStarted = true;
final sessionId = ++_cameraSessionId;
// Start checking microphone permission concurrently
final micPermissionFuture = Permission.microphone.isGranted;
try {
await _pendingDisposal;
if (sessionId != _cameraSessionId) return;
@ -143,12 +149,18 @@ class MainCameraController {
AppEnvironment.cameras = await availableCameras();
if (sessionId != _cameraSessionId) return;
}
} catch (e) {
Log.error('Error querying available cameras: $e');
initCameraStarted = false;
return;
}
var cameraId = sCameraId;
if (cameraId >= AppEnvironment.cameras.length) {
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) {

View file

@ -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<bool> 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<PermissionHandlerView>
_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<void> _requestPermissions() async {
try {
await permissionServices();
if (await checkPermissions()) {
_handleSuccess();
}
} catch (e) {
Log.error(e);
}
}
@override
@ -54,21 +82,32 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
Future<void> _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<Map<Permission, PermissionStatus>> permissionServices() async {
final statuses = await [
Permission.camera,
Permission.microphone,
Permission.notification,
].request();
@ -100,8 +139,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
try {
await permissionServices();
if (await checkPermissions()) {
// ignore: avoid_dynamic_calls
widget.onSuccess();
_handleSuccess();
}
} catch (e) {
Log.error(e);

View file

@ -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,8 +20,12 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
_mainCameraController.setState = () {
if (mounted) setState(() {});
};
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true));
}
});
}
@override
void dispose() {
@ -44,11 +49,13 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
onTapDown: _mainCameraController.onTapDown,
),
),
CameraPreviewControllerView(
Positioned.fill(
child: CameraPreviewControllerView(
mainController: _mainCameraController,
hideControllers: true,
isVisible: true,
),
),
],
),
);

View file

@ -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,8 +22,12 @@ class CameraSendToViewState extends State<CameraSendToView> {
_mainCameraController.setState = () {
if (mounted) setState(() {});
};
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true));
}
});
}
@override
void dispose() {
@ -46,11 +51,13 @@ class CameraSendToViewState extends State<CameraSendToView> {
onTapDown: _mainCameraController.onTapDown,
),
),
CameraPreviewControllerView(
Positioned.fill(
child: CameraPreviewControllerView(
mainController: _mainCameraController,
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_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<HomeView> 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<HomeView> 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<HomeView> with WidgetsBindingObserver {
});
}
if (_mainCameraController.cameraController == null &&
if (AppState.hasCameraPermissions &&
_mainCameraController.cameraController == null &&
!_mainCameraController.initCameraStarted &&
_offsetRatio < 1 &&
_isViewActive()) {

View file

@ -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