diff --git a/.kilo/plans/1784212500972-vision-camera-skia-frame-processor.md b/.kilo/plans/1784212500972-vision-camera-skia-frame-processor.md new file mode 100644 index 0000000..6f9843f --- /dev/null +++ b/.kilo/plans/1784212500972-vision-camera-skia-frame-processor.md @@ -0,0 +1,106 @@ +# Kế hoạch: Real-time Skia filter trên camera live view dùng vision-camera v4 + +## Vấn đề +`expo-camera` render camera preview lên native surface (PreviewView/SurfaceView) không thể intercept bằng Skia. Periodic capture bị từ chối vì mất live view. + +## Giải pháp +Dùng `react-native-vision-camera@4.7.3` với `useSkiaFrameProcessor` — frame processor chạy native thread, mỗi frame camera được vẽ qua Skia với ColorMatrix trước khi hiển thị. + +## Các bước thực hiện + +### Bước 1: Cài react-native-vision-camera@4.7.3 +```bash +npm install react-native-vision-camera@4.7.3 +``` +- v4.7.3 là latest v4, có export `useSkiaFrameProcessor` và `frameProcessor` prop trên `Camera` +- v5+ đã xoá Skia integration, không dùng được + +### Bước 2: Thêm Expo Config Plugin +Sửa `app.json`: +- Thay `expo-camera` plugin bằng `react-native-vision-camera` plugin (v4 có plugin riêng) +- Thêm `withEnableFrameProcessorsAndroid` để bật frame processor trên Android +```json +"plugins": [ + "react-native-vision-camera", + [ + "expo-camera", + { + "cameraPermission": "..." + } + ] +] +``` +Cần kiểm tra: v4 có expo plugin riêng (`withVisionCamera`) hay dùng thẳng tên package. + +### Bước 3: Cập nhật App.tsx +- Import `useCameraPermission` từ `react-native-vision-camera` thay vì `useCameraPermissions` từ `expo-camera` +- Props: `hasPermission`, `requestPermission` + +### Bước 4: Viết lại Viewfinder.tsx +Component mới dùng: +- `Camera` từ vision-camera với prop `frameProcessor={frameProcessor}` +- `useSkiaFrameProcessor` hook — worklet chạy native, mỗi frame: + 1. Tạo `Skia.Paint()` với `Skia.ColorFilter.MakeMatrix(colorMatrix)` + 2. Gọi `frame.render(paint)` — vẽ frame qua color filter +- `colorMatrix` là `SharedValue` từ react-native-reanimated (để worklet đọc được) +- Grain noise, frame border, GPS watermark: giữ nguyên overlay React Native / Skia Canvas trên top + +```tsx +const App = () => { + const frameProcessor = useSkiaFrameProcessor((frame) => { + 'worklet'; + const paint = Skia.Paint(); + paint.setColorFilter(Skia.ColorFilter.MakeMatrix(colorMatrixSV.value)); + frame.render(paint); + }, [colorMatrixSV]); + + return ( + + ); +}; +``` + +### Bước 5: Loại bỏ expo-camera khỏi code +- `CameraView` → `Camera` (vision-camera) +- Xoá imports không dùng +- Giữ `expo-camera` trong package.json (không gỡ, tránh lỗi autolinking) + +### Bước 6: Re-prebuild & build APK +```bash +rm -rf android +npx expo prebuild --platform android +echo "sdk.dir=/home/locpham/android-sdk" > android/local.properties +cd android && ./gradlew assembleDebug +``` +(Không chạy `clean` — tránh lỗi CMake codegen paths) + +## API đã xác nhận (v4.7.3) +| Export | Path | +|---|---| +| `Camera` | `react-native-vision-camera` | +| `useCameraDevice` | `react-native-vision-camera` | +| `useCameraPermission` | `react-native-vision-camera` | +| `useSkiaFrameProcessor` | `react-native-vision-camera` | +| `SkiaCameraCanvas` | `react-native-vision-camera` | +| Expo plugin (`withVisionCamera`) | `lib/module/expo-plugin/withVisionCamera` | + +## Rủi ro & Xử lý +| Rủi ro | Xử lý | +|---|---| +| `react-native-worklets-core` conflict | Đã có sẵn qua `react-native-reanimated` dependency | +| CameraX dependency (`NoClassDefFoundError`) | v4 dùng `CameraX` không phải Nitro — build an toàn hơn v5 | +| CMake codegen paths lỗi sau `clean` | Không dùng `./gradlew clean`, dùng `--rerun-tasks` nếu cần | +| Frame processor không chạy trên Android | Bật flag qua Expo plugin: `withEnableFrameProcessorsAndroid` | + +## Kiểm thử +1. Build APK thành công +2. Camera live view hiển thị với filter: + - B&W/monochrome → true grayscale + - Saturation, temp, tint, exposure thay đổi real-time +3. Grain noise, frame border, GPS watermark vẫn hoạt động +4. Library mode không bị ảnh hưởng diff --git a/CAMERA_PORT_ANDROID.md b/CAMERA_PORT_ANDROID.md new file mode 100644 index 0000000..d3c717b --- /dev/null +++ b/CAMERA_PORT_ANDROID.md @@ -0,0 +1,258 @@ +# 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` → `