improving camera

This commit is contained in:
otsmr 2026-01-18 15:23:52 +01:00
parent d83e9a26c4
commit cd5deca6b6
7 changed files with 114 additions and 84 deletions

View file

@ -1,8 +1,10 @@
# Changelog # Changelog
## 0.0.87 ## 0.0.89
- Added basic support for face filters - Adds option to manual focus in the camera
- Adds support to switch between front and back camera during video recording
- Adds basic face filters
## 0.0.86 ## 0.0.86

View file

@ -36,6 +36,7 @@ class MainCameraPreview extends StatelessWidget {
height: mainCameraController height: mainCameraController
.cameraController!.value.previewSize!.width, .cameraController!.value.previewSize!.width,
child: CameraPreview( child: CameraPreview(
key: mainCameraController.cameraPreviewKey,
mainCameraController.cameraController!, mainCameraController.cameraController!,
child: Stack( child: Stack(
children: [ children: [
@ -47,6 +48,24 @@ class MainCameraPreview extends StatelessWidget {
Positioned.fill( Positioned.fill(
child: mainCameraController.facePaint!, child: mainCameraController.facePaint!,
), ),
if (mainCameraController.focusPointOffset != null)
Positioned(
top: mainCameraController.focusPointOffset!.dy - 40,
left:
mainCameraController.focusPointOffset!.dx - 40,
child: Container(
height: 80,
width: 80,
clipBehavior: Clip.antiAliasWithSaveLayer,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 1,
color: Colors.white.withAlpha(150),
),
),
),
)
], ],
), ),
), ),

View file

