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

View file

@ -36,6 +36,7 @@ class MainCameraPreview extends StatelessWidget {
height: mainCameraController
.cameraController!.value.previewSize!.width,
child: CameraPreview(
key: mainCameraController.cameraPreviewKey,
mainCameraController.cameraController!,
child: Stack(
children: [
@ -47,6 +48,24 @@ class MainCameraPreview extends StatelessWidget {
Positioned.fill(
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;
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 {
double maxAvailableZoom = 1;
double minAvailableZoom = 1;

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:collection/collection.dart';
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.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:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -50,6 +52,7 @@ class MainCameraController {
Map<int, ScannedNewProfile> scannedNewProfiles = {};
String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey();
bool isSelectingFaceFilters = false;
final BarcodeScanner _barcodeScanner = BarcodeScanner();
@ -63,6 +66,7 @@ class MainCameraController {
bool _isBusyFaces = false;
CustomPaint? customPaint;
CustomPaint? facePaint;
Offset? focusPointOffset;
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
FaceFilterType get currentFilterType => _currentFilterType;
@ -83,44 +87,96 @@ class MainCameraController {
selectedCameraDetails = SelectedCameraDetails();
}
Future<CameraController?> selectCamera(int sCameraId, bool init) async {
Future<void> selectCamera(int sCameraId, bool init) async {
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 {
if (cameraController == null) return;
// do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo) {
var cameraId = sCameraId;
if (cameraId >= gCameras.length) {
Log.warn(
'Trying to select a non existing camera $cameraId >= ${gCameras.length}',
);
return;
}
try {
await cameraController!.stopImageStream();
} catch (e) {
// Log.warn(e);
if (init) {
for (; cameraId < gCameras.length; cameraId++) {
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;
customPaint = null;
await tmp!.dispose();
isSelectingFaceFilters = false;
setFilter(FaceFilterType.none);
zoomButtonKey = GlobalKey();
setState();
}
Future<void> onDoubleTap() async {
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) {
_currentFilterType = type;
if (_currentFilterType == FaceFilterType.none) {

View file

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

View file

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

View file

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