mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-25 00:24:07 +00:00
fix camera initalization issue
This commit is contained in:
parent
c4fc14a909
commit
f14a94d639
8 changed files with 148 additions and 57 deletions
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class _CameraPreviewControllerViewState
|
|||
);
|
||||
} else {
|
||||
return PermissionHandlerView(
|
||||
triggerPermissionRequest: widget.isVisible,
|
||||
onSuccess: () {
|
||||
setState(() {
|
||||
AppState.hasCameraPermissions = true;
|
||||
|
|
|
|||
|
|
@ -134,14 +134,25 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
Future<void> 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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<QrCodeScannerView> {
|
|||
_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<QrCodeScannerView> {
|
|||
onTapDown: _mainCameraController.onTapDown,
|
||||
),
|
||||
),
|
||||
CameraPreviewControllerView(
|
||||
mainController: _mainCameraController,
|
||||
hideControllers: true,
|
||||
isVisible: true,
|
||||
Positioned.fill(
|
||||
child: CameraPreviewControllerView(
|
||||
mainController: _mainCameraController,
|
||||
hideControllers: true,
|
||||
isVisible: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<CameraSendToView> {
|
|||
_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<CameraSendToView> {
|
|||
onTapDown: _mainCameraController.onTapDown,
|
||||
),
|
||||
),
|
||||
CameraPreviewControllerView(
|
||||
mainController: _mainCameraController,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
isVisible: true,
|
||||
Positioned.fill(
|
||||
child: CameraPreviewControllerView(
|
||||
mainController: _mainCameraController,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
isVisible: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue