fix camera freeze issue

This commit is contained in:
otsmr 2025-07-17 19:53:37 +02:00
parent 5d4fd66879
commit aeda40d34f
5 changed files with 117 additions and 112 deletions

1
.gitignore vendored
View file

@ -46,3 +46,4 @@ app.*.map.json
/android/app/release /android/app/release
/android/app/.cxx/ /android/app/.cxx/
android/.kotlin/ android/.kotlin/
devtools_options.yaml

View file

@ -1,41 +1,36 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart';
class HomeViewCameraPreview extends StatefulWidget { class HomeViewCameraPreview extends StatelessWidget {
const HomeViewCameraPreview({ const HomeViewCameraPreview({
required this.controller,
required this.screenshotController,
super.key, super.key,
}); });
@override final CameraController? controller;
State<HomeViewCameraPreview> createState() => _HomeViewCameraPreviewState(); final ScreenshotController screenshotController;
}
class _HomeViewCameraPreviewState extends State<HomeViewCameraPreview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (HomeViewState.cameraController == null || if (controller == null || !controller!.value.isInitialized) {
!HomeViewState.cameraController!.value.isInitialized) {
return Container(); return Container();
} }
return Positioned.fill( return Positioned.fill(
child: MediaViewSizing( child: MediaViewSizing(
child: Screenshot( child: Screenshot(
controller: HomeViewState.screenshotController, controller: screenshotController,
child: AspectRatio( child: AspectRatio(
aspectRatio: 9 / 16, aspectRatio: 9 / 16,
child: ClipRect( child: ClipRect(
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: width: controller!.value.previewSize!.height,
HomeViewState.cameraController!.value.previewSize!.height, height: controller!.value.previewSize!.width,
height: child: CameraPreview(controller!),
HomeViewState.cameraController!.value.previewSize!.width,
child: CameraPreview(HomeViewState.cameraController!),
), ),
), ),
), ),
@ -46,37 +41,34 @@ class _HomeViewCameraPreviewState extends State<HomeViewCameraPreview> {
} }
} }
class SendToCameraPreview extends StatefulWidget { class SendToCameraPreview extends StatelessWidget {
const SendToCameraPreview({ const SendToCameraPreview({
required this.cameraController,
required this.screenshotController,
super.key, super.key,
}); });
@override final CameraController? cameraController;
State<SendToCameraPreview> createState() => _SendToCameraPreviewState(); final ScreenshotController screenshotController;
}
class _SendToCameraPreviewState extends State<SendToCameraPreview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (CameraSendToViewState.cameraController == null || if (cameraController == null || !cameraController!.value.isInitialized) {
!CameraSendToViewState.cameraController!.value.isInitialized) {
return Container(); return Container();
} }
return Positioned.fill( return Positioned.fill(
child: MediaViewSizing( child: MediaViewSizing(
child: Screenshot( child: Screenshot(
controller: CameraSendToViewState.screenshotController, controller: screenshotController,
child: AspectRatio( child: AspectRatio(
aspectRatio: 9 / 16, aspectRatio: 9 / 16,
child: ClipRect( child: ClipRect(
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: CameraSendToViewState width: cameraController!.value.previewSize!.height,
.cameraController!.value.previewSize!.height, height: cameraController!.value.previewSize!.width,
height: CameraSendToViewState child: CameraPreview(cameraController!),
.cameraController!.value.previewSize!.width,
child: CameraPreview(CameraSendToViewState.cameraController!),
), ),
), ),
), ),

View file

@ -17,7 +17,6 @@ import 'package:twonly/src/views/camera/camera_preview_components/permissions_vi
import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart';
import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart'; import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart';
import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
@ -83,23 +82,21 @@ class SelectedCameraDetails {
bool cameraLoaded = false; bool cameraLoaded = false;
} }
class CameraPreviewControllerView extends StatefulWidget { class CameraPreviewControllerView extends StatelessWidget {
const CameraPreviewControllerView({ const CameraPreviewControllerView({
required this.cameraController,
required this.selectCamera, required this.selectCamera,
required this.isHomeView, required this.selectedCameraDetails,
required this.screenshotController,
super.key, super.key,
this.sendTo, this.sendTo,
}); });
final Contact? sendTo; final Contact? sendTo;
final void Function(int sCameraId, bool init, bool enableAudio) selectCamera; final void Function(int sCameraId, bool init, bool enableAudio) selectCamera;
final bool isHomeView; final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController;
@override
State<CameraPreviewControllerView> createState() =>
_CameraPreviewControllerView();
}
class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return FutureBuilder(
@ -108,13 +105,16 @@ class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
if (snap.hasData) { if (snap.hasData) {
if (snap.data!) { if (snap.data!) {
return CameraPreviewView( return CameraPreviewView(
sendTo: widget.sendTo, sendTo: sendTo,
selectCamera: widget.selectCamera, selectCamera: selectCamera,
isHomeView: widget.isHomeView, cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails,
screenshotController: screenshotController,
); );
} else { } else {
return PermissionHandlerView(onSuccess: () { return PermissionHandlerView(onSuccess: () {
setState(() {}); // setState(() {});
selectCamera(0, true, false);
}); });
} }
} else { } else {
@ -126,14 +126,19 @@ class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
} }
class CameraPreviewView extends StatefulWidget { class CameraPreviewView extends StatefulWidget {
const CameraPreviewView( const CameraPreviewView({
{required this.selectCamera, required this.selectCamera,
required this.isHomeView, required this.cameraController,
required this.selectedCameraDetails,
required this.screenshotController,
super.key, super.key,
this.sendTo}); this.sendTo,
});
final Contact? sendTo; final Contact? sendTo;
final bool isHomeView;
final void Function(int sCameraId, bool init, bool enableAudio) selectCamera; final void Function(int sCameraId, bool init, bool enableAudio) selectCamera;
final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController;
@override @override
State<CameraPreviewView> createState() => _CameraPreviewViewState(); State<CameraPreviewView> createState() => _CameraPreviewViewState();
@ -163,18 +168,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
initAsync(); initAsync();
} }
CameraController? get cameraController => widget.isHomeView
? HomeViewState.cameraController
: CameraSendToViewState.cameraController;
SelectedCameraDetails get selectedCameraDetails => widget.isHomeView
? HomeViewState.selectedCameraDetails
: CameraSendToViewState.selectedCameraDetails;
ScreenshotController get screenshotController => widget.isHomeView
? HomeViewState.screenshotController
: CameraSendToViewState.screenshotController;
Future<void> initAsync() async { Future<void> initAsync() async {
hasAudioPermission = await Permission.microphone.isGranted; hasAudioPermission = await Permission.microphone.isGranted;
if (!mounted) return; if (!mounted) return;
@ -200,15 +193,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<void> updateScaleFactor(double newScale) async { Future<void> updateScaleFactor(double newScale) async {
if (selectedCameraDetails.scaleFactor == newScale || if (widget.selectedCameraDetails.scaleFactor == newScale ||
cameraController == null) { widget.cameraController == null) {
return; return;
} }
await cameraController?.setZoomLevel(newScale.clamp( await widget.cameraController?.setZoomLevel(newScale.clamp(
selectedCameraDetails.minAvailableZoom, widget.selectedCameraDetails.minAvailableZoom,
selectedCameraDetails.maxAvailableZoom)); widget.selectedCameraDetails.maxAvailableZoom));
setState(() { setState(() {
selectedCameraDetails.scaleFactor = newScale; widget.selectedCameraDetails.scaleFactor = newScale;
}); });
} }
@ -240,26 +233,28 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
setState(() { setState(() {
sharePreviewIsShown = true; sharePreviewIsShown = true;
}); });
if (selectedCameraDetails.isFlashOn) { if (widget.selectedCameraDetails.isFlashOn) {
if (isFront) { if (isFront) {
setState(() { setState(() {
showSelfieFlash = true; showSelfieFlash = true;
}); });
} else { } else {
await cameraController?.setFlashMode(FlashMode.torch); await widget.cameraController?.setFlashMode(FlashMode.torch);
} }
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
} }
await cameraController?.pausePreview(); await widget.cameraController?.pausePreview();
if (!mounted) return; if (!mounted) return;
await cameraController?.setFlashMode( await widget.cameraController?.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off); widget.selectedCameraDetails.isFlashOn
? FlashMode.always
: FlashMode.off);
if (!mounted) return; if (!mounted) return;
imageBytes = screenshotController.capture( imageBytes = widget.screenshotController
pixelRatio: MediaQuery.of(context).devicePixelRatio); .capture(pixelRatio: MediaQuery.of(context).devicePixelRatio);
if (await pushMediaEditor(imageBytes, null)) { if (await pushMediaEditor(imageBytes, null)) {
return; return;
@ -305,26 +300,30 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
return true; return true;
} }
widget.selectCamera(selectedCameraDetails.cameraId, false, false); widget.selectCamera(widget.selectedCameraDetails.cameraId, false, false);
return false; return false;
} }
bool get isFront => bool get isFront =>
cameraController?.description.lensDirection == CameraLensDirection.front; widget.cameraController?.description.lensDirection ==
CameraLensDirection.front;
Future<void> onPanUpdate(dynamic details) async { Future<void> onPanUpdate(dynamic details) async {
if (isFront || details == null) { if (isFront || details == null) {
return; return;
} }
if (cameraController == null) return; if (widget.cameraController == null ||
if (!cameraController!.value.isInitialized) return; !widget.cameraController!.value.isInitialized) {
return;
}
selectedCameraDetails.scaleFactor = widget.selectedCameraDetails.scaleFactor =
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
(baseScaleFactor + (basePanY - (details.localPosition.dy as int)) / 30) (baseScaleFactor + (basePanY - (details.localPosition.dy as int)) / 30)
.clamp(1, selectedCameraDetails.maxAvailableZoom); .clamp(1, widget.selectedCameraDetails.maxAvailableZoom);
await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor); await widget.cameraController!
.setZoomLevel(widget.selectedCameraDetails.scaleFactor);
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
@ -353,12 +352,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<void> startVideoRecording() async { Future<void> startVideoRecording() async {
if (cameraController != null && cameraController!.value.isRecordingVideo) { if (widget.cameraController != null &&
widget.cameraController!.value.isRecordingVideo) {
return; return;
} }
if (hasAudioPermission && videoWithAudio) { if (hasAudioPermission && videoWithAudio) {
widget.selectCamera( widget.selectCamera(
selectedCameraDetails.cameraId, widget.selectedCameraDetails.cameraId,
false, false,
await Permission.microphone.isGranted && videoWithAudio, await Permission.microphone.isGranted && videoWithAudio,
); );
@ -369,7 +369,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
try { try {
await cameraController?.startVideoRecording(); await widget.cameraController?.startVideoRecording();
videoRecordingTimer = videoRecordingTimer =
Timer.periodic(const Duration(milliseconds: 15), (timer) { Timer.periodic(const Duration(milliseconds: 15), (timer) {
setState(() { setState(() {
@ -401,7 +401,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
videoRecordingTimer?.cancel(); videoRecordingTimer?.cancel();
videoRecordingTimer = null; videoRecordingTimer = null;
} }
if (cameraController == null || !cameraController!.value.isRecordingVideo) { if (widget.cameraController == null ||
!widget.cameraController!.value.isRecordingVideo) {
return; return;
} }
@ -412,7 +413,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
sharePreviewIsShown = true; sharePreviewIsShown = true;
}); });
File? videoPathFile; File? videoPathFile;
final videoPath = await cameraController?.stopVideoRecording(); final videoPath = await widget.cameraController?.stopVideoRecording();
if (videoPath != null) { if (videoPath != null) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// see https://github.com/flutter/flutter/issues/148335 // see https://github.com/flutter/flutter/issues/148335
@ -422,7 +423,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
videoPathFile = File(videoPath.path); videoPathFile = File(videoPath.path);
} }
} }
await cameraController?.pausePreview(); await widget.cameraController?.pausePreview();
if (await pushMediaEditor(null, videoPathFile)) { if (await pushMediaEditor(null, videoPathFile)) {
return; return;
} }
@ -450,8 +451,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (selectedCameraDetails.cameraId >= gCameras.length || if (widget.selectedCameraDetails.cameraId >= gCameras.length ||
cameraController == null) { widget.cameraController == null) {
return Container(); return Container();
} }
return MediaViewSizing( return MediaViewSizing(
@ -462,14 +463,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
setState(() { setState(() {
basePanY = details.localPosition.dy; basePanY = details.localPosition.dy;
baseScaleFactor = selectedCameraDetails.scaleFactor; baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
}); });
}, },
onLongPressMoveUpdate: onPanUpdate, onLongPressMoveUpdate: onPanUpdate,
onLongPressStart: (details) { onLongPressStart: (details) {
setState(() { setState(() {
basePanY = details.localPosition.dy; basePanY = details.localPosition.dy;
baseScaleFactor = selectedCameraDetails.scaleFactor; baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
}); });
// Get the position of the pointer // Get the position of the pointer
final renderBox = final renderBox =
@ -523,28 +524,28 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
tooltipText: context.lang.switchFrontAndBackCamera, tooltipText: context.lang.switchFrontAndBackCamera,
onPressed: () async { onPressed: () async {
widget.selectCamera( widget.selectCamera(
(selectedCameraDetails.cameraId + 1) % 2, (widget.selectedCameraDetails.cameraId + 1) % 2,
false, false,
false); false);
}, },
), ),
ActionButton( ActionButton(
selectedCameraDetails.isFlashOn widget.selectedCameraDetails.isFlashOn
? Icons.flash_on_rounded ? Icons.flash_on_rounded
: Icons.flash_off_rounded, : Icons.flash_off_rounded,
tooltipText: context.lang.toggleFlashLight, tooltipText: context.lang.toggleFlashLight,
color: selectedCameraDetails.isFlashOn color: widget.selectedCameraDetails.isFlashOn
? Colors.white ? Colors.white
: Colors.white.withAlpha(160), : Colors.white.withAlpha(160),
onPressed: () async { onPressed: () async {
if (selectedCameraDetails.isFlashOn) { if (widget.selectedCameraDetails.isFlashOn) {
await cameraController await widget.cameraController
?.setFlashMode(FlashMode.off); ?.setFlashMode(FlashMode.off);
selectedCameraDetails.isFlashOn = false; widget.selectedCameraDetails.isFlashOn = false;
} else { } else {
await cameraController await widget.cameraController
?.setFlashMode(FlashMode.always); ?.setFlashMode(FlashMode.always);
selectedCameraDetails.isFlashOn = true; widget.selectedCameraDetails.isFlashOn = true;
} }
setState(() {}); setState(() {});
}, },
@ -602,17 +603,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Column( child: Column(
children: [ children: [
if (cameraController!.value.isInitialized && if (widget.cameraController!.value.isInitialized &&
selectedCameraDetails.isZoomAble && widget.selectedCameraDetails.isZoomAble &&
!isFront && !isFront &&
!isVideoRecording) !isVideoRecording)
SizedBox( SizedBox(
width: 120, width: 120,
child: CameraZoomButtons( child: CameraZoomButtons(
key: widget.key, key: widget.key,
scaleFactor: selectedCameraDetails.scaleFactor, scaleFactor:
widget.selectedCameraDetails.scaleFactor,
updateScaleFactor: updateScaleFactor, updateScaleFactor: updateScaleFactor,
controller: cameraController!, controller: widget.cameraController!,
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),

View file

@ -13,9 +13,9 @@ class CameraSendToView extends StatefulWidget {
} }
class CameraSendToViewState extends State<CameraSendToView> { class CameraSendToViewState extends State<CameraSendToView> {
static CameraController? cameraController; CameraController? cameraController;
static ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
static SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
@override @override
void initState() { void initState() {
@ -54,11 +54,16 @@ class CameraSendToViewState extends State<CameraSendToView> {
onDoubleTap: toggleSelectedCamera, onDoubleTap: toggleSelectedCamera,
child: Stack( child: Stack(
children: [ children: [
const SendToCameraPreview(), SendToCameraPreview(
cameraController: cameraController,
screenshotController: screenshotController,
),
CameraPreviewControllerView( CameraPreviewControllerView(
selectCamera: selectCamera, selectCamera: selectCamera,
sendTo: widget.sendTo, sendTo: widget.sendTo,
isHomeView: false, cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails,
screenshotController: screenshotController,
), ),
], ],
), ),

View file

@ -55,9 +55,9 @@ class HomeViewState extends State<HomeView> {
Timer? disableCameraTimer; Timer? disableCameraTimer;
bool initCameraStarted = true; bool initCameraStarted = true;
static CameraController? cameraController; CameraController? cameraController;
static ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
static SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
bool onPageView(ScrollNotification notification) { bool onPageView(ScrollNotification notification) {
disableCameraTimer?.cancel(); disableCameraTimer?.cancel();
@ -145,7 +145,10 @@ class HomeViewState extends State<HomeView> {
onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
const HomeViewCameraPreview(), HomeViewCameraPreview(
controller: cameraController,
screenshotController: screenshotController,
),
Shade( Shade(
opacity: offsetRatio, opacity: offsetRatio,
), ),
@ -177,8 +180,10 @@ class HomeViewState extends State<HomeView> {
child: Opacity( child: Opacity(
opacity: 1 - (offsetRatio * 4) % 1, opacity: 1 - (offsetRatio * 4) % 1,
child: CameraPreviewControllerView( child: CameraPreviewControllerView(
cameraController: cameraController,
screenshotController: screenshotController,
selectedCameraDetails: selectedCameraDetails,
selectCamera: selectCamera, selectCamera: selectCamera,
isHomeView: true,
), ),
)), )),
], ],