@ -37,55 +37,6 @@ import 'package:url_launcher/url_launcher_string.dart';
int maxVideoRecordingTime = 60; int maxVideoRecordingTime = 60;
Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
SelectedCameraDetails details,
int sCameraId,
bool init,
) async {
var cameraId = sCameraId;
if (cameraId >= gCameras.length) return null;
if (init) {
for (; cameraId < gCameras.length; cameraId++) {
if (gCameras[cameraId].lensDirection == CameraLensDirection.back) {
break;
}
}
}
details.isZoomAble = false;
if (details.cameraId != cameraId) {
// switch between front and back
details.scaleFactor = 1;
}
final cameraController = CameraController(
gCameras[cameraId],
ResolutionPreset.high,
enableAudio: await Permission.microphone.isGranted,
imageFormatGroup:
Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,
);
await cameraController.initialize().then((_) async {
await cameraController.setZoomLevel(details.scaleFactor);
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
await cameraController
.setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off);
await cameraController
.getMaxZoomLevel()
.then((double value) => details.maxAvailableZoom = value);
await cameraController
.getMinZoomLevel()
.then((double value) => details.minAvailableZoom = value);
details
..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom
..cameraLoaded = true
..cameraId = cameraId;
}).catchError((Object e) {
Log.error('$e');
});
return (details, cameraController);
}
class SelectedCameraDetails { class SelectedCameraDetails {
double maxAvailableZoom = 1; double maxAvailableZoom = 1;
double minAvailableZoom = 1; double minAvailableZoom = 1;

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -50,6 +52,7 @@ class MainCameraController {
Map<int, ScannedNewProfile> scannedNewProfiles = {}; Map<int, ScannedNewProfile> scannedNewProfiles = {};
String? scannedUrl; String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey(); GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey();
bool isSelectingFaceFilters = false; bool isSelectingFaceFilters = false;
final BarcodeScanner _barcodeScanner = BarcodeScanner(); final BarcodeScanner _barcodeScanner = BarcodeScanner();
@ -63,6 +66,7 @@ class MainCameraController {
bool _isBusyFaces = false; bool _isBusyFaces = false;
CustomPaint? customPaint; CustomPaint? customPaint;
CustomPaint? facePaint; CustomPaint? facePaint;
Offset? focusPointOffset;
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
FaceFilterType get currentFilterType => _currentFilterType; FaceFilterType get currentFilterType => _currentFilterType;
@ -83,44 +87,96 @@ class MainCameraController {
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
} }
Future<CameraController?> selectCamera(int sCameraId, bool init) async { Future<void> selectCamera(int sCameraId, bool init) async {
initCameraStarted = true; initCameraStarted = true;
final opts = await initializeCameraController(
selectedCameraDetails,
sCameraId,
init,
);
if (opts != null) {
selectedCameraDetails = opts.$1;
cameraController = opts.$2;
}
isSelectingFaceFilters = false;
setFilter(FaceFilterType.none);
await cameraController?.startImageStream(_processCameraImage);
zoomButtonKey = GlobalKey();
setState();
return cameraController;
}
Future<void> toggleSelectedCamera() async { var cameraId = sCameraId;
if (cameraController == null) return; if (cameraId >= gCameras.length) {
// do not allow switching camera when recording Log.warn(
if (cameraController!.value.isRecordingVideo) { 'Trying to select a non existing camera $cameraId >= ${gCameras.length}',
);
return; return;
} }
try {
await cameraController!.stopImageStream(); if (init) {
} catch (e) { for (; cameraId < gCameras.length; cameraId++) {
// Log.warn(e); if (gCameras[cameraId].lensDirection == CameraLensDirection.back) {
break;
}
}
} }
final tmp = cameraController;
cameraController = null; selectedCameraDetails.isZoomAble = false;
if (selectedCameraDetails.cameraId != cameraId) {
// switched camera so reset the scaleFactor
selectedCameraDetails.scaleFactor = 1;
}
if (cameraController == null) {
cameraController = CameraController(
gCameras[cameraId],
ResolutionPreset.high,
enableAudio: await Permission.microphone.isGranted,
imageFormatGroup: Platform.isAndroid
? ImageFormatGroup.nv21
: ImageFormatGroup.bgra8888,
);
await cameraController?.initialize();
await cameraController?.startImageStream(_processCameraImage);
} else {
await HapticFeedback.lightImpact();
await cameraController?.setDescription(gCameras[cameraId]);
}
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
await cameraController
?.lockCaptureOrientation(DeviceOrientation.portraitUp);
await cameraController?.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
);
selectedCameraDetails.maxAvailableZoom =
await cameraController?.getMaxZoomLevel() ?? 1;
selectedCameraDetails.minAvailableZoom =
await cameraController?.getMinZoomLevel() ?? 1;
selectedCameraDetails
..isZoomAble = selectedCameraDetails.maxAvailableZoom !=
selectedCameraDetails.minAvailableZoom
..cameraLoaded = true
..cameraId = cameraId;
facePaint = null; facePaint = null;
customPaint = null; customPaint = null;
await tmp!.dispose(); isSelectingFaceFilters = false;
setFilter(FaceFilterType.none);
zoomButtonKey = GlobalKey();
setState();
}
Future<void> onDoubleTap() async {
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
} }
Future<void> onTapDown(TapDownDetails details) async {
final box =
cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
if (box == null) return;
final localPosition = box.globalToLocal(details.globalPosition);
focusPointOffset = Offset(localPosition.dx, localPosition.dy);
final dx = localPosition.dx / box.size.width;
final dy = localPosition.dy / box.size.height;
setState();
await HapticFeedback.lightImpact();
await cameraController?.setFocusPoint(Offset(dx, dy));
await cameraController?.setFocusMode(FocusMode.auto);
focusPointOffset = null;
setState();
}
void setFilter(FaceFilterType type) { void setFilter(FaceFilterType type) {
_currentFilterType = type; _currentFilterType = type;
if (_currentFilterType == FaceFilterType.none) { if (_currentFilterType == FaceFilterType.none) {

View file

@ -32,7 +32,8 @@ class QrCodeScannerState extends State<QrCodeScanner> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: _mainCameraController.toggleSelectedCamera, onDoubleTap: _mainCameraController.onDoubleTap,
onTapDown: _mainCameraController.onTapDown,
child: Stack( child: Stack(
children: [ children: [
MainCameraPreview( MainCameraPreview(

View file

@ -34,7 +34,8 @@ class CameraSendToViewState extends State<CameraSendToView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: _mainCameraController.toggleSelectedCamera, onDoubleTap: _mainCameraController.onDoubleTap,
onTapDown: _mainCameraController.onTapDown,
child: Stack( child: Stack(
children: [ children: [
MainCameraPreview( MainCameraPreview(

View file

@ -172,9 +172,9 @@ class HomeViewState extends State<HomeView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: offsetRatio == 0 onDoubleTap:
? _mainCameraController.toggleSelectedCamera offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
: null, onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MainCameraPreview(mainCameraController: _mainCameraController), MainCameraPreview(mainCameraController: _mainCameraController),