diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index 644a359..c710607 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -155,6 +155,35 @@ export default function Viewfinder({ // enough to pick the device up — no manual polling needed. const device = useCameraDevice('back'); + // CameraX negotiates the CameraFrameOutput aspect on its own (default 16:9 on + // this device), which center-crops the 4:3 sensor — so the ratio band could + // never display 100% of the sensor pixels. Force the frame stream to a 4:3 + // (sensor aspect) resolution supported for 'stream' outputs; the photo output + // keeps its own full-res 4:3 size and FOV, so the viewfinder then matches the + // captured photo exactly. Best supported 4:3 size closest to 1280x960 + // (720p-class, keeps the frame pipeline light); CameraX may still round it. + const frameTarget = useMemo(() => { + if (!device) return undefined; + try { + let best: { width: number; height: number } | undefined; + let bestScore = Infinity; + for (const s of device.getSupportedResolutions('stream')) { + const w = s.width; + const h = s.height; + if (!(w > 0 && h > 0)) continue; + if (Math.abs(w / h - 4 / 3) > 0.03) continue; + const score = Math.abs(Math.sqrt(w * h) - Math.sqrt(1280 * 960)); + if (score < bestScore) { + bestScore = score; + best = { width: w, height: h }; + } + } + return best ?? undefined; + } catch { + return undefined; + } + }, [device]); + const skiaImage = useImage(libraryImageUri || ''); // Wall-frame PNG artwork (project root). Loaded lazily so other frames pay // no asset cost; source switches to '' when unselected (useImage → null). @@ -629,7 +658,8 @@ export default function Viewfinder({ ' ratio=' + aspectRatio + ' frame=' + selectedFrame + ' view=' + vw + 'x' + vh + - ' feed=' + (cameraPhotoRect ? JSON.stringify([1, vw / vh, cameraPhotoRect.x / vw, cameraPhotoRect.y / vh, cameraPhotoRect.w / vw, cameraPhotoRect.h / vh]) : 'null') + ' feed=' + (cameraPhotoRect ? JSON.stringify([1, vw / vh, cameraPhotoRect.x / vw, cameraPhotoRect.y / vh, cameraPhotoRect.w / vw, cameraPhotoRect.h / vh]) : 'null') + + ' frameTarget=' + (frameTarget ? frameTarget.width + 'x' + frameTarget.height : 'null') ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [region, regionBand, aspectRatio, selectedFrame, cameraPhotoRect]); @@ -956,6 +986,7 @@ export default function Viewfinder({ isActive={appActive} outputs={cameraOutputs} pixelFormat="yuv" + targetResolution={frameTarget} onStarted={() => setCameraActive(true)} onFrame={handleFrame} onError={onSessionError}