From 35151bb325b28fc2c234ddaa26c505efc52f75bd Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 13 May 2026 17:12:07 +0300 Subject: [PATCH] camera: mirror preview only, not capture (telegram fidelity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop isVideoMirrored on the AVCaptureVideoDataOutput connection — the data output feeds both the preview texture AND the recorder, so any mirror set there ended up baked into the recorded MP4. Recorded video + captured JPEG now carry the raw sensor feed ("as others see you"), matching telegram-iOS and the stock iOS Camera app default. The selfie preview is mirrored inside UxCameraPreview itself (Transform.flip(flipX: true) around the Texture when description.lens == front) — the analog of telegram's CameraPreviewView.mirroring CALayer transform. Consumers (CameraThumb, etc.) don't need to know which lens is active. --- ios/Classes/Camera/CameraInstance.swift | 12 ++++++++++-- lib/src/camera/camera_preview.dart | 17 +++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ios/Classes/Camera/CameraInstance.swift b/ios/Classes/Camera/CameraInstance.swift index 673e852..70d1039 100644 --- a/ios/Classes/Camera/CameraInstance.swift +++ b/ios/Classes/Camera/CameraInstance.swift @@ -411,7 +411,15 @@ final class CameraInstance { } } - // Apply preview-output settings on the (new) connection. + // Apply preview-output orientation. Mirroring is deliberately + // NOT set here — the data output feeds both the preview + // texture and the recorder, so mirroring at the connection + // would land in the recorded MP4 too. Telegram avoids this + // by mirroring at the preview-LAYER level (CALayer transform + // in `CameraPreviewView.mirroring`). Our FlutterTexture + // equivalent is a `Transform.flip` in [CameraThumb] for the + // front camera — raw sensor feed at capture, mirror as a + // playback decision. if let videoConn = videoDataOutput?.connection(with: .video) { if videoConn.isVideoOrientationSupported { videoConn.videoOrientation = lockedOrientation?.avVideoOrientation @@ -419,7 +427,7 @@ final class CameraInstance { } if videoConn.isVideoMirroringSupported { videoConn.automaticallyAdjustsVideoMirroring = false - videoConn.isVideoMirrored = (device.position == .front) + videoConn.isVideoMirrored = false } } diff --git a/lib/src/camera/camera_preview.dart b/lib/src/camera/camera_preview.dart index 66b69d9..3252fdd 100644 --- a/lib/src/camera/camera_preview.dart +++ b/lib/src/camera/camera_preview.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import 'camera.dart' show UxCameraController, UxCameraValue; +import 'camera.dart' show UxCameraController, UxCameraLens, UxCameraValue; /// Renders the live preview for [controller] into a [Texture]. Sizes /// itself to the parent — wrap in `AspectRatio` / `FittedBox` / `Hero` @@ -10,6 +10,11 @@ import 'camera.dart' show UxCameraController, UxCameraValue; /// transparent placeholder. The widget rebuilds on every /// `UxCameraValue` change, so once the native session starts /// producing frames the texture appears automatically. +/// +/// Front-camera preview is auto-mirrored here (the analog of +/// telegram-iOS's `CameraPreviewView.mirroring` property), so the +/// recorded MP4 + captured JPEG carry the raw sensor feed while the +/// on-screen preview still reads as a natural mirror to the user. class UxCameraPreview extends StatelessWidget { const UxCameraPreview({super.key, required this.controller}); @@ -19,10 +24,14 @@ class UxCameraPreview extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller, - builder: (context, _, __) { + builder: (context, value, _) { final id = controller.textureId; - if (id == null) return const SizedBox.expand(); - return Texture(textureId: id); + if (id == null) return SizedBox.expand(); + final mirror = value.description.lens == UxCameraLens.front; + final texture = Texture(textureId: id); + return mirror + ? Transform.flip(flipX: true, child: texture) + : texture; }, ); }