# Camera trực tiếp: chỉ port những gì web có mà Android chưa có Phạm vi tài liệu: **chỉ liệt kê delta web-only**. Mọi thứ Android đã có thì ghi nhận một dòng rồi bỏ qua, không mô tả cách làm lại. - Web (tham chiếu): `docker/frontend/` — Vite/React/TS, CanvasKit WASM. Camera thêm ở `cb1839e` + `6814b05`. - Android (đích): `App.tsx` + `src/` ở gốc repo — Expo/RN, `react-native-vision-camera-skia` (worklet + GPU texture). Quy ước trích dẫn: cột **Web** trỏ vào file trong `docker/frontend/`; cột **Android** trỏ vào `App.tsx` / `src/` ở gốc repo. **Tóm tắt:** web-only đáng port chỉ có **3** việc — P1 (quan trắc FPS/độ phân giải), P2 (gate PRO ở lối vào camera, *cần quyết định*), P3 (luồng "chụp rồi chỉnh"). Web-only còn lại hoặc không port được, hoặc Android đã có bản tương đương. §6 ghi thêm phương án thay thế: đóng gói chính web studio thành app mobile. --- ## 1. Đối chiếu tính năng — cái nào thực sự web-only | | **Web** | **Android** | Delta | |---|---|---|---| | Lối vào | Nút `OPEN CAMERA` + badge `PRO` (`ui/ImageStage.tsx:1443-1444`, `:2002-2006`) | Chế độ `mode === 'camera'` (`App.tsx:1675`, `:1978`) | khác chỗ đặt, không phải thiếu | | Gate | **Khoá cả camera** sau PRO: `cameraSupported() ? () => (pro ? setShooting(true) : promptPro()) : undefined` (`App.tsx:3237-3241`), `pro = !!user?.verified` (`:522`) | Gate ở **lúc render look**: `proLookInUse` (`App.tsx:1269`), `promptPro` (`:1275-1285`). `CameraControls.tsx` không có `pro` nào | **WEB-ONLY → P2** | | Số đo trên viewfinder | `{w}×{h} · {fps} fps` (`ui/CameraView.tsx:214-218`, `i18n/en.ts:40`) | Không hiển thị gì (nhịp tim `frameBeatSync` chỉ phục vụ watchdog) | **WEB-ONLY → P1** | | Ảnh chụp đi đâu | Still **full cảm biến → studio**; look do studio áp, file ghi sau bằng export chung (`docker/frontend/src/App.tsx:902-918`) | Render look **ngay** → file + gallery + `pushPhoto`; **không mở editor** (`App.tsx:1476-1489`) | **WEB-ONLY → P3** | | Xem trước khi chỉnh | Cùng `renderPhoto`, khác đích ra (`drawTo` so với `encodeToBytes`) | Cùng SkSL, chạy trong worklet | tương đương | | Lens | Chỉ `facingMode user/environment` (`engine/camera.ts:12-14`) | Flip, zoom 1–10x, ultra-wide 0.5x qua module Camera2 ẩn (`Viewfinder.tsx:599-713`), RAW DNG (`App.tsx:337-340`) | Android hơn | | Panel khi đang ngắm | Rail + cột PRESETS vẫn cạnh viewfinder (`.stage-slot`, `styles/app.css:723-750`) | Overlay dp + panel ngay dưới khung (`Viewfinder.tsx:2774-2819`) | tương đương | | Chuỗi UI | i18n EN/VI, 9 khoá `cam.*` (`i18n/en.ts:36-44`, `i18n/vi.ts:45-53`) | Hardcode tiếng Anh (app không có hạ tầng i18n) | khác phạm vi, xem P6 | | Chống treo phiên | không cần (rAF tự dừng/tiếp) | Watchdog remount sau ~6 s không có khung (`Viewfinder.tsx:1846-1900`) | Android hơn | --- ## 2. Delta kỹ thuật theo lớp | Lớp | Web | Android | Kết luận | |---|---|---|---| | Nhận biết khả năng | `navigator.mediaDevices?.getUserMedia` + yêu cầu **secure context** (`engine/camera.ts:27-32`) | `useCameraPermission()` (`App.tsx:322`), manifest `CAMERA` | web-only, **không port** (không có khái niệm tương ứng) | | Phân loại lỗi | `err.name === 'NotAllowedError'` → `cam.denied`, còn lại `cam.fail` (`CameraView.tsx:176-181`) | 3 alert riêng: `Permission Needed` / `Camera Not Ready` / `Capture Failed` (`App.tsx:1338`, `:1348`, `:1499`) | ngữ nghĩa đã tương đương | | Mở phiên | `getUserMedia` → `