diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index a1c9e4aa0c..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: mrousavy -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: mrousavy -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml deleted file mode 100644 index 34cd358a64..0000000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: 🐛 Bug Report -description: File a bug report -title: "🐛 " -labels: [🐛 bug] -body: - - type: textarea - attributes: - label: What's happening? - description: Explain what you are trying to do and what happened instead. Be as precise as possible, I can't help you if I don't understand your issue. - placeholder: I wanted to take a picture, but the method failed with this error "[capture/photo-not-enabled] Failed to take photo, photo is not enabled!" - validations: - required: true - - type: textarea - attributes: - label: Reproduceable Code - description: > - Share a small reproduceable code snippet here (or the entire file if necessary). - Most importantly, share how you use the `` component and what props you pass to it. - This will be automatically formatted into code, so no need for backticks. - render: tsx - placeholder: > - const device = useCameraDevices() - - // ... - - - validations: - required: true - - type: textarea - attributes: - label: Relevant log output - description: > - Paste any relevant **native log output** (Xcode Logs/Android Studio Logcat) here. - This will be automatically formatted into code, so no need for backticks. - - * For iOS, run the project through Xcode and copy the logs from the log window. - - * For Android, either open the project through Android Studio and paste the logs from the logcat window, or run `adb logcat` in terminal. - render: shell - placeholder: > - 09:03:46 I ReactNativeJS: Running "VisionCameraExample" with {"rootTag":11} - - 09:03:47 I ReactNativeJS: Re-rendering App. Camera: undefined | Microphone: undefined - - 09:03:47 I VisionCamera: Installing JSI bindings... - - 09:03:47 I VisionCamera: Finished installing JSI bindings! - ... - validations: - required: true - - type: textarea - attributes: - label: Camera Device - description: > - Paste the JSON Camera `device` that was used here. - Make sure to leave out the `formats` prop as that is too long for the issue. - - Run this code in your app to get the `device` as a JSON: - - ``` - - console.log(JSON.stringify(device, (k, v) => k === "formats" ? [] : v, 2)) - - ``` - - This will be automatically formatted into code, so no need for backticks. - render: json - placeholder: > - { - "id": "com.apple.avfoundation.avcapturedevice.built-in_video:6", - "devices": [ - "ultra-wide-angle-camera", - "wide-angle-camera" - ], - "formats": null, - "hardwareLevel": "full", - "hasFlash": true, - "hasTorch": true, - "isMultiCam": true, - "minZoom": 1, - "neutralZoom": 2, - "maxZoom": 123.75, - "name": "Back Dual Wide Camera", - "position": "back", - "supportsFocus": true, - "supportsLowLightBoost": false, - "supportsRawCapture": false - } - validations: - required: true - - type: input - attributes: - label: Device - description: > - Which device are you seeing this Problem on? - Mention the full name of the phone, as well as the operating system and version. - If you have tested this on multiple devices (ex. Android and iOS) then mention all of those devices (comma separated) - placeholder: ex. iPhone 11 Pro (iOS 14.3) - validations: - required: true - - type: input - attributes: - label: VisionCamera Version - description: Which version of react-native-vision-camera are you using? - placeholder: ex. 3.1.6 - validations: - required: true - - type: dropdown - attributes: - label: Can you reproduce this issue in the VisionCamera Example app? - description: > - Try to build the example app (`package/example/`) and see if the issue is reproduceable here. - **Note:** If you don't try this in the example app, I most likely won't help you with your issue. - options: - - I didn't try (⚠️ your issue might get ignored & closed if you don't try this) - - Yes, I can reproduce the same issue in the Example app here - - No, I cannot reproduce the issue in the Example app - default: 0 - validations: - required: true - - type: checkboxes - attributes: - label: Additional information - description: Please check all the boxes that apply - options: - - label: I am using Expo - - label: I have enabled Frame Processors (react-native-worklets-core) - - label: I have read the [Troubleshooting Guide](https://react-native-vision-camera.com/docs/guides/troubleshooting) - required: true - - label: I agree to follow this project's [Code of Conduct](https://github.com/mrousavy/react-native-vision-camera/blob/main/CODE_OF_CONDUCT.md) - required: true - - label: I searched for [similar issues in this repository](https://github.com/mrousavy/react-native-vision-camera/issues) and found none. - required: true diff --git a/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml b/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml deleted file mode 100644 index f97039fc52..0000000000 --- a/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: 🔧 Build Error -description: File a build error bug report -title: "🔧 " -labels: [🔧 build error] -body: - - type: textarea - attributes: - label: How were you trying to build the app? - description: Explain how you tried to build the app, through Xcode, `yarn ios`, a CI, or other. Be as precise as possible, I can't help you if I don't understand your issue. - placeholder: I tried to build my app with react-native-vision-camera using the `yarn ios` command, and it failed. - validations: - required: true - - type: textarea - attributes: - label: Full build logs - description: Share the full build logs that appear in the console. Make sure you don't just paste the last few lines here, but rather everything from start to end. - render: tsx - placeholder: > - $ react-native run-android - - > Configure project :react-native-vision-camera - - [VisionCamera] react-native-worklets-core found, Frame Processors enabled! - - ... - validations: - required: true - - type: textarea - attributes: - label: Project dependencies - description: Share all of your project's dependencies including their versions from `package.json`. This is useful if there are any other conflicting libraries. - render: json - placeholder: > - "dependencies": { - "react-native": "^0.73.4", - "react-native-reanimated": "^3.9.0", - "react-native-vision-camera": "^4.0.0", - "react-native-worklets-core": "^1.0.0", - ... - }, - validations: - required: true - - type: input - attributes: - label: VisionCamera Version - description: Which version of react-native-vision-camera are you using? - placeholder: ex. 3.1.6 - validations: - required: true - - type: dropdown - attributes: - label: Target platforms - description: Select the platforms where the build error occurs. - multiple: true - options: - - iOS - - Android - validations: - required: true - - type: dropdown - attributes: - label: Operating system - description: Select your operating system that you are trying to build on. - multiple: true - options: - - MacOS - - Windows - - Linux - validations: - required: true - - type: dropdown - attributes: - label: Can you build the VisionCamera Example app? - description: > - Try to build the example app (`package/example/`) and see if the issue is reproduceable here. - **Note:** If you don't try to build the example app, I most likely won't help you with your issue. - options: - - I didn't try (⚠️ your issue might get ignored & closed if you don't try this) - - Yes, I can successfully build the Example app here - - No, I cannot build the Example app either - default: 0 - validations: - required: true - - type: checkboxes - attributes: - label: Additional information - description: Please check all the boxes that apply - options: - - label: I am using Expo - - label: I have enabled Frame Processors (react-native-worklets-core) - - label: I have read the [Troubleshooting Guide](https://react-native-vision-camera.com/docs/guides/troubleshooting) - required: true - - label: I agree to follow this project's [Code of Conduct](https://github.com/mrousavy/react-native-vision-camera/blob/main/CODE_OF_CONDUCT.md) - required: true - - label: I searched for [similar issues in this repository](https://github.com/mrousavy/react-native-vision-camera/issues) and found none. - required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml deleted file mode 100644 index 6a704922cb..0000000000 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: ✨ Feature request -description: Suggest an idea for this project -title: "✨ " -labels: [✨ feature] -body: - - type: textarea - attributes: - label: What feature or enhancement are you suggesting? - description: Explain what the feature or enhancement you're suggesting is, how it might improve VisionCamera and what it's pros and cons are. - placeholder: I think it would be great to have support for rotation in VisionCamera. - validations: - required: true - - type: dropdown - attributes: - label: What Platforms whould this feature/enhancement affect? - description: Select the platforms where this feature/enhancement should work on. Features/Enhancements that work on all platforms are more likely to be picked up than platform specifics. - multiple: true - options: - - iOS - - Android - validations: - required: true - - type: textarea - attributes: - label: Alternatives/Workarounds - description: Explain if there are any alternatives/workarounds for the feature/enhancement. - placeholder: I am currently manually applying the rotation to the image once it has been taken. - validations: - required: true - - type: checkboxes - attributes: - label: Additional information - description: Please check all the boxes that apply - options: - - label: I agree to follow this project's [Code of Conduct](https://github.com/mrousavy/react-native-vision-camera/blob/main/CODE_OF_CONDUCT.md) - required: true - - label: I searched for [similar feature requests in this repository](https://github.com/mrousavy/react-native-vision-camera/issues?q=is%3Aopen+is%3Aissue+label%3A%22✨+enhancement%22) and found none. - required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..04a76ee21b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,206 @@ +name: "🐛 Bug report (runtime error / crash / unexpected behavior)" +description: "Something is broken at runtime — a crash, a frozen preview, wrong output, wrong camera selected, etc." +title: "🐛 " +labels: ["🐛 bug"] +body: + - type: markdown + attributes: + value: | + ## Before opening this issue + + Camera behavior depends on the device, OS version, format, lens, multi-cam support, permissions, thermal state, and dozens of other factors. **We cannot debug something we cannot reproduce.** Bug reports without a reproduction will be **closed without investigation**. + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: Please confirm you've done each of these. Every box must be ticked. + options: + - label: I have read and followed every applicable step in the [Nitro Modules Troubleshooting guide](https://nitro.margelo.com/docs/guides/troubleshooting). + required: true + - label: I have read the [VisionCamera Troubleshooting guide](https://visioncamera.margelo.com/docs). + required: true + - label: I have searched [existing issues](https://github.com/mrousavy/react-native-vision-camera/issues?q=is%3Aissue) and found nothing matching. + required: true + - label: I am on the [latest version of react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera/releases), or have a specific reason I cannot upgrade. + required: true + + - type: input + id: repro + attributes: + label: Reproduction + description: | + **Required.** Either (strongly preferred) a link to a PR on this repo that adds a failing harness test reproducing the bug, **or** a link to a minimal public GitHub repository that reproduces the issue on a fresh clone. + + ### ✅ Preferred: open a PR with a failing harness test + + Add the smallest possible `it(...)` block under [`apps/simple-camera/__tests__/`](https://github.com/mrousavy/react-native-vision-camera/tree/main/apps/simple-camera/__tests__) that reproduces the bug. Follow [the harness-tests README](https://github.com/mrousavy/react-native-vision-camera/blob/main/apps/simple-camera/__tests__/README.md) for the conventions — it explains where to put the test, how to write it against the `VisionCamera` imperative API, and the hard-vs-soft requirement rules. + + This is the fastest path to a fix: your PR's CI run is the reproduction (no maintainer setup needed), the maintainer can push a fix to the same branch until CI goes green, and both the fix and the regression test land together so the same bug can never come back silently. + + Link the PR URL here. Example: `https://github.com/mrousavy/react-native-vision-camera/pull/1234` + + ### 🥈 Fallback: a reproduction repo + + If the bug genuinely cannot be expressed as a harness test (for example, something specific to a `` component with custom rendering that isn't covered by the imperative tests yet), fork this repo and modify the [`apps/simple-camera`](https://github.com/mrousavy/react-native-vision-camera/tree/main/apps/simple-camera) app, or start from a fresh `npx @react-native-community/cli init` and add only what's needed to show the bug. Link the repo URL here instead. + + Issues without either a PR link or a reproduction repo link are **invalid** and will be closed. Stripped-down snippets in the description do not count. + placeholder: "https://github.com/mrousavy/react-native-vision-camera/pull/ (or a repo URL as fallback)" + validations: + required: true + + - type: textarea + id: repro-steps + attributes: + label: Steps to reproduce + description: | + Exact, numbered steps someone can follow on a fresh clone of the repo above. Include: + - Which device + OS to run on (if specific) + - Which screen / button to interact with + - What to watch for + placeholder: | + 1. Clone https://github.com//vision-camera-bug-repro + 2. `bun install && cd ios && pod install` + 3. Run on iPhone 15 Pro (iOS 17.5) + 4. Tap "Start recording", wait 3 seconds + 5. App crashes with ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + placeholder: "Recording should continue until I tap Stop." + validations: + required: true + + - type: textarea + id: actual + attributes: + label: What actually happened? + description: Describe the actual behavior. If the app crashes, include the crash signal / exception type. + placeholder: "App crashes with EXC_BAD_ACCESS on the capture session queue." + validations: + required: true + + - type: dropdown + id: platforms + attributes: + label: Affected platforms + multiple: true + options: + - iOS (device) + - iOS (simulator) + - Android (device) + - Android (emulator) + validations: + required: true + + - type: input + id: device + attributes: + label: Device(s) affected + description: Exact model and OS version. Include every device you tested on. + placeholder: "iPhone 15 Pro (iOS 17.5.1), Pixel 8 (Android 14)" + validations: + required: true + + - type: input + id: version-exact + attributes: + label: VisionCamera version + description: | + The exact version from your `package.json` (e.g. `5.2.1`). + See [Releases](https://github.com/mrousavy/react-native-vision-camera/releases) for the full list. + placeholder: "5.2.1" + validations: + required: true + + - type: input + id: rn-version + attributes: + label: React Native version + placeholder: "0.85.2" + validations: + required: true + + - type: dropdown + id: architecture + attributes: + label: React Native architecture + options: + - New Architecture (Fabric / bridgeless) + - Old Architecture (Paper) + validations: + required: true + + - type: checkboxes + id: features + attributes: + label: Features being used + description: Which VisionCamera features are in use when the bug happens? Check all that apply. + options: + - label: Preview + - label: Photo capture + - label: Video capture + - label: Frame Processors (worklets) + - label: Skia Frame Processors + - label: Code/Barcode Scanner + - label: Location metadata + - label: Multi-cam + - label: Depth data + - label: HDR / custom dynamic range + - label: Custom format / FPS / resolution + + - type: textarea + id: logs + attributes: + label: Relevant logs / stack trace + description: | + Paste the full crash log, Xcode console output, or `adb logcat` output. Redact anything sensitive. + + **Do not paste screenshots of logs** — paste the text. It will automatically be rendered as a code block. + render: shell + placeholder: | + // iOS example: + Exception Type: EXC_CRASH (SIGABRT) + Termination Reason: SIGNAL 6 Abort trap: 6 + Crashed Thread: 8 com.margelo.camera.session + + Thread 8 Crashed: + 0 libsystem_kernel.dylib __pthread_kill + 8 + 1 libsystem_pthread.dylib pthread_kill + 268 + 2 libsystem_c.dylib abort + 136 + 3 AVFCapture -[AVCaptureOutput attachToFigCaptureSession:] + 108 + 4 AVFCapture -[AVCaptureSession _makeConfigurationLive:] + 344 + ... + + // Android example: + FATAL EXCEPTION: mrousavy/VisionCamera.CameraQueue + Process: com.example.app, PID: 12345 + java.lang.IllegalStateException: Camera not initialized + at com.mrousavy.camera.core.CameraSession.startRunning(CameraSession.kt:142) + at com.mrousavy.camera.react.CameraView.onAttachedToWindow(CameraView.kt:89) + ... + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Anything else that might be relevant — other libraries, custom native modules, Expo vs bare, etc. You can drag-and-drop screenshots or a short screen recording here too (especially useful for broken preview, wrong orientation, or glitched frames). + + - type: checkboxes + id: submission + attributes: + label: Submission + description: Please confirm each of the following about what you submitted above. Every box must be ticked. + options: + - label: The reproduction I linked is either (preferred) a PR against this repo that adds a failing harness test following [the harness-tests README](https://github.com/mrousavy/react-native-vision-camera/blob/main/apps/simple-camera/__tests__/README.md), or (fallback) a public repo that reproduces the bug on a fresh clone. I understand the issue will be closed without one. + required: true + - label: I pasted logs as text (not screenshots). + required: true + - label: I wrote this report in my own words. I did not paste AI-generated descriptions of the bug. + required: true diff --git a/.github/ISSUE_TEMPLATE/build-error-android.yml b/.github/ISSUE_TEMPLATE/build-error-android.yml new file mode 100644 index 0000000000..d77b967c70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/build-error-android.yml @@ -0,0 +1,222 @@ +name: "🛠️ Build error (Android)" +description: "Your app fails to build / compile / link on Android when using react-native-vision-camera." +title: "🛠️ (Android) " +labels: ["🛠️ build-error", "platform:android"] +body: + - type: markdown + attributes: + value: | + ## Before opening this issue + + Most build errors are caused by environment or setup issues, not by VisionCamera itself. + + **A public reproduction repository is required.** Build errors depend on your toolchain, native dependencies, Gradle config, and dozens of other factors — we cannot debug something we cannot reproduce. Reports without a repro will be **closed without investigation**. + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: Please confirm you've done each of these. Every box must be ticked. + options: + - label: I have read and followed every applicable step in the [Nitro Modules Troubleshooting guide](https://nitro.margelo.com/docs/guides/troubleshooting). + required: true + - label: I have read the [VisionCamera Troubleshooting guide](https://visioncamera.margelo.com/docs) and [Installation guide](https://visioncamera.margelo.com/docs). + required: true + - label: I did a clean build — deleted `node_modules`, `android/build`, `android/.gradle`, `android/app/build`, and `~/.gradle/caches`, then reinstalled. + required: true + - label: I searched [existing build-error issues](https://github.com/mrousavy/react-native-vision-camera/issues?q=is%3Aissue+label%3A%22%F0%9F%9B%A0%EF%B8%8F+build-error%22) and found nothing matching. + required: true + + - type: textarea + id: error + attributes: + label: Full build error output + description: | + The complete error as printed by Gradle — not just the last line. Run with `--stacktrace` if the failure is inside a task and include it. + + **Paste as text**, not as a screenshot. Use a code block. + render: shell + validations: + required: true + + - type: input + id: version-exact + attributes: + label: VisionCamera version + description: | + Exact version from `package.json` (e.g. `5.2.1`). + See [Releases](https://github.com/mrousavy/react-native-vision-camera/releases) for the full list. + placeholder: "5.2.1" + validations: + required: true + + - type: input + id: rn-version + attributes: + label: React Native version + placeholder: "0.85.2" + validations: + required: true + + - type: dropdown + id: architecture + attributes: + label: React Native architecture + options: + - New Architecture (Fabric / bridgeless) + - Old Architecture (Paper) + validations: + required: true + + - type: dropdown + id: expo + attributes: + label: Expo? + options: + - Bare React Native (no Expo) + - Expo (managed, EAS Build) + - Expo (bare workflow with config plugins) + validations: + required: true + + - type: input + id: host-os + attributes: + label: Host OS + version + description: What OS are you building on? Include architecture if relevant. + placeholder: "macOS 14.5 (Apple Silicon) / Ubuntu 22.04 / Windows 11" + validations: + required: true + + - type: input + id: android-studio-version + attributes: + label: Android Studio version + description: From Android Studio → About. Even if you build from the CLI, please fill this out. + placeholder: "Android Studio Koala 2024.1.1" + validations: + required: true + + - type: input + id: gradle-version + attributes: + label: Gradle version + description: From `android/gradle/wrapper/gradle-wrapper.properties`. + placeholder: "8.7" + validations: + required: true + + - type: input + id: agp-version + attributes: + label: Android Gradle Plugin (AGP) version + description: From `android/build.gradle`. + placeholder: "8.3.2" + validations: + required: true + + - type: input + id: kotlin-version + attributes: + label: Kotlin version + description: From `android/build.gradle` `kotlinVersion`. + placeholder: "1.9.24" + validations: + required: true + + - type: input + id: jdk-version + attributes: + label: JDK version + description: From `java --version`. + placeholder: "OpenJDK 17.0.10" + validations: + required: true + + - type: input + id: ndk-version + attributes: + label: NDK version + description: From `android/build.gradle` `ndkVersion`. Leave blank if not set. + placeholder: "26.1.10909125" + + - type: input + id: android-min-sdk + attributes: + label: Android minSdkVersion + placeholder: "24" + validations: + required: true + + - type: input + id: android-compile-sdk + attributes: + label: Android compileSdkVersion + placeholder: "34" + validations: + required: true + + - type: input + id: android-target-sdk + attributes: + label: Android targetSdkVersion + placeholder: "34" + validations: + required: true + + - type: dropdown + id: target + attributes: + label: Build target + multiple: true + options: + - Physical device + - Emulator + validations: + required: true + + - type: dropdown + id: package-manager + attributes: + label: Package manager + options: + - npm + - yarn (classic) + - yarn (berry / pnp) + - pnpm + - bun + validations: + required: true + + - type: input + id: repro + attributes: + label: Reproduction repository + description: | + **Required.** A link to a minimal public GitHub repository that reproduces the build error on a fresh clone. + + Start from a fresh `npx @react-native-community/cli init` and add only what's needed to trigger the error. + + Issues without a reproduction repo are **invalid** and will be closed. Stripped-down snippets in the description do not count. + placeholder: "https://github.com//vision-camera-build-repro" + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Any other relevant info — other native libraries, custom Gradle/Kotlin changes, monorepo setup, EAS Build config, R8/ProGuard rules, Hermes vs JSC, etc. + + - type: checkboxes + id: submission + attributes: + label: Submission + description: Please confirm each of the following about what you submitted above. Every box must be ticked. + options: + - label: I pasted the full error output as text (not a screenshot). + required: true + - label: I wrote this report in my own words. I did not paste AI-generated summaries of the error. + required: true + - label: The reproduction repository I linked is public and reproduces the build error on a fresh clone. I understand the issue will be closed without one. + required: true diff --git a/.github/ISSUE_TEMPLATE/build-error-ios.yml b/.github/ISSUE_TEMPLATE/build-error-ios.yml new file mode 100644 index 0000000000..63127baedb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/build-error-ios.yml @@ -0,0 +1,182 @@ +name: "🛠️ Build error (iOS)" +description: "Your app fails to build / compile / link on iOS when using react-native-vision-camera." +title: "🛠️ (iOS) " +labels: ["🛠️ build-error", "platform:ios"] +body: + - type: markdown + attributes: + value: | + ## Before opening this issue + + Most build errors are caused by environment or setup issues, not by VisionCamera itself. + + **A public reproduction repository is required.** Build errors depend on your toolchain, native dependencies, Podfile config, and dozens of other factors — we cannot debug something we cannot reproduce. Reports without a repro will be **closed without investigation**. + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: Please confirm you've done each of these. Every box must be ticked. + options: + - label: I have read and followed every applicable step in the [Nitro Modules Troubleshooting guide](https://nitro.margelo.com/docs/guides/troubleshooting). + required: true + - label: I have read the [VisionCamera Troubleshooting guide](https://visioncamera.margelo.com/docs) and [Installation guide](https://visioncamera.margelo.com/docs). + required: true + - label: I did a clean build — deleted `node_modules`, `ios/Pods`, `ios/build`, and Xcode's DerivedData, then reinstalled. + required: true + - label: I searched [existing build-error issues](https://github.com/mrousavy/react-native-vision-camera/issues?q=is%3Aissue+label%3A%22%F0%9F%9B%A0%EF%B8%8F+build-error%22) and found nothing matching. + required: true + + - type: textarea + id: error + attributes: + label: Full build error output + description: | + The complete error as printed by `xcodebuild` / `pod install` — not just the last line. + + **Paste as text**, not as a screenshot. Use a code block. + render: shell + validations: + required: true + + - type: input + id: version-exact + attributes: + label: VisionCamera version + description: | + Exact version from `package.json` (e.g. `5.2.1`). + See [Releases](https://github.com/mrousavy/react-native-vision-camera/releases) for the full list. + placeholder: "5.2.1" + validations: + required: true + + - type: input + id: rn-version + attributes: + label: React Native version + placeholder: "0.85.2" + validations: + required: true + + - type: dropdown + id: architecture + attributes: + label: React Native architecture + options: + - New Architecture (Fabric / bridgeless) + - Old Architecture (Paper) + validations: + required: true + + - type: dropdown + id: expo + attributes: + label: Expo? + options: + - Bare React Native (no Expo) + - Expo (managed, EAS Build) + - Expo (bare workflow with config plugins) + validations: + required: true + + - type: input + id: host-os + attributes: + label: Host macOS version + description: From `sw_vers`. Include chip (Apple Silicon / Intel). + placeholder: "macOS 14.5 (Sonoma) on Apple Silicon (M2)" + validations: + required: true + + - type: input + id: xcode-version + attributes: + label: Xcode version + description: From `xcodebuild -version`. + placeholder: "Xcode 15.4 (15F31d)" + validations: + required: true + + - type: input + id: ios-deployment-target + attributes: + label: iOS Deployment Target + description: From your `Podfile` / target settings. + placeholder: "15.1" + validations: + required: true + + - type: input + id: cocoapods-version + attributes: + label: CocoaPods version + description: From `pod --version`. + placeholder: "1.15.2" + validations: + required: true + + - type: input + id: ruby-version + attributes: + label: Ruby version + description: From `ruby --version` (matters for CocoaPods). + placeholder: "3.2.2" + validations: + required: true + + - type: dropdown + id: target + attributes: + label: Build target + multiple: true + options: + - Physical device + - Simulator + validations: + required: true + + - type: dropdown + id: package-manager + attributes: + label: Package manager + options: + - npm + - yarn (classic) + - yarn (berry / pnp) + - pnpm + - bun + validations: + required: true + + - type: input + id: repro + attributes: + label: Reproduction repository + description: | + **Required.** A link to a minimal public GitHub repository that reproduces the build error on a fresh clone. + + Start from a fresh `npx @react-native-community/cli init` and add only what's needed to trigger the error. + + Issues without a reproduction repo are **invalid** and will be closed. Stripped-down snippets in the description do not count. + placeholder: "https://github.com//vision-camera-build-repro" + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Any other relevant info — other native libraries, custom Podfile changes, monorepo setup, EAS Build config, use_frameworks!, static/dynamic linking, etc. + + - type: checkboxes + id: submission + attributes: + label: Submission + description: Please confirm each of the following about what you submitted above. Every box must be ticked. + options: + - label: I pasted the full error output as text (not a screenshot). + required: true + - label: I wrote this report in my own words. I did not paste AI-generated summaries of the error. + required: true + - label: The reproduction repository I linked is public and reproduces the build error on a fresh clone. I understand the issue will be closed without one. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5ef62f86fe..69ed05af27 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: + - name: Documentation + url: https://visioncamera.margelo.com/docs + about: Check the docs before opening an issue — most questions are answered there. - name: Troubleshooting Guide - url: https://react-native-vision-camera.com/docs/guides/troubleshooting - about: Please read the Troubleshooting Guide before opening an issue. - - name: Margelo Community Discord - url: https://discord.gg/6CSHz2qAvA - about: Discuss and chat about react-native-vision-camera or other Margelo libraries with our team or other community members. Remember to read the rules! - - name: CameraX Issue Tracker - url: https://issuetracker.google.com/issues?q=componentid:618491%20status:open# - about: If you experience an issue on Android, browsing through CameraX issues often leads to a solution that we can implement in VisionCamera as well. + url: https://visioncamera.margelo.com/docs + about: Common build and runtime issues with known fixes. + - name: Discord + url: https://margelo.com/discord + about: Chat with the community for general questions and help. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000000..25f20db5de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,86 @@ +name: "✨ Feature request" +description: "Suggest a new feature, API, or capability for react-native-vision-camera." +title: "✨ " +labels: ["✨ feature"] +body: + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: Please confirm you've done each of these. Every box must be ticked. + options: + - label: I have searched [existing issues](https://github.com/mrousavy/react-native-vision-camera/issues?q=is%3Aissue) for a similar request. + required: true + + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: Describe the real-world use case. Focus on the problem first, not the solution. + placeholder: | + I'm building a document scanner and I need to detect when the camera is focused on a flat surface before capturing, so the user doesn't get blurry scans. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed API / solution + description: | + What would the new feature look like? Ideally include a code sketch of how you'd use it. + If it's platform-specific (e.g. iOS-only), call that out. + placeholder: | + ```ts + const focus = useCameraFocusState(camera) + if (focus.isStable && focus.subjectArea === 'document') { ... } + ``` + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What workarounds have you tried? Why are they not good enough? + placeholder: | + I tried polling `camera.getIsFocused()` every 100ms but it's noisy and doesn't tell me when the scene has stabilized. + + - type: dropdown + id: platforms + attributes: + label: Platforms this should target + multiple: true + options: + - iOS + - Android + - Both + validations: + required: true + + - type: dropdown + id: contribute + attributes: + label: Would you be willing to contribute this? + options: + - "Yes — I'd like to open a PR." + - "Maybe, with guidance." + - "No, I'm requesting only." + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Links to related AVFoundation / CameraX APIs, papers, other libraries that do this, etc. + + - type: checkboxes + id: submission + attributes: + label: Submission + description: Please confirm each of the following about what you submitted above. Every box must be ticked. + options: + - label: I described the problem, not just the solution. + required: true + - label: I wrote this request in my own words. I did not paste AI-generated proposals. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 28922496d4..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,44 +0,0 @@ - - -## What - - - -## Changes - - - -## Tested on - - - -## Related issues - - diff --git a/.github/actions/collect-devicefarm-results/action.yml b/.github/actions/collect-devicefarm-results/action.yml new file mode 100644 index 0000000000..7701b3e71e --- /dev/null +++ b/.github/actions/collect-devicefarm-results/action.yml @@ -0,0 +1,88 @@ +name: Collect Device Farm Results +description: Find, print, and publish Harness Device Farm test results + +inputs: + artifact-folder: + description: Device Farm artifact folder produced by the test action + required: true + platform: + description: Platform label used in filenames and log output + required: true + test-step-outcome: + description: Outcome of the upstream Device Farm execution step + required: false + +outputs: + junit-file: + description: Copied JUnit file path + value: ${{ steps.find-harness-junit.outputs.junit_file }} + log-file: + description: Copied harness log file path + value: ${{ steps.find-harness-log.outputs.log_file }} + +runs: + using: composite + steps: + - name: Find Harness JUnit result + id: find-harness-junit + shell: bash + run: | + set -euo pipefail + ARTIFACT_DIR="${{ inputs.artifact-folder }}" + PLATFORM="${{ inputs.platform }}" + WORKSPACE_JUNIT=".github/test-results/harness-results-${PLATFORM}.junit.xml" + bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \ + "$ARTIFACT_DIR" \ + 'harness-results.junit.xml' \ + "$WORKSPACE_JUNIT" + echo "junit_file=$WORKSPACE_JUNIT" >> "$GITHUB_OUTPUT" + + - name: Find Harness output log + id: find-harness-log + shell: bash + run: | + set -euo pipefail + ARTIFACT_DIR="${{ inputs.artifact-folder }}" + PLATFORM="${{ inputs.platform }}" + WORKSPACE_LOG=".github/test-results/harness-output-${PLATFORM}.log" + bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \ + "$ARTIFACT_DIR" \ + 'harness-output.log' \ + "$WORKSPACE_LOG" + echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT" + + - name: Publish Harness test results + id: publish-harness-results + if: steps.find-harness-junit.outputs.junit_file != '' + continue-on-error: true + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: ${{ steps.find-harness-junit.outputs.junit_file }} + comment_mode: off + job_summary: true + check_run: false + action_fail: true + action_fail_on_inconclusive: true + fail_on: test failures + + - name: Print Harness output log + shell: bash + run: | + set -euo pipefail + LOG_FILE="${{ steps.find-harness-log.outputs.log_file }}" + PLATFORM="${{ inputs.platform }}" + PUBLISH_OUTCOME="${{ steps.publish-harness-results.outcome }}" + TEST_STEP_OUTCOME="${{ inputs.test-step-outcome }}" + echo "===== Harness ${PLATFORM} output log =====" + cat "$LOG_FILE" + echo "===== End Harness ${PLATFORM} output log =====" + + if [[ "$PUBLISH_OUTCOME" == "failure" ]]; then + echo "Harness test result publication reported failures." >&2 + exit 1 + fi + + if [[ "$TEST_STEP_OUTCOME" == "failure" ]]; then + echo "Device Farm test step reported failure." >&2 + exit 1 + fi diff --git a/.github/actions/upload-devicefarm-artifact/action.yml b/.github/actions/upload-devicefarm-artifact/action.yml new file mode 100644 index 0000000000..c4c9e964a5 --- /dev/null +++ b/.github/actions/upload-devicefarm-artifact/action.yml @@ -0,0 +1,169 @@ +name: Upload File to AWS Device Farm +description: Create and upload a file to AWS Device Farm and wait until processing finishes. + +inputs: + project-arn: + description: Device Farm project ARN. + required: true + file-path: + description: Path to the file to upload. + required: true + upload-type: + description: Device Farm upload type (for example ANDROID_APP, APPIUM_NODE_TEST_SPEC). + required: false + default: ANDROID_APP + upload-name: + description: Optional upload name shown in Device Farm. Defaults to filename. + required: false + default: '' + content-type: + description: MIME type for the upload. + required: false + default: application/vnd.android.package-archive + aws-region: + description: AWS region where the Device Farm project exists. + required: false + default: us-west-2 + poll-interval-seconds: + description: Poll interval in seconds while waiting for processing to complete. + required: false + default: '5' + max-wait-seconds: + description: Maximum wait time in seconds before timing out. + required: false + default: '600' + +outputs: + file-arn: + description: ARN for the uploaded file. + value: ${{ steps.upload.outputs.upload_arn }} + upload-arn: + description: Device Farm upload ARN. + value: ${{ steps.upload.outputs.upload_arn }} + upload-name: + description: Effective upload name in Device Farm. + value: ${{ steps.upload.outputs.upload_name }} + upload-status: + description: Final upload processing status. + value: ${{ steps.upload.outputs.upload_status }} + +runs: + using: composite + steps: + - id: upload + shell: bash + run: | + set -euo pipefail + + PROJECT_ARN="${{ inputs.project-arn }}" + FILE_PATH="${{ inputs.file-path }}" + UPLOAD_TYPE="${{ inputs.upload-type }}" + UPLOAD_NAME_INPUT="${{ inputs.upload-name }}" + CONTENT_TYPE="${{ inputs.content-type }}" + AWS_REGION="${{ inputs.aws-region }}" + POLL_INTERVAL="${{ inputs.poll-interval-seconds }}" + MAX_WAIT="${{ inputs.max-wait-seconds }}" + + if [[ -z "$FILE_PATH" ]]; then + echo "No input file provided. Set file-path." >&2 + exit 1 + fi + + if [[ ! -f "$FILE_PATH" ]]; then + echo "Upload file not found: $FILE_PATH" >&2 + exit 1 + fi + + REQUIRE_ZIP="false" + if [[ "$UPLOAD_TYPE" == "ANDROID_APP" || "$UPLOAD_TYPE" == "IOS_APP" || "$UPLOAD_TYPE" == *_TEST_PACKAGE ]]; then + REQUIRE_ZIP="true" + fi + + if [[ "$REQUIRE_ZIP" == "true" ]] && ! unzip -tq "$FILE_PATH" >/dev/null 2>&1; then + echo "File is not a valid ZIP archive (required for upload type $UPLOAD_TYPE): $FILE_PATH" >&2 + exit 1 + fi + + if ! [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || ! [[ "$MAX_WAIT" =~ ^[0-9]+$ ]]; then + echo "poll-interval-seconds and max-wait-seconds must be positive integers." >&2 + exit 1 + fi + if (( POLL_INTERVAL <= 0 || MAX_WAIT <= 0 )); then + echo "poll-interval-seconds and max-wait-seconds must be greater than 0." >&2 + exit 1 + fi + + if [[ -n "$UPLOAD_NAME_INPUT" ]]; then + UPLOAD_NAME="$UPLOAD_NAME_INPUT" + else + UPLOAD_NAME="$(basename "$FILE_PATH")" + fi + + IFS=$'\t' read -r UPLOAD_ARN UPLOAD_URL CREATED_UPLOAD_NAME INITIAL_STATUS < <( + aws devicefarm create-upload \ + --region "$AWS_REGION" \ + --project-arn "$PROJECT_ARN" \ + --name "$UPLOAD_NAME" \ + --type "$UPLOAD_TYPE" \ + --content-type "$CONTENT_TYPE" \ + --query 'upload.[arn,url,name,status]' \ + --output text + ) + + if [[ -z "$UPLOAD_ARN" || -z "$UPLOAD_URL" ]]; then + echo "Failed to create Device Farm upload." >&2 + exit 1 + fi + + SIGNED_HEADERS="$(sed -n 's/.*[?&]X-Amz-SignedHeaders=\([^&]*\).*/\1/p' <<< "$UPLOAD_URL")" + echo "Created upload '$CREATED_UPLOAD_NAME' with initial status '$INITIAL_STATUS'." + echo "Signed headers for upload URL: ${SIGNED_HEADERS:-unknown}" + + curl --fail --silent --show-error \ + --request PUT \ + --header "content-type: $CONTENT_TYPE" \ + --upload-file "$FILE_PATH" \ + "$UPLOAD_URL" + + elapsed=0 + STATUS="PROCESSING" + MESSAGE="" + while true; do + IFS=$'\t' read -r STATUS MESSAGE < <( + aws devicefarm get-upload \ + --region "$AWS_REGION" \ + --arn "$UPLOAD_ARN" \ + --query 'upload.[status,message]' \ + --output text + ) + + case "$STATUS" in + SUCCEEDED) + break + ;; + FAILED | ERRORED) + echo "Device Farm upload failed: ${MESSAGE:-}" >&2 + aws devicefarm get-upload \ + --region "$AWS_REGION" \ + --arn "$UPLOAD_ARN" \ + --query 'upload.{status:status,message:message,metadata:metadata,name:name,type:type,contentType:contentType}' \ + --output json >&2 || true + exit 1 + ;; + *) + ;; + esac + + if (( elapsed >= MAX_WAIT )); then + echo "Timed out waiting for upload to finish. Last status: $STATUS" >&2 + exit 1 + fi + + sleep "$POLL_INTERVAL" + elapsed=$((elapsed + POLL_INTERVAL)) + done + + echo "file_arn=$UPLOAD_ARN" >> "$GITHUB_OUTPUT" + echo "upload_arn=$UPLOAD_ARN" >> "$GITHUB_OUTPUT" + echo "upload_name=$CREATED_UPLOAD_NAME" >> "$GITHUB_OUTPUT" + echo "upload_status=$STATUS" >> "$GITHUB_OUTPUT" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 249e397712..00da1cb69d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,39 +1,31 @@ version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - labels: - - "🛠 dependencies" - package-ecosystem: "gradle" - directory: "/package/android/" + directories: + - "/apps/simple-camera/android/" + - "/packages/react-native-vision-camera/android/" + - "/packages/react-native-vision-camera-barcode-scanner/android/" + - "/packages/react-native-vision-camera-location/android/" + - "/packages/react-native-vision-camera-resizer/android/" schedule: interval: "daily" - labels: - - "🛠 dependencies" - - "🤖 android" - # - package-ecosystem: "npm" - # directory: "/package/" - # schedule: - # interval: "monthly" - # labels: - # - "🛠 dependencies" - # - "☕️ js" - # - package-ecosystem: "npm" - # directory: "/package/example/" - # schedule: - # interval: "monthly" - # labels: - # - "🛠 dependencies" - # - "🛸 example" - # - "☕️ js" - # - package-ecosystem: "npm" - # directory: "/docs/" - # schedule: - # interval: "monthly" - # labels: - # - "🛠 dependencies" - # - "📚 documentation" - # - "☕️ js" + groups: + camera-libs: + patterns: + - "androidx.camera:*" + gms-location: + patterns: + - "com.google.android.gms:play-services-location" + mlkit: + patterns: + - "com.google.mlkit:*" + - "com.google.android.gms:play-services-mlkit-*" + gradle-build-and-plugins: + patterns: + - "*" + exclude-patterns: + - "androidx.camera:*" + - "com.google.android.gms:play-services-location" + - "com.google.mlkit:*" + - "com.google.android.gms:play-services-mlkit-*" diff --git a/.github/funding-octocat.svg b/.github/funding-octocat.svg deleted file mode 100644 index 3b7609f8a2..0000000000 --- a/.github/funding-octocat.svg +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- This library helped you?
Consider sponsoring!
-
-
-
diff --git a/.github/scripts/find-devicefarm-artifact.sh b/.github/scripts/find-devicefarm-artifact.sh new file mode 100644 index 0000000000..2221ea4055 --- /dev/null +++ b/.github/scripts/find-devicefarm-artifact.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# From AWS devicefarm we receive a zip file with all the artifacts. This script helps retrieving +# a specific artifact file from the zip and copying it to a destination path in the workspace. + +set -euo pipefail + +ARTIFACT_DIR="${1:-}" +TARGET_NAME="${2:-}" +DESTINATION_PATH="${3:-}" + +if [[ -z "$ARTIFACT_DIR" || ! -d "$ARTIFACT_DIR" ]]; then + echo "Device Farm artifact folder missing: '$ARTIFACT_DIR'" >&2 + exit 1 +fi + +if [[ -z "$TARGET_NAME" ]]; then + echo "Expected target artifact filename as the second argument." >&2 + exit 1 +fi + +if [[ -z "$DESTINATION_PATH" ]]; then + echo "Expected destination path as the third argument." >&2 + exit 1 +fi + +FOUND_FILE="$(find "$ARTIFACT_DIR" -type f -name "$TARGET_NAME" | head -n 1 || true)" +if [[ -z "$FOUND_FILE" ]]; then + EXTRACT_DIR="$(mktemp -d)" + + while IFS= read -r ZIP_FILE; do + unzip -o -qq "$ZIP_FILE" -d "$EXTRACT_DIR" || true + done < <(find "$ARTIFACT_DIR" -type f -name '*.zip') + + FOUND_FILE="$(find "$EXTRACT_DIR" -type f -name "$TARGET_NAME" | head -n 1 || true)" +fi + +if [[ -z "$FOUND_FILE" ]]; then + echo "Could not find $TARGET_NAME in Device Farm artifacts." >&2 + exit 1 +fi + +mkdir -p "$(dirname "$DESTINATION_PATH")" +cp "$FOUND_FILE" "$DESTINATION_PATH" + +echo "Found artifact file: $FOUND_FILE" +echo "Copied artifact file to workspace: $DESTINATION_PATH" +printf '%s\n' "$DESTINATION_PATH" diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml deleted file mode 100644 index f7b1f4a31f..0000000000 --- a/.github/workflows/build-android.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Build Android - -on: - push: - branches: - - main - paths: - - '.github/workflows/build-android.yml' - - 'package/android/**' - - 'package/example/android/**' - - 'package/yarn.lock' - - 'package/example/yarn.lock' - pull_request: - paths: - - '.github/workflows/build-android.yml' - - 'package/android/**' - - 'package/example/android/**' - - 'package/yarn.lock' - - 'package/example/yarn.lock' - -jobs: - build: - name: Build Android Example App - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./package - steps: - - uses: actions/checkout@v4 - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 17 - java-package: jdk - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-with-fps-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-with-fps-yarn- - - name: Install node_modules - run: yarn install --frozen-lockfile - - name: Install node_modules for example/ - run: yarn install --frozen-lockfile --cwd example - - - name: Restore Gradle cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - key: ${{ runner.os }}-with-fps-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-with-fps-gradle- - - name: Run Gradle Build for example/android/ - run: cd example/android && ./gradlew assembleDebug --no-daemon --build-cache && cd ../.. - - # Gradle cache doesn't like daemons - - name: Stop Gradle Daemon - run: cd android && ./gradlew --stop - - build-no-frame-processors: - name: Build Android Example App (without Frame Processors) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./package - steps: - - uses: actions/checkout@v4 - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 17 - java-package: jdk - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-without-fps-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-without-fps-yarn- - - name: Install node_modules - run: yarn install --frozen-lockfile - - name: Install node_modules for example/ - run: yarn install --frozen-lockfile --cwd example - - name: Remove worklets, skia and reanimated - run: yarn remove react-native-worklets-core @shopify/react-native-skia react-native-reanimated --cwd example - - - name: Restore Gradle cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - key: ${{ runner.os }}-without-fps-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-without-fps-gradle- - - name: Run Gradle Build for example/android/ - run: cd example/android && ./gradlew assembleDebug --no-daemon --build-cache && cd ../.. - - # Gradle cache doesn't like daemons - - name: Stop Gradle Daemon - run: cd android && ./gradlew --stop diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml deleted file mode 100644 index a979d50b5e..0000000000 --- a/.github/workflows/build-ios.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Build iOS - -on: - push: - branches: - - main - paths: - - '.github/workflows/build-ios.yml' - - 'package/ios/**' - - 'package/*.podspec' - - 'package/example/ios/**' - pull_request: - paths: - - '.github/workflows/build-ios.yml' - - 'package/ios/**' - - 'package/*.podspec' - - 'package/example/ios/**' - -jobs: - build: - name: Build iOS Example App - runs-on: macOS-latest - defaults: - run: - working-directory: package/example/ios - steps: - - uses: actions/checkout@v4 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-with-fps-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-with-fps-yarn- - - name: Install node_modules for example/ - run: yarn install --frozen-lockfile --cwd .. - - - name: Restore buildcache - uses: mikehardy/buildcache-action@v2 - continue-on-error: true - - - name: Setup Ruby (bundle) - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.7.2 - bundler-cache: true - working-directory: package/example/ios - - - name: Restore Pods cache - uses: actions/cache@v4 - with: - path: package/example/ios/Pods - key: ${{ runner.os }}-with-fps-pods-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-with-fps-pods- - - name: Install Pods - run: pod install - - name: Install xcpretty - run: gem install xcpretty - - name: Build App - run: "set -o pipefail && xcodebuild \ - CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ - -derivedDataPath build -UseModernBuildSystem=YES \ - -workspace VisionCameraExample.xcworkspace \ - -scheme VisionCameraExample \ - -sdk iphonesimulator \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 15' \ - build \ - CODE_SIGNING_ALLOWED=NO | xcpretty" - - build-no-frame-processors: - name: Build iOS Example App without Frame Processors - runs-on: macOS-latest - defaults: - run: - working-directory: package/example/ios - steps: - - uses: actions/checkout@v4 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-without-fps-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-without-fps-yarn- - - name: Install node_modules for example/ - run: yarn install --frozen-lockfile --cwd .. - - name: Remove worklets, skia and reanimated - run: yarn remove react-native-worklets-core @shopify/react-native-skia react-native-reanimated --cwd .. - - - name: Restore buildcache - uses: mikehardy/buildcache-action@v2 - continue-on-error: true - - - name: Setup Ruby (bundle) - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.7.2 - bundler-cache: true - working-directory: package/example/ios - - - name: Restore Pods cache - uses: actions/cache@v4 - with: - path: package/example/ios/Pods - key: ${{ runner.os }}-without-fps-pods-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-without-fps-pods- - - name: Install Pods - run: pod install - - name: Install xcpretty - run: gem install xcpretty - - name: Build App - run: "set -o pipefail && xcodebuild \ - CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ - -derivedDataPath build -UseModernBuildSystem=YES \ - -workspace VisionCameraExample.xcworkspace \ - -scheme VisionCameraExample \ - -sdk iphonesimulator \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 15' \ - build \ - CODE_SIGNING_ALLOWED=NO | xcpretty" diff --git a/.github/workflows/compress-images.yml b/.github/workflows/compress-images.yml deleted file mode 100644 index c12d854870..0000000000 --- a/.github/workflows/compress-images.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Compress Images (docs) -on: - pull_request: - # Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed. - # See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference. - paths: - - ".github/workflows/compress-images.yml" - - "**.jpg" - - "**.jpeg" - - "**.png" - - "**.webp" - -jobs: - compress-images: - # Only run on Pull Requests within the same repository, and not from forks. - if: github.event.pull_request.head.repo.full_name == github.repository - name: 🗂 Compress images - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - - name: Compress Images - uses: calibreapp/image-actions@main - with: - # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. - # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions - githubToken: ${{ secrets.GITHUB_TOKEN }} - ignorePaths: "e2e/**" - jpegQuality: "80" - jpegProgressive: false - pngQuality: "80" - webpQuality: "80" diff --git a/.github/workflows/harness-android-emulator.yml b/.github/workflows/harness-android-emulator.yml new file mode 100644 index 0000000000..d2694500ce --- /dev/null +++ b/.github/workflows/harness-android-emulator.yml @@ -0,0 +1,161 @@ +name: Harness Android Emulator + +concurrency: + group: harness-android-emulator-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +on: + workflow_dispatch: + inputs: + api_level: + description: 'Android API level for the emulator' + required: false + default: '35' + type: string + device_arch: + description: 'Device architecture (x86_64, arm64-v8a)' + required: false + default: 'x86_64' + type: choice + options: + - x86_64 + - arm64-v8a + device_profile: + description: 'Android emulator profile' + required: false + default: 'pixel' + type: string + emulator_name: + description: 'Android emulator (AVD) name from rn-harness config' + required: false + default: 'Pixel_API_35' + type: string + push: + branches: + - main + paths: + - '.github/workflows/harness-android-emulator.yml' + - 'apps/simple-camera/**' + - 'packages/react-native-vision-camera/**' + - 'packages/react-native-vision-camera-barcode-scanner/**' + - 'packages/react-native-vision-camera-location/**' + - 'packages/react-native-vision-camera-resizer/**' + - 'packages/react-native-vision-camera-skia/**' + - 'bun.lock' + - 'package.json' + pull_request: + paths: + - '.github/workflows/harness-android-emulator.yml' + - 'apps/simple-camera/**' + - 'packages/react-native-vision-camera/**' + - 'packages/react-native-vision-camera-barcode-scanner/**' + - 'packages/react-native-vision-camera-location/**' + - 'packages/react-native-vision-camera-resizer/**' + - 'packages/react-native-vision-camera-skia/**' + - 'bun.lock' + - 'package.json' + +env: + HARNESS_PROJECT_ROOT: apps/simple-camera + HARNESS_ANDROID_APP_BUILD_OUTPUT: apps/simple-camera/android/app/build/outputs/apk/debug/app-debug.apk + HARNESS_ANDROID_BUNDLE_ID: com.margelo.nitro.camera.example.simple + HARNESS_ANDROID_DEVICE_MODE: emulator + HARNESS_ANDROID_API_LEVEL: ${{ github.event.inputs.api_level || '35' }} + HARNESS_ANDROID_DEVICE_ARCH: ${{ github.event.inputs.device_arch || 'x86_64' }} + HARNESS_ANDROID_DEVICE_PROFILE: ${{ github.event.inputs.device_profile || 'pixel' }} + HARNESS_ANDROID_DISK_SIZE: 1G + HARNESS_ANDROID_HEAP_SIZE: 1G + HARNESS_ANDROID_EMULATOR: ${{ github.event.inputs.emulator_name || 'Pixel_API_35' }} + HARNESS_ANDROID_EMULATOR_OPTIONS: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back emulated -camera-front emulated + HARNESS_ANDROID_EMULATOR_BOOT_TIMEOUT_SECONDS: 240 + HARNESS_ANDROID_STARTUP_TIMEOUT_SECONDS: 60 + HARNESS_ANDROID_TEST_TIMEOUT_SECONDS: 600 + +jobs: + test-android: + name: Test Android + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup JDK 17 + uses: actions/setup-java@v5 + with: + distribution: zulu + java-version: '17' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Restore Gradle/CMake cache + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + apps/simple-camera/android/.gradle + apps/simple-camera/android/app/.cxx + key: ${{ runner.os }}-gradle-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'packages/**/CMakeLists.txt', 'packages/**/*.cmake', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-gradle-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}- + ${{ runner.os }}-gradle- + + - name: Restore Android app from cache + id: cache-apk-restore + uses: actions/cache/restore@v4 + with: + path: ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + key: harness-android-apk-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'apps/simple-camera/android/app/src/**', 'packages/**/android/**', 'packages/**/cpp/**', 'packages/**/nitrogen/generated/**', 'apps/simple-camera/package.json', 'bun.lock') }} + + - name: Build Android app + if: steps.cache-apk-restore.outputs.cache-hit != 'true' + working-directory: apps/simple-camera/android + run: ./gradlew assembleDebug -PreactNativeArchitectures=${{ env.HARNESS_ANDROID_DEVICE_ARCH }} --no-daemon --build-cache --console=plain + + - name: Save Android app to cache + if: steps.cache-apk-restore.outputs.cache-hit != 'true' && success() + uses: actions/cache/save@v4 + with: + path: ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + key: harness-android-apk-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'apps/simple-camera/android/app/src/**', 'packages/**/android/**', 'packages/**/cpp/**', 'packages/**/nitrogen/generated/**', 'apps/simple-camera/package.json', 'bun.lock') }} + + - name: Verify Android app artifact + run: test -f ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + + - name: Run Harness tests on Android + uses: reactivecircus/android-emulator-runner@v2 + with: + working-directory: ${{ env.HARNESS_PROJECT_ROOT }} + api-level: ${{ env.HARNESS_ANDROID_API_LEVEL }} + arch: ${{ env.HARNESS_ANDROID_DEVICE_ARCH }} + profile: ${{ env.HARNESS_ANDROID_DEVICE_PROFILE }} + disk-size: ${{ env.HARNESS_ANDROID_DISK_SIZE }} + heap-size: ${{ env.HARNESS_ANDROID_HEAP_SIZE }} + force-avd-creation: true + avd-name: ${{ env.HARNESS_ANDROID_EMULATOR }} + emulator-boot-timeout: ${{ env.HARNESS_ANDROID_EMULATOR_BOOT_TIMEOUT_SECONDS }} + disable-animations: true + emulator-options: ${{ env.HARNESS_ANDROID_EMULATOR_OPTIONS }} + script: bash ./scripts/run-harness-android-ci.sh + + - name: Stop Gradle Daemon + if: always() + working-directory: apps/simple-camera/android + run: ./gradlew --stop diff --git a/.github/workflows/harness-aws-device.yml b/.github/workflows/harness-aws-device.yml new file mode 100644 index 0000000000..34672e71b9 --- /dev/null +++ b/.github/workflows/harness-aws-device.yml @@ -0,0 +1,404 @@ +name: Harness AWS Device + +### About +# +# This workflow performs Android and iOS e2e tests with React Native Harness +# on AWS Device Farm real devices. + +# This is needed for the AWS action to be able to upload properly to AWS +permissions: + id-token: write + contents: read + pull-requests: read + +concurrency: + group: harness-aws-device-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +on: + workflow_dispatch: + inputs: + device_arch: + description: "Device architecture (x86_64, arm64-v8a)" + required: false + default: "arm64-v8a" + type: choice + options: + - x86_64 + - arm64-v8a + push: + branches: + - main + paths: + - ".github/workflows/harness-aws-device.yml" + - ".github/actions/upload-devicefarm-artifact/**" + - "apps/simple-camera/**" + - "packages/react-native-vision-camera/**" + - "packages/react-native-vision-camera-barcode-scanner/**" + - "packages/react-native-vision-camera-location/**" + - "packages/react-native-vision-camera-resizer/**" + - "packages/react-native-vision-camera-skia/**" + - "bun.lock" + - "package.json" + pull_request: + paths: + - ".github/workflows/harness-aws-device.yml" + - ".github/actions/upload-devicefarm-artifact/**" + - "apps/simple-camera/**" + - "packages/react-native-vision-camera/**" + - "packages/react-native-vision-camera-barcode-scanner/**" + - "packages/react-native-vision-camera-location/**" + - "packages/react-native-vision-camera-resizer/**" + - "packages/react-native-vision-camera-skia/**" + - "bun.lock" + - "package.json" + +env: + USE_CCACHE: 1 + HARNESS_AWS_REGION: us-west-2 + HARNESS_DEVICE_FARM_PROJECT_ARN: arn:aws:devicefarm:us-west-2:633665345122:project:210b1942-012b-4653-9673-f3ff91c5e649 + HARNESS_XCODE_VERSION: "26.2" + HARNESS_ANDROID_APP_BUILD_OUTPUT: apps/simple-camera/android/app/build/outputs/apk/debug/app-debug.apk + HARNESS_IOS_APP_BUILD_OUTPUT: apps/simple-camera/ios/build/devicefarm/SimpleCamera.ipa + HARNESS_IOS_DERIVED_DATA_OUTPUT: apps/simple-camera/ios/build/devicefarm/DerivedData + HARNESS_DEVICE_FARM_TEST_PACKAGE_OUTPUT: devicefarm-test-package.zip + HARNESS_ANDROID_BUNDLE_ID: com.margelo.nitro.camera.example.simple + HARNESS_ANDROID_DEVICE_ARCH: ${{ github.event.inputs.device_arch || 'arm64-v8a' }} + # Name of the device pool on AWS to pick Android devices from. + # (iOS pool config lives alongside the `test-ios` job and will be restored + # when iOS Device Farm testing is re-enabled — see `build-ios`.) + HARNESS_DEVICE_FARM_ANDROID_DEVICE_POOL_ARN: LatestFlagshipsDynamic + +jobs: + detect-changes: + name: Detect Changes + runs-on: ubuntu-latest + outputs: + run_android: ${{ steps.set_dispatch.outputs.run_android || steps.set_filter.outputs.run_android }} + run_ios: ${{ steps.set_dispatch.outputs.run_ios || steps.set_filter.outputs.run_ios }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - id: filter + if: ${{ github.event_name != 'workflow_dispatch' }} + uses: dorny/paths-filter@v3 + with: + filters: | + android: + - 'apps/simple-camera/android/**' + - 'apps/simple-camera/device-farm-tests/AwsTestSpec.yml' + - 'packages/**/*.kt' + - 'packages/**/android/**/*.@(cpp|hpp)' + ios: + - 'apps/simple-camera/ios/**' + - 'apps/simple-camera/device-farm-tests/AwsTestSpecIOS.yml' + - 'packages/**/*.@(swift|mm)' + shared: + - 'apps/simple-camera/__tests__/vision-camera.harness.ts' + - 'apps/simple-camera/src/**' + - 'apps/simple-camera/rn-harness.config.mjs' + - 'apps/simple-camera/package.json' + - 'packages/**' + - '!packages/**/*.kt' + - '!packages/**/android/**/*.@(cpp|hpp)' + - '!packages/**/*.@(swift|mm)' + - 'patches/**' + - '.github/workflows/harness-aws-device.yml' + - '.github/actions/upload-devicefarm-artifact/**' + - 'bun.lock' + - 'package.json' + + - id: set_dispatch + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo "run_android=true" >> "$GITHUB_OUTPUT" + echo "run_ios=true" >> "$GITHUB_OUTPUT" + + - id: set_filter + if: ${{ github.event_name != 'workflow_dispatch' }} + run: | + echo "run_android=${{ steps.filter.outputs.android == 'true' || steps.filter.outputs.shared == 'true' }}" >> "$GITHUB_OUTPUT" + echo "run_ios=${{ steps.filter.outputs.ios == 'true' || steps.filter.outputs.shared == 'true' }}" >> "$GITHUB_OUTPUT" + + prepare-devicefarm-assets: + name: Prepare Device Farm Assets + runs-on: ubuntu-latest + needs: + - detect-changes + # iOS Device Farm testing is temporarily disabled (see `test-ios`/`build-ios` jobs), + # so this only needs to run when Android testing is scheduled. + if: ${{ needs.detect-changes.outputs.run_android == 'true' }} + outputs: + test_package_arn: ${{ steps.upload-test-package.outputs.upload-arn }} + android_test_spec_arn: ${{ steps.upload-android-test-spec.outputs.upload-arn }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::633665345122:role/GitHubDeviceFarmRole + aws-region: ${{ env.HARNESS_AWS_REGION }} + + - name: Create Device Farm test package zip + run: | + rm -f "${{ env.HARNESS_DEVICE_FARM_TEST_PACKAGE_OUTPUT }}" + git archive \ + --format=zip \ + --output "${{ env.HARNESS_DEVICE_FARM_TEST_PACKAGE_OUTPUT }}" \ + HEAD + unzip -tq "${{ env.HARNESS_DEVICE_FARM_TEST_PACKAGE_OUTPUT }}" + + - name: Upload Device Farm test package + id: upload-test-package + uses: ./.github/actions/upload-devicefarm-artifact + with: + project-arn: ${{ env.HARNESS_DEVICE_FARM_PROJECT_ARN }} + aws-region: ${{ env.HARNESS_AWS_REGION }} + file-path: ${{ env.HARNESS_DEVICE_FARM_TEST_PACKAGE_OUTPUT }} + upload-type: APPIUM_NODE_TEST_PACKAGE + content-type: application/zip + upload-name: DeviceFarmTestPackage-${{ github.run_id }}-${{ github.run_attempt }}.zip + + - name: Upload Device Farm Android test spec + id: upload-android-test-spec + uses: ./.github/actions/upload-devicefarm-artifact + with: + project-arn: ${{ env.HARNESS_DEVICE_FARM_PROJECT_ARN }} + aws-region: ${{ env.HARNESS_AWS_REGION }} + file-path: apps/simple-camera/device-farm-tests/AwsTestSpec.yml + upload-type: APPIUM_NODE_TEST_SPEC + content-type: application/octet-stream + upload-name: AwsTestSpecAndroid-${{ github.run_id }}-${{ github.run_attempt }}.yml + + test-android: + name: Test Android + runs-on: ubuntu-latest + timeout-minutes: 45 + needs: + - detect-changes + - prepare-devicefarm-assets + if: ${{ needs.detect-changes.outputs.run_android == 'true' }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup JDK 17 + uses: actions/setup-java@v5 + with: + distribution: zulu + java-version: "17" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Restore Gradle/CMake cache + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + apps/simple-camera/android/.gradle + apps/simple-camera/android/app/.cxx + key: ${{ runner.os }}-gradle-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'packages/**/CMakeLists.txt', 'packages/**/*.cmake', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-gradle-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}- + ${{ runner.os }}-gradle- + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::633665345122:role/GitHubDeviceFarmRole + aws-region: ${{ env.HARNESS_AWS_REGION }} + + - name: Restore Android app from cache + id: cache-apk-restore + uses: actions/cache/restore@v4 + with: + path: ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + key: harness-android-apk-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'apps/simple-camera/android/app/src/**', 'packages/**/android/**', 'packages/**/cpp/**', 'packages/**/nitrogen/generated/**', 'apps/simple-camera/package.json', 'bun.lock') }} + + - name: Build Android app + if: steps.cache-apk-restore.outputs.cache-hit != 'true' + working-directory: apps/simple-camera/android + run: ./gradlew assembleDebug -Pandroid.injected.testOnly=false -PreactNativeArchitectures=${{ env.HARNESS_ANDROID_DEVICE_ARCH }} --no-daemon --build-cache --console=plain + + - name: Save Android app to cache + if: steps.cache-apk-restore.outputs.cache-hit != 'true' && success() + uses: actions/cache/save@v4 + with: + path: ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + key: harness-android-apk-${{ env.HARNESS_ANDROID_DEVICE_ARCH }}-${{ hashFiles('apps/simple-camera/android/**/*.gradle*', 'apps/simple-camera/android/**/gradle-wrapper.properties', 'apps/simple-camera/android/app/src/**', 'packages/**/android/**', 'packages/**/cpp/**', 'packages/**/nitrogen/generated/**', 'apps/simple-camera/package.json', 'bun.lock') }} + + - name: Verify Android app artifact + run: test -f ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + + - name: Upload APK to AWS Device Farm + id: upload-apk + uses: ./.github/actions/upload-devicefarm-artifact + with: + project-arn: ${{ env.HARNESS_DEVICE_FARM_PROJECT_ARN }} + aws-region: ${{ env.HARNESS_AWS_REGION }} + file-path: ${{ env.HARNESS_ANDROID_APP_BUILD_OUTPUT }} + upload-name: app-debug-${{ github.run_id }}-${{ github.run_attempt }}.apk + + - name: Schedule Device Farm Android Automated Test + id: run-test + uses: aws-actions/aws-devicefarm-mobile-device-testing@v2.3 + continue-on-error: true + with: + run-settings-json: | + { + "name": "GitHubAction-${{ github.workflow }}-android_${{ github.run_id }}_${{ github.run_attempt }}", + "projectArn": "${{ env.HARNESS_DEVICE_FARM_PROJECT_ARN }}", + "appArn": "${{ steps.upload-apk.outputs['upload-arn'] }}", + "devicePoolArn": "${{ env.HARNESS_DEVICE_FARM_ANDROID_DEVICE_POOL_ARN }}", + "test": { + "type": "APPIUM_NODE", + "testPackageArn": "${{ needs.prepare-devicefarm-assets.outputs.test_package_arn }}", + "testSpecArn": "${{ needs.prepare-devicefarm-assets.outputs.android_test_spec_arn }}" + } + } + artifact-types: ALL + + - name: Collect Android Device Farm results + if: always() + uses: ./.github/actions/collect-devicefarm-results + with: + artifact-folder: ${{ steps.run-test.outputs.artifact-folder }} + platform: android + test-step-outcome: ${{ steps.run-test.outcome }} + + # iOS testing on AWS Device Farm is temporarily disabled. + # Device Farm has no UI operator to tap through runtime permission dialogs + # (Camera, Microphone, Location, etc.), so the app hangs on first launch. + # Unlike Android, there is no equivalent to `adb shell pm grant` for iOS, so + # we depend on an upcoming React Native Harness feature to pre-grant iOS + # permissions. Until that ships, we still build the iOS app here as a + # compile-only smoke test (and to keep the DerivedData/Pods caches warm), but + # we do NOT schedule any Device Farm run. + # TODO: Re-enable iOS Device Farm testing once Harness ships iOS permission + # pre-granting. + build-ios: + name: Build iOS + runs-on: macos-latest + timeout-minutes: 45 + needs: + - detect-changes + if: ${{ needs.detect-changes.outputs.run_ios == 'true' }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + max-size: 1.5G + key: ${{ runner.os }}-${{ runner.arch }}-xcode${{ env.HARNESS_XCODE_VERSION }}-ccache-harness-ios + create-symlink: true + + - name: Setup ccache behavior + run: | + echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV + echo "CCACHE_FILECLONE=true" >> $GITHUB_ENV + echo "CCACHE_DEPEND=true" >> $GITHUB_ENV + echo "CCACHE_INODECACHE=true" >> $GITHUB_ENV + + - name: Setup Ruby (bundle) + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7.2 + bundler-cache: true + working-directory: apps/simple-camera/ + + - name: Select Xcode ${{ env.HARNESS_XCODE_VERSION }} + run: sudo xcode-select -s "/Applications/Xcode_${{ env.HARNESS_XCODE_VERSION }}.app/Contents/Developer" + + - name: Restore CocoaPods cache + uses: actions/cache@v5 + with: + path: | + ~/Library/Caches/CocoaPods + apps/simple-camera/ios/Pods + key: ${{ runner.os }}-pods-${{ hashFiles('apps/simple-camera/ios/Podfile.lock', 'apps/simple-camera/package.json', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + + - name: Install Pods + working-directory: apps/simple-camera/ios + env: + RCT_USE_PREBUILT_RNCORE: "0" + run: bun pods + + - name: Restore DerivedData cache + id: cache-deriveddata-restore + uses: actions/cache/restore@v4 + with: + path: ${{ env.HARNESS_IOS_DERIVED_DATA_OUTPUT }} + key: ${{ runner.os }}-${{ runner.arch }}-xcode${{ env.HARNESS_XCODE_VERSION }}-dd-${{ hashFiles('bun.lock', 'apps/simple-camera/Gemfile.lock', 'apps/simple-camera/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-xcode${{ env.HARNESS_XCODE_VERSION }}-dd- + + - name: Build iOS app + run: | + set -euo pipefail + DERIVED_DATA="${{ env.HARNESS_IOS_DERIVED_DATA_OUTPUT }}" + IPA_PATH="${{ env.HARNESS_IOS_APP_BUILD_OUTPUT }}" + APP_PATH="${DERIVED_DATA}/Build/Products/Debug-iphoneos/SimpleCamera.app" + PAYLOAD_ROOT="$(mktemp -d)" + PAYLOAD_DIR="$PAYLOAD_ROOT/Payload" + + rm -f "$IPA_PATH" + mkdir -p "$DERIVED_DATA" "$(dirname "$IPA_PATH")" "$PAYLOAD_DIR" + + xcodebuild \ + CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ + -workspace apps/simple-camera/ios/SimpleCamera.xcworkspace \ + -scheme SimpleCamera \ + -configuration Debug \ + -sdk iphoneos \ + -destination generic/platform=iOS \ + -derivedDataPath "$DERIVED_DATA" \ + -showBuildTimingSummary \ + ONLY_ACTIVE_ARCH=YES \ + build \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY= \ + COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify --renderer github-actions + + test -d "$APP_PATH" + cp -R "$APP_PATH" "$PAYLOAD_DIR/" + + ( + cd "$PAYLOAD_ROOT" + zip -qry "$GITHUB_WORKSPACE/$IPA_PATH" Payload + ) + + - name: Save DerivedData cache + if: steps.cache-deriveddata-restore.outputs.cache-hit != 'true' && success() + uses: actions/cache/save@v4 + with: + path: ${{ env.HARNESS_IOS_DERIVED_DATA_OUTPUT }} + key: ${{ steps.cache-deriveddata-restore.outputs.cache-primary-key }} + + - name: Verify iOS app artifact + run: test -f ${{ env.HARNESS_IOS_APP_BUILD_OUTPUT }} diff --git a/.github/workflows/lint-cpp.yml b/.github/workflows/lint-cpp.yml new file mode 100644 index 0000000000..76cfdc5abe --- /dev/null +++ b/.github/workflows/lint-cpp.yml @@ -0,0 +1,36 @@ +name: Lint C++ + +on: + push: + branches: + - main + paths: + - '.github/workflows/lint-cpp.yml' + - '**/*.h' + - '**/*.hpp' + - '**/*.cpp' + - '**/*.c' + - '**/*.mm' + pull_request: + paths: + - '.github/workflows/lint-cpp.yml' + - '**/*.h' + - '**/*.hpp' + - '**/*.cpp' + - '**/*.c' + - '**/*.mm' + +jobs: + lint: + name: Format C++ + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + + - name: Run clang-format + run: bun run lint-cpp + + - name: Verify no files have changed after format + run: git diff --exit-code HEAD -- . ':(exclude)bun.lock' + diff --git a/.github/workflows/lint-kotlin.yml b/.github/workflows/lint-kotlin.yml new file mode 100644 index 0000000000..1b668d5960 --- /dev/null +++ b/.github/workflows/lint-kotlin.yml @@ -0,0 +1,34 @@ +name: Lint Kotlin + +on: + push: + branches: + - main + paths: + - '.github/workflows/lint-kotlin.yml' + - '**/*.kt' + pull_request: + paths: + - '.github/workflows/lint-kotlin.yml' + - '**/*.kt' + +jobs: + lint: + name: Format Kotlin + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + + - name: Install ktlint + run: | + curl -sSLO https://github.com/pinterest/ktlint/releases/latest/download/ktlint + chmod a+x ktlint + sudo mv ktlint /usr/local/bin/ + + - name: Run ktlint + run: bun run lint-kotlin + + - name: Verify no files have changed after format + run: git diff --exit-code HEAD -- . ':(exclude)bun.lock' + diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml new file mode 100644 index 0000000000..c62538866d --- /dev/null +++ b/.github/workflows/lint-swift.yml @@ -0,0 +1,28 @@ +name: Lint Swift + +on: + push: + branches: + - main + paths: + - '.github/workflows/lint-swift.yml' + - '**/*.swift' + pull_request: + paths: + - '.github/workflows/lint-swift.yml' + - '**/*.swift' + +jobs: + lint: + name: Format Swift + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + + - name: Run swift format + run: bun run lint-swift + + - name: Verify no files have changed after format + run: git diff --exit-code HEAD -- . ':(exclude)bun.lock' + diff --git a/.github/workflows/validate-android.yml b/.github/workflows/validate-android.yml deleted file mode 100644 index 4b6fc8a311..0000000000 --- a/.github/workflows/validate-android.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Validate Android - -on: - push: - branches: - - main - paths: - - '.github/workflows/validate-android.yml' - - 'package/android/**' - - 'package/android/.editorconfig' - pull_request: - paths: - - '.github/workflows/validate-android.yml' - - 'package/android/**' - - 'package/android/.editorconfig' - -jobs: - KTLint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Download ktlint - working-directory: ./package/android/ - run: | - curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.1.1/ktlint - chmod a+x ktlint - - name: Run ktlint - working-directory: ./package/android/ - run: | - ./ktlint --reporter=checkstyle,output=build/ktlint-report.xml --relative --editorconfig=./.editorconfig - continue-on-error: true - - uses: yutailang0119/action-ktlint@v4 - with: - report-path: ./package/android/build/*.xml - continue-on-error: false - - uses: actions/upload-artifact@v4 - with: - name: ktlint-report - path: ./package/android/build/*.xml diff --git a/.github/workflows/validate-cpp.yml b/.github/workflows/validate-cpp.yml deleted file mode 100644 index b73832a024..0000000000 --- a/.github/workflows/validate-cpp.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Validate C++ - -on: - push: - branches: - - main - paths: - - '.github/workflows/validate-cpp.yml' - - 'package/android/src/main/cpp/**' - - 'package/ios/**' - pull_request: - paths: - - '.github/workflows/validate-cpp.yml' - - 'package/android/src/main/cpp/**' - - 'package/ios/**' - -jobs: - lint: - name: Check clang-format - runs-on: ubuntu-latest - strategy: - matrix: - path: - - 'package/android/src/main/cpp' - - 'package/ios' - steps: - - uses: actions/checkout@v4 - - name: Run clang-format style check - uses: mrousavy/clang-format-action@v1 - with: - clang-format-version: '16' - check-path: ${{ matrix.path }} - clang-format-style-path: package/.clang-format - diff --git a/.github/workflows/validate-ios.yml b/.github/workflows/validate-ios.yml deleted file mode 100644 index e4870c7bd0..0000000000 --- a/.github/workflows/validate-ios.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Validate iOS - -on: - push: - branches: - - main - paths: - - '.github/workflows/validate-ios.yml' - - 'package/ios/**' - pull_request: - paths: - - '.github/workflows/validate-ios.yml' - - 'package/ios/**' - -jobs: - SwiftLint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./package - steps: - - uses: actions/checkout@v4 - - name: Run SwiftLint GitHub Action (--strict) - uses: norio-nomura/action-swiftlint@master - with: - args: --strict - env: - WORKING_DIRECTORY: ios - SwiftFormat: - runs-on: macOS-latest - defaults: - run: - working-directory: ./package/ios - steps: - - uses: actions/checkout@v4 - - - name: Install SwiftFormat - run: brew install swiftformat - - - name: Format Swift code - run: swiftformat --verbose . - - - name: Verify formatted code is unchanged - run: git diff --exit-code HEAD diff --git a/.github/workflows/validate-js.yml b/.github/workflows/validate-js.yml deleted file mode 100644 index fbd637c68a..0000000000 --- a/.github/workflows/validate-js.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Validate JS - -on: - push: - branches: - - main - paths: - - '.github/workflows/validate-js.yml' - - 'package/src/**' - - 'package/*.json' - - 'package/*.js' - - 'package/*.lock' - - 'package/example/src/**' - - 'package/example/*.json' - - 'package/example/*.js' - - 'package/example/*.lock' - - 'package/example/*.tsx' - pull_request: - paths: - - '.github/workflows/validate-js.yml' - - 'package/src/**' - - 'package/*.json' - - 'package/*.js' - - 'package/*.lock' - - 'package/example/src/**' - - 'package/example/*.json' - - 'package/example/*.js' - - 'package/example/*.lock' - - 'package/example/*.tsx' - -jobs: - compile: - name: Compile JS (tsc) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./package - steps: - - uses: actions/checkout@v4 - - - name: Install reviewdog - uses: reviewdog/action-setup@v1 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install node_modules - run: yarn install --frozen-lockfile - - name: Install node_modules (example/) - run: yarn install --frozen-lockfile --cwd example - - - name: Run TypeScript # Reviewdog tsc errorformat: %f:%l:%c - error TS%n: %m - run: | - yarn typescript | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-on-error -tee - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run TypeScript in example/ # Reviewdog tsc errorformat: %f:%l:%c - error TS%n: %m - run: | - cd example && yarn typescript | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-on-error -tee && cd .. - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - lint: - name: Lint JS (eslint, prettier) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./package - steps: - - uses: actions/checkout@v4 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Restore node_modules from cache - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install node_modules - run: yarn install --frozen-lockfile - - name: Install node_modules (example/) - run: yarn install --frozen-lockfile --cwd example - - - name: Run ESLint - run: yarn lint -f @jamesacarr/github-actions - - - name: Run ESLint with auto-fix - run: yarn lint --fix - - - name: Run ESLint in example/ - run: cd example && yarn lint -f @jamesacarr/github-actions && cd .. - - - name: Run ESLint in example/ with auto-fix - run: cd example && yarn lint --fix && cd .. - - - name: Verify no files have changed after auto-fix - run: git diff --exit-code HEAD diff --git a/.gitignore b/.gitignore index 8bbf73d843..263c2f24cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,36 @@ -.DS_Store -**/node_modules/ - -# no yarn/npm in the root repo! -/package-lock.json -/yarn.lock - -# when switching from v2 -> v3 branches -/example -/ios -/android -/lib +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +**/.DS_Store + +# Claude Code local settings +.claude/settings.local.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..0b89c715ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, + "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" } +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 96b64bce96..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by creating an issue on the GitHub repository. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 186de9f4fa..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contributing - -## Guidelines - -1. Don't be an asshole. -2. Don't waste anyone's time. - -## Get started - -1. Fork & clone the repository -2. Install dependencies - ``` - cd react-native-vision-camera - cd package - yarn bootstrap - ``` - -Read the READMEs in [`android/`](android/README.md) and [`ios/`](ios/README.md) for a quick overview of the native development workflow. - -> You can also open VisionCamera in [a quick online editor (github1s)](https://github1s.com/mrousavy/react-native-vision-camera) - -### JS/TS - -1. Open the entire folder in Visual Studio Code -2. Start the metro bundler in the `example/` directory using `yarn start` -3. Run either the iOS or Android project to test changes - -> Run `yarn check-js` to validate codestyle - -### iOS - -1. Open the `example/ios/VisionCameraExample.xcworkspace` file with Xcode -2. Change signing configuration to your developer account -3. Select your device in the devices drop-down -4. Hit run - -> Run `yarn check-ios` to validate codestyle - -### Android - -1. Open the `example/android/` folder with Android Studio -2. Start the metro bundler in the `example/` directory using `yarn start` -3. Select your device in the devices drop-down -4. Once your device is connected, make sure it can find the metro bundler's port: - ``` - adb reverse tcp:8081 tcp:8081 - ``` -6. Hit run - -> Run `yarn check-android` to validate codestyle - -### Docs - -1. Edit the relevant file, it may be easiest to search for what you're editing to find the right file -2. Install all dependencies by running `yarn` inside the `docs` folder - -> Run `yarn start` to generate the docs, you can then view them in your browser to confirm your changes - -## Committing - -### Codestyle - -Great code produces great products. That's why we love to keep our codebases clean, and to achieve that, we use linters and formatters which output errors when something isn't formatted the way we like it to be. - -Before pushing your changes, you can verify that everything is still correctly formatted by running all linters: - -``` -yarn check-all -``` - -This validates Swift, Kotlin, C++ and JS/TS code: - -```bash -$ yarn check-all - yarn run v1.22.10 - Formatting Swift code.. - Linting Swift code.. - Linting Kotlin code.. - Linting C++ code.. - Linting JS/TS code.. - All done! - ✨ Done in 8.05s. -``` - -### PR messages - -When creating a pull-request, make sure to use the [conventional changelog](https://github.com/conventional-changelog/conventional-changelog) format for it's title: - -* ✨ For new features or enhancements, use `feat`. Example: `feat: Support ultra-wide-angle cameras` -* 🐛 For bugfixes or build improvements, use `fix`. Example: `fix: Fix iOS 13 crash when switching cameras` -* 💨 For performance improvements, use `perf`. Example: `perf: Improve takePhoto() performance by re-using file-session` -* 🛠️ When upgrading dependencies, use `chore(deps)`. Example: `chore(deps): Upgrade react-native to 0.70` -* 📚 When changing anything in the documentation, use `docs`. Example: `docs: Fix typo in installation instructions` - -Pull-requests will be squash-committed, so no need to prefix your individual commits with the conventional changelog format as long as the commit messages are descriptive. diff --git a/LICENSE b/LICENSE index 90c2790293..b160bffffb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,20 @@ -Copyright 2021 Marc Rousavy +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2025 Marc Rousavy +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4df2faaa25..85d6ff1250 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ - - - VisionCamera + + + VisionCamera
- +
### Features @@ -21,12 +21,12 @@ VisionCamera is a powerful, high-performance Camera library for React Native. It * 📱 Customizable devices and multi-cameras ("fish-eye" zoom) * 🎞️ Customizable resolutions and aspect-ratios (4k/8k images) * ⏱️ Customizable FPS (30..240 FPS) -* 🧩 [Frame Processors](https://react-native-vision-camera.com/docs/guides/frame-processors) (JS worklets to run facial recognition, AI object detection, realtime video chats, ...) +* 🧩 [Frame Processors](https://visioncamera.margelo.com/docs/frame-output) (JS worklets to run facial recognition, AI object detection, realtime video chats, ...) * 🎨 Drawing shapes, text, filters or shaders onto the Camera * 🔍 Smooth zooming (Reanimated) * ⏯️ Fast pause and resume * 🌓 HDR & Night modes -* ⚡ Custom C++/GPU accelerated video pipeline (OpenGL) +* ⚡ Custom C++/GPU accelerated resizer (Metal/Vulkan) Install VisionCamera from npm: @@ -35,14 +35,19 @@ npm i react-native-vision-camera cd ios && pod install ``` -..and get started by [setting up permissions](https://react-native-vision-camera.com/docs/guides)! +..and get started by [setting up permissions](https://visioncamera.margelo.com/docs)! ### Documentation -* [Guides](https://react-native-vision-camera.com/docs/guides) -* [API](https://react-native-vision-camera.com/docs/api) -* [Example](./package/example/) -* [Frame Processor Plugins](https://react-native-vision-camera.com/docs/guides/frame-processor-plugins-community) +- [Documentation Website](https://visioncamera.margelo.com) +- [Documentation LLMs.txt](https://visioncamera.margelo.com/llms.txt) +- [Community Discord](https://margelo.com/discord) +- [Example App](./apps/simple-camera/) + +### VisionCamera V4 + +As VisionCamera V5 is released, VisionCamera V4 is no longer actively maintained. +The VisionCamera V4 code has been archived under [margelo/react-native-vision-camera-v4-snapshot](https://github.com/margelo/react-native-vision-camera-v4-snapshot), and the old documentation page is deployed at [visioncamera4.margelo.com](https://visioncamera4.margelo.com). ### ShadowLens @@ -50,10 +55,10 @@ To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/proje @@ -61,35 +66,25 @@ To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/proje ```tsx function App() { - const device = useCameraDevice('back') - - if (device == null) return return ( ) } ``` -> See the [example](./package/example/) app +> See the [example](./apps/simple-camera/) app ### Adopting at scale - - This library helped you? Consider sponsoring! - - -VisionCamera is provided _as is_, I work on it in my free time. - -If you're integrating VisionCamera in a production app, consider [funding this project](https://github.com/sponsors/mrousavy) and contact me to receive premium enterprise support, help with issues, prioritize bugfixes, request features, help at integrating VisionCamera and/or Frame Processors, and more. +VisionCamera is built by [Margelo](https://margelo.com). +We make apps better and faster. ### Socials * 🐦 [**Follow me on Twitter**](https://twitter.com/mrousavy) for updates * 📝 [**Check out my blog**](https://mrousavy.com/blog) for examples and experiments -* 💬 [**Join the Margelo Community Discord**](https://discord.gg/6CSHz2qAvA) for chatting about VisionCamera -* 💖 [**Sponsor me on GitHub**](https://github.com/sponsors/mrousavy) to support my work -* 🍪 [**Buy me a Ko-Fi**](https://ko-fi.com/mrousavy) to support my work +* 💬 [**Join the Margelo Community Discord**](https://margelo.com/discord) for chatting about VisionCamera diff --git a/apps/simple-camera/.bundle/config b/apps/simple-camera/.bundle/config new file mode 100644 index 0000000000..848943bb52 --- /dev/null +++ b/apps/simple-camera/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/apps/simple-camera/.gitignore b/apps/simple-camera/.gitignore new file mode 100644 index 0000000000..e59bd3f3a1 --- /dev/null +++ b/apps/simple-camera/.gitignore @@ -0,0 +1,78 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# react-native-harness (auto-generated manifest + crash dumps) +.harness/ diff --git a/apps/simple-camera/.watchmanconfig b/apps/simple-camera/.watchmanconfig new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/simple-camera/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/apps/simple-camera/Gemfile b/apps/simple-camera/Gemfile new file mode 100644 index 0000000000..40b4a3d0a4 --- /dev/null +++ b/apps/simple-camera/Gemfile @@ -0,0 +1,16 @@ +source 'https://rubygems.org' + +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 2.6.10" + +# Exclude problematic versions of cocoapods and activesupport that cause build failures. +gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' +gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' +gem 'xcodeproj', '< 1.26.0' +gem 'concurrent-ruby', '< 1.3.4' + +# Ruby 3.4.0 has removed some libraries from the standard library. +gem 'bigdecimal' +gem 'logger' +gem 'benchmark' +gem 'mutex_m' diff --git a/apps/simple-camera/Gemfile.lock b/apps/simple-camera/Gemfile.lock new file mode 100644 index 0000000000..00383a1267 --- /dev/null +++ b/apps/simple-camera/Gemfile.lock @@ -0,0 +1,116 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (6.1.7.10) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (3.3.1) + claide (1.1.0) + cocoapods (1.15.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.15.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.3) + escape (0.0.4) + ethon (0.15.0) + ffi (>= 1.15.0) + ffi (1.17.2) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.7.6) + logger (1.7.0) + minitest (5.25.4) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.3.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.2.0) + public_suffix (4.0.7) + rexml (3.4.4) + ruby-macho (2.5.1) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.25.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (>= 3.3.6, < 4.0) + zeitwerk (2.6.18) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport (>= 6.1.7.5, != 7.1.0) + benchmark + bigdecimal + cocoapods (>= 1.13, != 1.15.1, != 1.15.0) + concurrent-ruby (< 1.3.4) + logger + mutex_m + xcodeproj (< 1.26.0) + +RUBY VERSION + ruby 2.7.6p219 + +BUNDLED WITH + 2.3.22 diff --git a/apps/simple-camera/README.md b/apps/simple-camera/README.md new file mode 100644 index 0000000000..e2e5c9406a --- /dev/null +++ b/apps/simple-camera/README.md @@ -0,0 +1,97 @@ +This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). + +# Getting Started + +> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. + +## Step 1: Start Metro + +First, you will need to run **Metro**, the JavaScript build tool for React Native. + +To start the Metro dev server, run the following command from the root of your React Native project: + +```sh +# Using npm +npm start + +# OR using Yarn +yarn start +``` + +## Step 2: Build and run your app + +With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: + +### Android + +```sh +# Using npm +npm run android + +# OR using Yarn +yarn android +``` + +### iOS + +For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). + +The first time you create a new project, run the Ruby bundler to install CocoaPods itself: + +```sh +bundle install +``` + +Then, and every time you update your native dependencies, run: + +```sh +bundle exec pod install +``` + +For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). + +```sh +# Using npm +npm run ios + +# OR using Yarn +yarn ios +``` + +If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. + +This is one way to run your app — you can also build it directly from Android Studio or Xcode. + +## Step 3: Modify your app + +Now that you have successfully run the app, let's make changes! + +Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). + +When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: + +- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). +- **iOS**: Press R in iOS Simulator. + +## Congratulations! :tada: + +You've successfully run and modified your React Native App. :partying_face: + +### Now what? + +- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). +- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). + +# Troubleshooting + +If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. + +# Learn More + +To learn more about React Native, take a look at the following resources: + +- [React Native Website](https://reactnative.dev) - learn more about React Native. +- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how to set up your environment. +- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. +- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. +- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. diff --git a/apps/simple-camera/__tests__/README.md b/apps/simple-camera/__tests__/README.md new file mode 100644 index 0000000000..c88d1f924c --- /dev/null +++ b/apps/simple-camera/__tests__/README.md @@ -0,0 +1,273 @@ +# VisionCamera Harness Tests + +This folder contains the on-device test suite for the VisionCamera imperative +API. Tests run on a real phone (local `adb` device or an AWS Device Farm +device) through [react-native-harness], which embeds a Jest-compatible runner +in the `simple-camera` app and talks to it over a Metro-driven bridge. + +--- + +## Why these tests exist + +Two goals, in order: + +1. **Regressions in the public API surface fail CI automatically.** Every + feature of `VisionCamera` that this library supports on real hardware has a + test here. If a refactor breaks `capturePhoto`, the CI run turns red on the + next PR. +2. **Bug reports become executable.** Anyone who finds a bug is expected to + open a PR here that adds a single failing test reproducing the issue — + **not** a separate reproduction repo. The maintainer fixes the bug on the + same branch until the CI goes green, and the test is merged along with + the fix. That way the same bug can never regress silently again. + +**If you are reporting a bug:** open a PR that adds the smallest possible +`it(...)` block somewhere under this folder, aligned with the rules below. +Then open the issue referencing the PR — the CI run on the PR is the +reproduction. You do **not** need to create a separate repo. + +--- + +## Layout + +Tests are split by domain. Each file tests one slice of the imperative +`VisionCamera` API: + +| File | Covers | +|------|--------| +| [visioncamera.devices.harness.ts](visioncamera.devices.harness.ts) | `VisionCamera.createDeviceFactory`, device enumeration, per-device capabilities, `getCameraForId`, `addOnCameraDevicesChangedListener`, `getSupportedExtensions`, `userPreferredCamera` | +| [visioncamera.session.harness.ts](visioncamera.session.harness.ts) | `createCameraSession`, `configure`, `start`, `stop`, `addOnStartedListener` / `addOnStoppedListener` / `addOnErrorListener` / interruption listeners, reconfigure-while-running, multi-cam | +| [visioncamera.photo.harness.ts](visioncamera.photo.harness.ts) | `createPhotoOutput`, `capturePhoto` / `capturePhotoToFile`, container formats (JPEG, HEIC, DNG), flash / mirror / quality / resolution options, capture lifecycle callbacks, preview images | +| [visioncamera.video.harness.ts](visioncamera.video.harness.ts) | `createVideoOutput`, `Recorder` lifecycle, audio, `maxDuration` / `maxFileSize` stops, pause / resume / cancel, persistent recorder, higher-resolution codecs | +| [visioncamera.frame.harness.ts](visioncamera.frame.harness.ts) | `createFrameOutput`, worklet install via `react-native-vision-camera-worklets`, YUV / RGB / native pixel formats, `scheduleOnRN`, `createSynchronizable`, `setOnFrameDroppedCallback`, `enablePreviewSizedOutputBuffers` | +| [visioncamera.constraints.harness.ts](visioncamera.constraints.harness.ts) | `VisionCamera.resolveConstraints` + `onSessionConfigSelected`, FPS / HDR / stabilization / binned / pixelFormat / resolutionBias constraints | +| [visioncamera.controller.harness.ts](visioncamera.controller.harness.ts) | `CameraController` — zoom, torch, exposure bias, focus metering, low-light boost, subject area listener | + +Pick the file that best matches what you're testing. If you're reproducing a +bug that spans multiple outputs, put it in the file most central to the +failure. If nothing fits, open a new `visioncamera..harness.ts` — +Jest picks up anything matching `__tests__/**/*.harness.{ts,tsx}`. + +--- + +## How a test is written + +The contract is deliberately strict so that the tests read exactly like +VisionCamera user code — contributors and LLMs should be able to drop in a +reproduction without having to learn framework-specific helpers. + +### 1. Use the `VisionCamera` API as-is. **No helpers.** + +Every test builds up its session inline, end-to-end, from `VisionCamera` up. +Do **not** extract helpers like `createSession()` or `configureAndStart()` — +the API should read in tests exactly as users would write it in their app. + +```ts +it('captures a JPEG Photo in-memory', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.FHD_4_3, + containerFormat: 'jpeg', + quality: 0.9, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + expect(photo.containerFormat).toBe('jpeg') + photo.dispose() + + await session.stop() +}) +``` + +**`beforeAll` may cache trivial API results** (e.g. the `CameraDeviceFactory` +and the default back / front `CameraDevice`). It must not wrap any camera +session setup — every `it` block gets its own `session`, `photoOutput`, etc. + +### 2. Hard vs. soft requirements + +Cameras differ. A failing hard requirement is a real bug; a missing soft +feature is a device limitation and should not fail the test. + +- **Hard requirement** — checked with `expect(...)`, makes the test fail. + Examples: a back camera exists; a photo output produces a photo with + `width > 0`; `session.configure` returns one controller per connection. +- **Soft requirement** — gated by the matching capability flag and a + `console.log('[SKIP] : ')` early-return when not supported. + The skip log is deliberately visible in CI so we can see what the current + test device can't cover and pick a different device if needed. + +```ts +if (!backDevice.supportsPhotoHDR) { + console.log('[SKIP] photoHDR: not supported on this device') + return +} +// hard-assert HDR behavior from here on +``` + +Capability flags live on `CameraDevice` (`hasFlash`, `hasTorch`, +`supportsFocusMetering`, `supportsExposureBias`, `supportsPhotoHDR`, +`supportsFPS(n)`, `supportsVideoStabilizationMode('cinematic')`, etc.) and on +`VisionCamera` (`supportsMultiCamSessions`). Use them. Do **not** introduce +ad-hoc try/catch wrappers around an operation just to silently skip it — if +there is no way to query support upfront, flag that as a missing API (see +"Known API gaps" below) and `it.skip` the test with a TODO explaining what +would let you turn it into a hard requirement. + +### 3. Test behavior, not types + +Nitro Modules enforce types at the bridge already. Skip `typeof x === 'number'` +or `Array.isArray(devices)` assertions — they only add noise. Assert things +that require the device actually doing camera work. + +### 4. Prefer callbacks over polled state + +`session.isRunning` updates asynchronously on Android. Wait for +`session.addOnStartedListener(...)` and `addOnStoppedListener(...)` using +`waitUntil(() => started, { timeout: 10_000 })` instead of polling `isRunning` +in a sleep loop. + +### 5. Don't silently swallow errors + +No `.catch(() => undefined)` around otherwise-expected-to-succeed calls. If +`session.stop()` can throw, the test should fail — that's a regression. Only +add a `try { ... } catch { console.log('[SKIP] ...') }` when you genuinely +cannot gate the code path any other way (see the HEIC / DNG photo tests — no +API exposes which container formats the device supports). + +### 6. Dispose only when it matters + +`Photo`, `Frame`, and `Image` hold large native buffers — call `.dispose()` +as soon as you're done with them. You do **not** need to dispose +`CameraSession`, `CameraController`, or outputs in tests; the JS runtime GC +frees them between tests. + +### 7. No artificial `setTimeout` delays + +Tests must only wait on events they actually depend on +(`session.addOnStartedListener`, `onRecordingFinished`, a frame counter, a +`CompletableDeferred`). Sleeping a random number of milliseconds "so the +camera settles" introduces flakiness and masks real regressions. If you +catch yourself writing `await sleep(500)` to "make it work", treat it as a +bug to fix, not a patch to keep. + +### 8. Platform guards + +iOS-only features (`setFocusLocked`, `setExposureLocked`, `continuity +camera`, `getSupportedVideoCodecs`, etc.) and Android-only features +(`enableHigherResolutionCodecs`) should start with a +`if (Platform.OS !== 'ios') { console.log('[SKIP] ...: iOS only'); return }` +guard. Do not branch on `Platform.OS` to mask behavioral differences that +should be identical across platforms — flag those as bugs. + +--- + +## Running the tests + +```sh +# Build the debug APK once +cd apps/simple-camera && bun run build:android + +# Install + grant camera / microphone / location permissions +adb install -r android/app/build/outputs/apk/debug/app-debug.apk +BUNDLE_ID=com.margelo.nitro.camera.example.simple +adb shell pm grant $BUNDLE_ID android.permission.CAMERA +adb shell pm grant $BUNDLE_ID android.permission.RECORD_AUDIO +adb shell pm grant $BUNDLE_ID android.permission.ACCESS_FINE_LOCATION +adb shell pm grant $BUNDLE_ID android.permission.ACCESS_COARSE_LOCATION + +# Run the full harness suite against the connected device +HARNESS_ANDROID_DEVICE_MANUFACTURER= \ +HARNESS_ANDROID_DEVICE_MODEL= \ +bun run test:harness:android + +# Or just one file +HARNESS_ANDROID_DEVICE_MANUFACTURER= \ +HARNESS_ANDROID_DEVICE_MODEL= \ +bun run test:harness:android -- --testPathPatterns=photo +``` + +`HARNESS_ANDROID_DEVICE_MANUFACTURER` / `HARNESS_ANDROID_DEVICE_MODEL` come +from `adb shell getprop ro.product.manufacturer` / `ro.product.model`. On AWS +Device Farm they're set automatically by the workflow. + +Permissions are granted once per install. If you reinstall the APK with +`adb install -r`, re-run the `pm grant` lines before the next test run — +otherwise the first test's `expect(cameraPermissionStatus).toBe('authorized')` +will fail. + +The `.harness/` folder is auto-generated by the harness bundler and is +gitignored. You can safely delete it. + +--- + +## Known API gaps / currently-skipped tests + +A few tests are authored but `it.skip`'d because the VisionCamera API +doesn't yet expose the precondition they need. Each skip has a `TODO` in the +file pointing at what needs to land first. Today: + +- **Photo container format support** — HEIC and DNG capture work on some + devices and fail on others, but there is no `CameraDevice.supportedPhotoContainerFormats` + today. These tests are `it.skip` with a TODO until the API lands. Once it + exists the tests become soft-requirements gated on the flag. +- **`initialZoom` / `initialExposureBias` on Android** — `applyInitialConfig` + runs at `configure()` time, before CameraX's LifecycleOwner reaches STARTED. + `CameraControl.setExposureCompensationIndex` silently fails in that state. + The corresponding tests are `it.skip` until the initial config application + happens at a point where CameraX accepts it. +- **`enablePreviewSizedOutputBuffers` on Android** — the flag is not honored + by `HybridFrameOutput.kt` today (`TODO: enablePreviewSizedOutputBuffers is + not taken into account here.`). +- **`onFrameDropped` on Android** — `HybridFrameOutput.setOnFrameDroppedCallback` + is a no-op (`TODO: CameraX does not have a way to figure out if a Frame + has been dropped or not.`). + +If you hit another case where you can't write a test because an API is +missing, add the test with `it.skip` and a TODO explaining the precondition +— that way when the API lands we already know which tests to flip back on. + +--- + +## CI + +Harness tests run on every push and PR that touches this folder, the +VisionCamera library, or the harness workflow config — see +[.github/workflows/harness-aws-device.yml](../../../.github/workflows/harness-aws-device.yml) +and [.github/workflows/harness-android-emulator.yml](../../../.github/workflows/harness-android-emulator.yml). + +The AWS Device Farm run is the source of truth: it's a real phone, a real +SoC, a real camera pipeline. The emulator run is best-effort and may skip +hardware-dependent tests. + +If your PR fails CI, the fastest way to debug is: + +1. Download the `harness output log` artifact from the failed workflow run. + It contains the full JS console output per test. +2. Grep for `[SKIP]` to see which soft requirements were skipped — that + tells you what your test device lacks. +3. Grep for `FAIL` to see which `it` blocks failed and their stack traces. +4. Open the JUnit XML artifact in your IDE's JUnit viewer for a structured + summary. + +--- + +## High-level components / hooks (``, `useCamera()`, etc.) + +Tests for higher-level React components and hooks are **not yet in scope +here**. They'll live alongside the imperative tests when added — same +principles, same folder. + +[react-native-harness]: https://github.com/margelo/react-native-harness diff --git a/apps/simple-camera/__tests__/visioncamera.constraints.harness.ts b/apps/simple-camera/__tests__/visioncamera.constraints.harness.ts new file mode 100644 index 0000000000..90addfc8e4 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.constraints.harness.ts @@ -0,0 +1,368 @@ +import { + beforeAll, + describe, + expect, + it, + waitUntil, +} from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, + CameraSessionConfig, + Constraint, +} from 'react-native-vision-camera' +import { + CommonDynamicRanges, + CommonResolutions, + VisionCamera, +} from 'react-native-vision-camera' + +describe('VisionCamera - Constraints', () => { + let factory: CameraDeviceFactory + let backDevice: CameraDevice + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + const back = factory.getDefaultCamera('back') + expect(back).toBeDefined() + if (back == null) throw new Error('no back camera') + backDevice = back + }) + + it('resolves a baseline config with no constraints', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + + const config = received + if (config == null) throw new Error('no config') + expect(backDevice.supportedPixelFormats).toContain(config.nativePixelFormat) + console.log(`baseline config: ${config.toString()}`) + await session.stop() + }) + + it('resolves an explicit fps: 30 constraint', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [{ fps: 30 }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.selectedFPS).toBe(30) + await session.stop() + }) + + it('resolves a fps: 60 constraint if the device supports it', async () => { + if (!backDevice.supportsFPS(60)) { + console.log('[SKIP] fps: 60 not supported on this device') + return + } + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [{ fps: 60 }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.selectedFPS).toBe(60) + await session.stop() + }) + + it('resolves photoHDR: true when the device supports photo HDR', async () => { + if (!backDevice.supportsPhotoHDR) { + console.log('[SKIP] photoHDR: not supported on this device') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [{ resolutionBias: photoOutput }, { photoHDR: true }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.isPhotoHDREnabled).toBe(true) + await session.stop() + }) + + it('resolves a HDR video dynamic range when the device supports it', async () => { + const hasHdr = backDevice.supportedVideoDynamicRanges.some( + (d) => d.bitDepth === 'hdr-10-bit', + ) + if (!hasHdr) { + console.log('[SKIP] video HDR: no HDR dynamic range on this device') + return + } + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [ + { videoDynamicRange: CommonDynamicRanges.ANY_HDR }, + { resolutionBias: videoOutput }, + ], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.selectedVideoDynamicRange?.bitDepth).toBe('hdr-10-bit') + await session.stop() + }) + + it('resolves a video stabilization constraint when supported', async () => { + if (!backDevice.supportsVideoStabilizationMode('cinematic')) { + console.log('[SKIP] videoStabilizationMode: "cinematic" not supported') + return + } + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [{ videoStabilizationMode: 'cinematic' }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.selectedVideoStabilizationMode).toBe('cinematic') + await session.stop() + }) + + it('resolves a preview stabilization constraint when supported', async () => { + if (!backDevice.supportsPreviewStabilizationMode('preview-optimized')) { + console.log( + '[SKIP] previewStabilizationMode: "preview-optimized" not supported', + ) + return + } + const session = await VisionCamera.createCameraSession(false) + const previewOutput = VisionCamera.createPreviewOutput() + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: previewOutput, mirrorMode: 'auto' }], + constraints: [{ previewStabilizationMode: 'preview-optimized' }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + expect(received?.selectedPreviewStabilizationMode).toBe('preview-optimized') + await session.stop() + }) + + it('resolves a binned: true constraint when supported', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [{ binned: true }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + if (received?.isBinned !== true) { + console.log( + `[SKIP] binned: true: device resolved to isBinned=${received?.isBinned}`, + ) + await session.stop() + return + } + expect(received.isBinned).toBe(true) + await session.stop() + }) + + it('honors a pixelFormat constraint matching a supported device format', async () => { + const candidate = backDevice.supportedPixelFormats.find( + (f) => f !== 'unknown', + ) + if (candidate == null) { + console.log( + '[SKIP] pixelFormat constraint: device has no non-unknown formats', + ) + return + } + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.HD_16_9, + pixelFormat: 'native', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + let received: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [{ pixelFormat: candidate }], + onSessionConfigSelected: (config) => { + received = config + }, + }, + ]) + await waitUntil(() => received != null, { timeout: 5_000 }) + console.log( + `requested pixelFormat=${candidate} resolved=${received?.nativePixelFormat}`, + ) + expect(received?.nativePixelFormat).toBe(candidate) + await session.stop() + }) + + it('resolves the same config via VisionCamera.resolveConstraints and session.configure', async () => { + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const outputConfig = { + output: photoOutput, + mirrorMode: 'auto' as const, + } + const constraints: Constraint[] = [{ fps: 30 }] + + const standalone = await VisionCamera.resolveConstraints( + backDevice, + [outputConfig], + constraints, + ) + + const session = await VisionCamera.createCameraSession(false) + let sessionConfig: CameraSessionConfig | undefined + await session.configure([ + { + input: backDevice, + outputs: [outputConfig], + constraints, + onSessionConfigSelected: (config) => { + sessionConfig = config + }, + }, + ]) + await waitUntil(() => sessionConfig != null, { timeout: 5_000 }) + expect(sessionConfig?.selectedFPS).toBe(standalone.selectedFPS) + expect(sessionConfig?.nativePixelFormat).toBe(standalone.nativePixelFormat) + expect(sessionConfig?.isPhotoHDREnabled).toBe(standalone.isPhotoHDREnabled) + expect(sessionConfig?.isBinned).toBe(standalone.isBinned) + + await session.stop() + }) + + it('applies constraint priority ordering for resolutionBias', async () => { + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HIGHEST_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.VGA_16_9, + enableAudio: false, + }) + + const photoFirst = await VisionCamera.resolveConstraints( + backDevice, + [ + { output: photoOutput, mirrorMode: 'auto' }, + { output: videoOutput, mirrorMode: 'auto' }, + ], + [{ resolutionBias: photoOutput }, { resolutionBias: videoOutput }], + ) + const videoFirst = await VisionCamera.resolveConstraints( + backDevice, + [ + { output: photoOutput, mirrorMode: 'auto' }, + { output: videoOutput, mirrorMode: 'auto' }, + ], + [{ resolutionBias: videoOutput }, { resolutionBias: photoOutput }], + ) + console.log( + `resolutionBias photo-first: nativePixelFormat=${photoFirst.nativePixelFormat} binned=${photoFirst.isBinned}`, + ) + console.log( + `resolutionBias video-first: nativePixelFormat=${videoFirst.nativePixelFormat} binned=${videoFirst.isBinned}`, + ) + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.controller.harness.ts b/apps/simple-camera/__tests__/visioncamera.controller.harness.ts new file mode 100644 index 0000000000..dd17e172ae --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.controller.harness.ts @@ -0,0 +1,350 @@ +import { beforeAll, describe, expect, it } from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, +} from 'react-native-vision-camera' +import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' + +describe('VisionCamera - Controller', () => { + let factory: CameraDeviceFactory + let backDevice: CameraDevice + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + const back = factory.getDefaultCamera('back') + expect(back).toBeDefined() + if (back == null) throw new Error('no back camera') + backDevice = back + }) + + it('sets zoom to min, max, and mid values', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + await controller.setZoom(controller.minZoom) + expect(controller.zoom).toBe(controller.minZoom) + + await controller.setZoom(controller.maxZoom) + expect(controller.zoom).toBe(controller.maxZoom) + + const mid = (controller.minZoom + controller.maxZoom) / 2 + await controller.setZoom(mid) + expect(controller.zoom).toBeGreaterThanOrEqual(controller.minZoom) + expect(controller.zoom).toBeLessThanOrEqual(controller.maxZoom) + } finally { + await session.stop() + } + }) + + it('starts and cancels a zoom animation', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + await controller.setZoom(controller.minZoom) + controller.startZoomAnimation(controller.maxZoom, 0.5).catch(() => { + // expected when we cancel the animation + }) + await new Promise((resolve) => setTimeout(resolve, 100)) + await controller.cancelZoomAnimation() + } finally { + await session.stop() + } + }) + + it('honors initialZoom passed to configure', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const desiredZoom = Math.min( + Math.max(backDevice.minZoom, 1.5), + backDevice.maxZoom, + ) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + initialZoom: desiredZoom, + }, + ]) + await session.start() + try { + expect(controller?.zoom).toBe(desiredZoom) + } finally { + await session.stop() + } + }) + + it('sets torchMode on/off when the device has a torch', async () => { + if (!backDevice.hasTorch) { + console.log('[SKIP] torch: device has no torch') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + // TODO: Add setTorchMode('on', STRENGTH) test when we expose something like + // CameraDevice.supportsTorchStrength - currently this might throw on + // some phones without a way to check upfront if it supports setting strength! + await controller.setTorchMode('on') + expect(controller.torchMode).toBe('on') + + await controller.setTorchMode('off') + expect(controller.torchMode).toBe('off') + } finally { + await session.stop() + } + }) + + it('sets exposure bias to min/max when the device supports it', async () => { + if (!backDevice.supportsExposureBias) { + console.log('[SKIP] exposureBias: not supported on this device') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + await controller.setExposureBias(backDevice.maxExposureBias) + expect(controller.exposureBias).toBe(backDevice.maxExposureBias) + + await controller.setExposureBias(backDevice.minExposureBias) + expect(controller.exposureBias).toBe(backDevice.minExposureBias) + + await controller.setExposureBias(0) + expect(controller.exposureBias).toBe(0) + } finally { + await session.stop() + } + }) + + // TODO: Re-enable this test once Android applies `initialExposureBias` after the + // camera is active. Today HybridCameraSession.applyInitialConfig fires + // setExposureCompensationIndex at `configure()` time — before the + // LifecycleOwner is active — so CameraX rejects it with + // "Camera is not active" and the value is never applied. + // (initialZoom happens to survive because setZoomRatio is applied as local + // state before the camera streams.) + it.skip('honors initialExposureBias passed to configure', async () => { + if (!backDevice.supportsExposureBias) { + console.log( + '[SKIP] initialExposureBias: device does not support exposure bias', + ) + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const initial = backDevice.maxExposureBias + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + initialExposureBias: initial, + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + expect(controller.exposureBias).toBe(initial) + } finally { + await session.stop() + } + }) + + it('runs focusTo and resetFocus when the device supports focus metering', async () => { + if (!backDevice.supportsFocusMetering) { + console.log('[SKIP] focusTo: device does not support focus metering') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + const point = VisionCamera.createNormalizedMeteringPoint(0.5, 0.5) + await controller.focusTo(point, { + modes: ['AF'], + responsiveness: 'snappy', + }) + await controller.resetFocus() + } finally { + await session.stop() + } + }) + + it('enables low-light boost via CameraController.configure when supported', async () => { + if (!backDevice.supportsLowLightBoost) { + console.log('[SKIP] low-light boost: not supported on this device') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + await controller.configure({ enableLowLightBoost: true }) + expect(controller.isLowLightBoostEnabled).toBe(true) + await controller.configure({ enableLowLightBoost: false }) + expect(controller.isLowLightBoostEnabled).toBe(false) + } finally { + await session.stop() + } + }) + + it('exposes sane controller state while running', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + expect(controller.device.id).toBe(backDevice.id) + expect(controller.minZoom).toBeLessThanOrEqual(controller.maxZoom) + expect(controller.zoom).toBeGreaterThanOrEqual(controller.minZoom) + expect(controller.zoom).toBeLessThanOrEqual(controller.maxZoom) + console.log( + `controller: zoom=${controller.zoom} displayable=${controller.displayableZoomFactor} ` + + `focusMode=${controller.focusMode} exposureMode=${controller.exposureMode} wbMode=${controller.whiteBalanceMode} ` + + `isConnected=${controller.isConnected}`, + ) + } finally { + await session.stop() + } + }) + + it('registers a subject area changed listener without throwing', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + const subscription = controller.addSubjectAreaChangedListener(() => {}) + expect(subscription.remove).toBeDefined() + subscription.remove() + } finally { + await session.stop() + } + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts new file mode 100644 index 0000000000..8d1d82d885 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts @@ -0,0 +1,206 @@ +import { beforeAll, describe, expect, it } from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, +} from 'react-native-vision-camera' +import { VisionCamera } from 'react-native-vision-camera' + +describe('VisionCamera - Devices', () => { + let factory: CameraDeviceFactory + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + }) + + it('enumerates at least one back and one front camera', () => { + const back = factory.getDefaultCamera('back') + const front = factory.getDefaultCamera('front') + expect(back).toBeDefined() + expect(front).toBeDefined() + + const hasBack = factory.cameraDevices.some((d) => d.position === 'back') + const hasFront = factory.cameraDevices.some((d) => d.position === 'front') + expect(hasBack).toBe(true) + expect(hasFront).toBe(true) + }) + + it('logs external cameras when present (optional)', () => { + const external = factory.cameraDevices.filter( + (d) => d.position === 'external', + ) + if (external.length === 0) { + console.log('[SKIP] external cameras: none available on this device') + return + } + for (const device of external) { + console.log( + `external camera: id=${device.id} name=${device.localizedName}`, + ) + } + }) + + it('returns the same device when calling getCameraForId with a known id', () => { + const first = factory.cameraDevices[0] + expect(first).toBeDefined() + if (first == null) return + + const looked = factory.getCameraForId(first.id) + expect(looked).toBeDefined() + expect(looked?.id).toBe(first.id) + }) + + it('returns undefined when calling getCameraForId with an unknown id', () => { + const looked = factory.getCameraForId( + `definitely-not-a-real-camera-id-${Date.now()}`, + ) + expect(looked).toBe(undefined) + }) + + it('returns an array from getSupportedExtensions for the default back camera', async () => { + const device = factory.getDefaultCamera('back') + if (device == null) { + console.log('[SKIP] getSupportedExtensions: no back device') + return + } + const extensions = await factory.getSupportedExtensions(device) + console.log( + `back camera extensions: ${extensions.map((e) => e.type).join(', ') || '(none)'}`, + ) + }) + + it('subscribes and unsubscribes a devices-changed listener', () => { + const subscription = factory.addOnCameraDevicesChangedListener(() => {}) + expect(subscription.remove).toBeDefined() + subscription.remove() + subscription.remove() + }) + + it('reports sane capability invariants for each device', () => { + for (const device of factory.cameraDevices) { + const label = `${device.position}:${device.id}` + + expect(device.minZoom).toBeLessThanOrEqual(device.maxZoom) + + if (device.supportsExposureBias) { + expect(device.minExposureBias).toBeLessThanOrEqual( + device.maxExposureBias, + ) + } + + if (device.mediaTypes.includes('video')) { + expect(device.supportedPixelFormats.length).toBeGreaterThan(0) + expect(device.supportedFPSRanges.length).toBeGreaterThan(0) + for (const range of device.supportedFPSRanges) { + expect(range.min).toBeLessThanOrEqual(range.max) + } + } + + console.log( + `device ${label}: type=${device.type} virtual=${device.isVirtualDevice} ` + + `zoom=${device.minZoom}-${device.maxZoom} fpsRanges=${device.supportedFPSRanges + .map((r) => `${r.min}-${r.max}`) + .join(',')}`, + ) + } + }) + + it('logs optional hardware capabilities per device', () => { + const capabilities: Array<{ + device: CameraDevice + caps: Record + }> = factory.cameraDevices.map((device) => ({ + device, + caps: { + hasFlash: device.hasFlash, + hasTorch: device.hasTorch, + supportsPhotoHDR: device.supportsPhotoHDR, + supportsLowLightBoost: device.supportsLowLightBoost, + supportsFocusMetering: device.supportsFocusMetering, + supportsExposureBias: device.supportsExposureBias, + supports60fps: device.supportsFPS(60), + supports120fps: device.supportsFPS(120), + supportsCinematicStab: + device.supportsVideoStabilizationMode('cinematic'), + supportsPreviewImage: device.supportsPreviewImage, + hdrRanges: device.supportedVideoDynamicRanges.length, + }, + })) + + for (const { device, caps } of capabilities) { + console.log( + `caps ${device.position}:${device.id}: ${JSON.stringify(caps)}`, + ) + } + }) + + it('returns non-empty getSupportedResolutions for photo/video streams on a back device', () => { + const device = factory.getDefaultCamera('back') + if (device == null) return + const photoResolutions = device.getSupportedResolutions('photo') + const videoResolutions = device.getSupportedResolutions('video') + expect(photoResolutions.length).toBeGreaterThan(0) + expect(videoResolutions.length).toBeGreaterThan(0) + }) + + it('gets and sets userPreferredCamera', () => { + const back = factory.getDefaultCamera('back') + if (back == null) return + const previous = factory.userPreferredCamera + factory.userPreferredCamera = back + expect(factory.userPreferredCamera?.id).toBe(back.id) + factory.userPreferredCamera = previous + }) + + it('returns an empty supportedMultiCamDeviceCombinations array when the platform does not support multi-cam', () => { + if (VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations empty: platform supports multi-cam', + ) + return + } + expect(factory.supportedMultiCamDeviceCombinations.length).toBe(0) + }) + + it('returns at least one supportedMultiCamDeviceCombinations combination when the platform supports multi-cam', () => { + if (!VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations non-empty: multi-cam not supported on this platform', + ) + return + } + expect( + factory.supportedMultiCamDeviceCombinations.length, + ).toBeGreaterThanOrEqual(1) + }) + + it('every device in a supportedMultiCamDeviceCombinations combination is also present in cameraDevices', () => { + const combinations = factory.supportedMultiCamDeviceCombinations + if (combinations.length === 0) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations device lookup: no combinations on this platform', + ) + return + } + const knownIds = new Set(factory.cameraDevices.map((d) => d.id)) + for (const combination of combinations) { + expect(combination.length).toBeGreaterThan(0) + for (const device of combination) { + expect(knownIds.has(device.id)).toBe(true) + } + } + }) + + it('logs every supported multi-cam device combination', () => { + const combinations = factory.supportedMultiCamDeviceCombinations + console.log( + `supportedMultiCamDeviceCombinations: ${combinations.length} combinations`, + ) + for (const [index, combination] of combinations.entries()) { + const description = combination + .map((d) => `${d.position}:${d.id}`) + .join(', ') + console.log(` [${index}] ${description}`) + } + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.frame.harness.ts b/apps/simple-camera/__tests__/visioncamera.frame.harness.ts new file mode 100644 index 0000000000..2cd90cbaa7 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.frame.harness.ts @@ -0,0 +1,313 @@ +import { + beforeAll, + describe, + expect, + it, + waitUntil, +} from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, + FrameDroppedReason, +} from 'react-native-vision-camera' +import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' +import { provider as workletsProvider } from 'react-native-vision-camera-worklets' +import { createSynchronizable, scheduleOnRN } from 'react-native-worklets' + +describe('VisionCamera - Frame', () => { + let factory: CameraDeviceFactory + let backDevice: CameraDevice + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + const back = factory.getDefaultCamera('back') + expect(back).toBeDefined() + if (back == null) throw new Error('no back camera') + backDevice = back + }) + + it('delivers frames to a worklet and posts back via scheduleOnRN', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.HD_16_9, + pixelFormat: 'native', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + let framesReceived = 0 + const onFrameReceived = () => { + framesReceived++ + } + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + scheduleOnRN(onFrameReceived) + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => framesReceived >= 3, { timeout: 15_000 }) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + await session.stop() + } + expect(framesReceived).toBeGreaterThanOrEqual(3) + }) + + it('delivers YUV frames with planar access when streaming in yuv', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.HD_16_9, + pixelFormat: 'yuv', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + let reportedWidth = 0 + let reportedHeight = 0 + let reportedPlanes = -1 + const report = (w: number, h: number, planes: number) => { + reportedWidth = w + reportedHeight = h + reportedPlanes = planes + } + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + const w = frame.width + const h = frame.height + let planeCount = 0 + if (frame.isPlanar) { + const planes = frame.getPlanes() + planeCount = planes.length + } + scheduleOnRN(report, w, h, planeCount) + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => reportedWidth > 0 && reportedHeight > 0, { + timeout: 15_000, + }) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + await session.stop() + } + console.log( + `yuv frame reported ${reportedWidth}x${reportedHeight} planes=${reportedPlanes}`, + ) + expect(reportedWidth).toBeGreaterThan(0) + expect(reportedHeight).toBeGreaterThan(0) + expect(reportedPlanes).toBeGreaterThanOrEqual(1) + }) + + it('delivers frames when streaming in rgb', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.VGA_16_9, + pixelFormat: 'rgb', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + let frameCount = 0 + const onFrame = () => { + frameCount++ + } + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + scheduleOnRN(onFrame) + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => frameCount >= 1, { timeout: 15_000 }) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + await session.stop() + } + }) + + it('synchronizes state from the worklet using createSynchronizable', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.VGA_16_9, + pixelFormat: 'native', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + const counter = createSynchronizable(0) + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + counter.setBlocking((prev) => prev + 1) + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => counter.getBlocking() >= 3, { timeout: 15_000 }) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + await session.stop() + } + expect(counter.getBlocking()).toBeGreaterThanOrEqual(3) + }) + + // TODO: Re-enable this test once the Android CameraX ImageAnalysis pipeline surfaces + // dropped-frame notifications. Today HybridFrameOutput.setOnFrameDroppedCallback + // is a no-op on Android (see the `TODO: CameraX does not have a way to figure + // out if a Frame has been dropped` comment in HybridFrameOutput.kt). + it.skip('invokes the onFrameDropped callback when the worklet stalls', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.HD_16_9, + pixelFormat: 'native', + enablePreviewSizedOutputBuffers: false, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [{ fps: 30 }], + }, + ]) + + let droppedReason: FrameDroppedReason | undefined + frameOutput.setOnFrameDroppedCallback((reason) => { + droppedReason = reason + }) + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + const start = Date.now() + // Deliberately stall so subsequent frames are dropped. + while (Date.now() - start < 150) { + // busy wait + } + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => droppedReason != null, { timeout: 15_000 }) + console.log(`frame dropped reason: ${droppedReason}`) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + frameOutput.setOnFrameDroppedCallback(undefined) + await session.stop() + } + }) + + // TODO: Re-enable once the Android frame output honors `enablePreviewSizedOutputBuffers` + // (today HybridFrameOutput.kt / HybridDepthFrameOutput.kt both have a + // `TODO: enablePreviewSizedOutputBuffers is not taken into account here.`). + // Actually, maybe we should remoev `enablePreviewSizedOutputBuffers` in favor of + // the simple, yet more flexible `targetResolution: ...` prop anyways. + it.skip('delivers smaller buffers when enablePreviewSizedOutputBuffers is true', async () => { + const session = await VisionCamera.createCameraSession(false) + const frameOutput = VisionCamera.createFrameOutput({ + targetResolution: CommonResolutions.UHD_16_9, + pixelFormat: 'native', + enablePreviewSizedOutputBuffers: true, + enablePhysicalBufferRotation: false, + enableCameraMatrixDelivery: false, + allowDeferredStart: false, + dropFramesWhileBusy: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: frameOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + let reportedWidth = 0 + let reportedHeight = 0 + const report = (w: number, h: number) => { + reportedWidth = w + reportedHeight = h + } + + const runtime = workletsProvider.createRuntimeForThread(frameOutput.thread) + runtime.setOnFrameCallback(frameOutput, (frame) => { + 'worklet' + scheduleOnRN(report, frame.width, frame.height) + frame.dispose() + }) + + await session.start() + try { + await waitUntil(() => reportedWidth > 0 && reportedHeight > 0, { + timeout: 15_000, + }) + } finally { + runtime.setOnFrameCallback(frameOutput, undefined) + await session.stop() + } + console.log( + `preview-sized frame: ${reportedWidth}x${reportedHeight} (requested target ${CommonResolutions.UHD_16_9.width}x${CommonResolutions.UHD_16_9.height})`, + ) + const requestedPixels = + CommonResolutions.UHD_16_9.width * CommonResolutions.UHD_16_9.height + const actualPixels = reportedWidth * reportedHeight + expect(actualPixels).toBeLessThan(requestedPixels) + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.photo.harness.ts b/apps/simple-camera/__tests__/visioncamera.photo.harness.ts new file mode 100644 index 0000000000..41ffc04da8 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.photo.harness.ts @@ -0,0 +1,517 @@ +import { Platform } from 'react-native' +import { + beforeAll, + describe, + expect, + it, + waitUntil, +} from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, + FlashMode, + MirrorMode, + QualityPrioritization, + Size, +} from 'react-native-vision-camera' +import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' + +describe('VisionCamera - Photo', () => { + let factory: CameraDeviceFactory + let backDevice: CameraDevice + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + const back = factory.getDefaultCamera('back') + expect(back).toBeDefined() + if (back == null) throw new Error('no back camera') + backDevice = back + }) + + it('captures a JPEG Photo in-memory', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.FHD_4_3, + containerFormat: 'jpeg', + quality: 0.9, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + expect(photo.height).toBeGreaterThan(0) + expect(photo.containerFormat).toBe('jpeg') + expect(photo.isRawPhoto).toBe(false) + + const image = await photo.toImageAsync() + expect(image).toBeDefined() + image.dispose() + photo.dispose() + + await session.stop() + }) + + // TODO: Re-enable once VisionCamera exposes a way to query supported photo + // container formats upfront (see the TODO in CameraPhotoOutput.nitro.ts + // near `TargetPhotoContainerFormat`). Without that API there is no + // precondition to gate on, and the HEIC path throws at configure time + // on devices that do not support the format. + it.skip('captures a HEIC Photo', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.FHD_4_3, + containerFormat: 'heic', + quality: 0.9, + qualityPrioritization: 'balanced', + }) + try { + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + expect(photo.height).toBeGreaterThan(0) + photo.dispose() + } finally { + await session.stop() + } + }) + + // TODO: Re-enable once VisionCamera exposes a way to query RAW / DNG support + // upfront. Today the CameraX DngCreator path also crashes natively on + // some devices with a buffer-size assertion + // (java.lang.AssertionError: Height and width of image buffer did not + // match height and width of either the preCorrectionActiveArraySize or + // the pixelArraySize.) — see androidx.camera.core.imagecapture.DngImage2Disk. + it.skip('captures a RAW DNG Photo to a file', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.FHD_4_3, + containerFormat: 'dng', + quality: 1.0, + qualityPrioritization: 'quality', + }) + try { + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'off' }], + constraints: [], + }, + ]) + await session.start() + const file = await photoOutput.capturePhotoToFile( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(file.filePath.length).toBeGreaterThan(0) + } finally { + await session.stop() + } + }) + + it('captures with each qualityPrioritization the device supports', async () => { + const priorities: QualityPrioritization[] = ['quality', 'balanced'] + if (backDevice.supportsSpeedQualityPrioritization) { + priorities.push('speed') + } else { + console.log('[SKIP] qualityPrioritization: speed not supported on device') + } + + for (const qualityPrioritization of priorities) { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + photo.dispose() + await session.stop() + } + }) + + it('captures at several target resolutions', async () => { + const targets: Size[] = [ + CommonResolutions.HD_4_3, + CommonResolutions.FHD_4_3, + CommonResolutions.HIGHEST_4_3, + ] + for (const targetResolution of targets) { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + console.log( + `target=${targetResolution.width}x${targetResolution.height} => resolved=${photo.width}x${photo.height}`, + ) + expect(photo.width).toBeGreaterThan(0) + expect(photo.height).toBeGreaterThan(0) + photo.dispose() + await session.stop() + } + }) + + it('invokes all capture lifecycle callbacks', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + let willBegin = 0 + let willCapture = 0 + let didCapture = 0 + + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + { + onWillBeginCapture: () => { + willBegin++ + }, + onWillCapturePhoto: () => { + willCapture++ + }, + onDidCapturePhoto: () => { + didCapture++ + }, + }, + ) + // Wait for the callbacks to drain BEFORE we stop the session, otherwise + // pending callback invocations can be dropped. + await waitUntil( + () => willBegin >= 1 && willCapture >= 1 && didCapture >= 1, + { timeout: 5_000 }, + ) + photo.dispose() + await session.stop() + + expect(willBegin).toBe(1) + expect(willCapture).toBe(1) + expect(didCapture).toBe(1) + }) + + it('delivers a preview image when previewImageTargetSize is set and the device supports it', async () => { + if (!backDevice.supportsPreviewImage) { + console.log( + '[SKIP] onPreviewImageAvailable: device has no preview image support', + ) + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + previewImageTargetSize: { width: 256, height: 192 }, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + let previewImageFired = false + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + { + onPreviewImageAvailable: (image) => { + previewImageFired = true + image.dispose() + }, + }, + ) + photo.dispose() + await session.stop() + + await waitUntil(() => previewImageFired, { timeout: 5_000 }) + }) + + it('captures with each flashMode the device supports', async () => { + const modes: FlashMode[] = ['off', 'auto'] + if (backDevice.hasFlash) { + modes.push('on') + } else { + console.log('[SKIP] flashMode on: device has no flash') + } + + for (const flashMode of modes) { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode, enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + photo.dispose() + await session.stop() + } + }) + + it('toggles enableShutterSound and enableRedEyeReduction without error', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + for (const enableShutterSound of [true, false]) { + for (const enableRedEyeReduction of [true, false]) { + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound, enableRedEyeReduction }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + photo.dispose() + } + } + + await session.stop() + }) + + it('applies enableDistortionCorrection when the device supports it', async () => { + if (!backDevice.supportsDistortionCorrection) { + console.log('[SKIP] enableDistortionCorrection: not supported on device') + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'off' }], + constraints: [], + }, + ]) + await session.start() + + const photo = await photoOutput.capturePhoto( + { + flashMode: 'off', + enableShutterSound: false, + enableDistortionCorrection: true, + }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + photo.dispose() + await session.stop() + }) + + it('honors the mirrorMode on each output configuration', async () => { + const modes: MirrorMode[] = ['off', 'on', 'auto'] + for (const mirrorMode of modes) { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + console.log( + `mirrorMode=${mirrorMode} => photo.isMirrored=${photo.isMirrored}`, + ) + switch (mirrorMode) { + case 'off': + expect(photo.isMirrored).toBe(false) + break + case 'on': + expect(photo.isMirrored).toBe(true) + break + case 'auto': + expect(photo.isMirrored).toBe(backDevice.position === 'front') + break + } + photo.dispose() + await session.stop() + } + }) + + it('captures a Photo from the default front camera', async () => { + const front = factory.getDefaultCamera('front') + expect(front).toBeDefined() + if (front == null) return + + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: front, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const photo = await photoOutput.capturePhoto( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(photo.width).toBeGreaterThan(0) + photo.dispose() + await session.stop() + }) + + it('writes different file paths for subsequent capturePhotoToFile calls', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const file1 = await photoOutput.capturePhotoToFile( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + const file2 = await photoOutput.capturePhotoToFile( + { flashMode: 'off', enableShutterSound: false }, + {}, + ) + expect(file1.filePath.length).toBeGreaterThan(0) + expect(file2.filePath.length).toBeGreaterThan(0) + expect(file1.filePath).not.toBe(file2.filePath) + + await session.stop() + }) + + it('exposes depth delivery support flags without throwing', async () => { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + console.log( + `photoOutput support flags: depthData=${photoOutput.supportsDepthDataDelivery} calibrationData=${photoOutput.supportsCameraCalibrationDataDelivery}`, + ) + if (Platform.OS === 'ios' && !photoOutput.supportsDepthDataDelivery) { + console.log( + '[SKIP] supportsDepthDataDelivery: not supported for this device/output combination', + ) + } + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.session.harness.ts b/apps/simple-camera/__tests__/visioncamera.session.harness.ts new file mode 100644 index 0000000000..e08f296118 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.session.harness.ts @@ -0,0 +1,312 @@ +import { + beforeAll, + describe, + expect, + it, + waitUntil, +} from 'react-native-harness' +import type { CameraDeviceFactory } from 'react-native-vision-camera' +import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' + +describe('VisionCamera - Session', () => { + let factory: CameraDeviceFactory + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + }) + + it('configures, starts and stops a session for every listed device', async () => { + const devices = factory.cameraDevices + expect(devices.length).toBeGreaterThan(0) + + for (const device of devices) { + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + let started = false + let stopped = false + const startSub = session.addOnStartedListener(() => { + started = true + }) + const stopSub = session.addOnStoppedListener(() => { + stopped = true + }) + + const controllers = await session.configure([ + { + input: device, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + expect(controllers.length).toBe(1) + expect(controllers[0]?.device.id).toBe(device.id) + + await session.start() + await waitUntil(() => started, { timeout: 10_000 }) + await session.stop() + await waitUntil(() => stopped, { timeout: 10_000 }) + + startSub.remove() + stopSub.remove() + console.log( + `session ok: ${device.position}:${device.id} (${device.localizedName})`, + ) + } + }) + + it('fires onStarted/onStopped exactly once per lifecycle', async () => { + const device = factory.getDefaultCamera('back') + if (device == null) return + + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + let startedCount = 0 + let stoppedCount = 0 + const startSub = session.addOnStartedListener(() => { + startedCount++ + }) + const stopSub = session.addOnStoppedListener(() => { + stoppedCount++ + }) + + await session.configure([ + { + input: device, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + await session.start() + await waitUntil(() => startedCount === 1, { timeout: 10_000 }) + await session.stop() + await waitUntil(() => stoppedCount === 1, { timeout: 10_000 }) + + expect(startedCount).toBe(1) + expect(stoppedCount).toBe(1) + + startSub.remove() + stopSub.remove() + }) + + it('stops delivering events after a listener subscription is removed', async () => { + const device = factory.getDefaultCamera('back') + if (device == null) return + + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + await session.configure([ + { + input: device, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + + let startedAfterRemove = 0 + const startSub = session.addOnStartedListener(() => { + startedAfterRemove++ + }) + startSub.remove() + + // Second listener we keep attached so we can observe that the session + // actually started before requesting a stop. + let actuallyStarted = false + const secondStartSub = session.addOnStartedListener(() => { + actuallyStarted = true + }) + + let stopped = false + const stopSub = session.addOnStoppedListener(() => { + stopped = true + }) + + await session.start() + await waitUntil(() => actuallyStarted, { timeout: 10_000 }) + await session.stop() + await waitUntil(() => stopped, { timeout: 10_000 }) + + expect(startedAfterRemove).toBe(0) + secondStartSub.remove() + stopSub.remove() + }) + + it('registers an onError listener without throwing', async () => { + const session = await VisionCamera.createCameraSession(false) + const subscription = session.addOnErrorListener(() => {}) + expect(subscription.remove).toBeDefined() + subscription.remove() + }) + + it('registers interruption listeners without throwing', async () => { + const session = await VisionCamera.createCameraSession(false) + const a = session.addOnInterruptionStartedListener(() => {}) + const b = session.addOnInterruptionEndedListener(() => {}) + a.remove() + b.remove() + }) + + it('reconfigures a running session with a new output set', async () => { + const device = factory.getDefaultCamera('back') + if (device == null) return + + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + await session.configure([ + { + input: device, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + + const controllers = await session.configure([ + { + input: device, + outputs: [ + { output: photoOutput, mirrorMode: 'auto' }, + { output: videoOutput, mirrorMode: 'auto' }, + ], + constraints: [], + }, + ]) + expect(controllers.length).toBe(1) + + await session.stop() + }) + + it('supports a multi-cam session when the platform allows it', async () => { + if (!VisionCamera.supportsMultiCamSessions) { + console.log('[SKIP] multi-cam session: not supported on this platform') + return + } + const back = factory.getDefaultCamera('back') + const front = factory.getDefaultCamera('front') + if (back == null || front == null) return + + const session = await VisionCamera.createCameraSession(true) + const backPhoto = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const frontPhoto = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + + const controllers = await session.configure([ + { + input: back, + outputs: [{ output: backPhoto, mirrorMode: 'off' }], + constraints: [], + }, + { + input: front, + outputs: [{ output: frontPhoto, mirrorMode: 'on' }], + constraints: [], + }, + ]) + expect(controllers.length).toBe(2) + expect(controllers[0]?.device.id).toBe(back.id) + expect(controllers[1]?.device.id).toBe(front.id) + + let started = false + const sub = session.addOnStartedListener(() => { + started = true + }) + await session.start() + await waitUntil(() => started, { timeout: 15_000 }) + await session.stop() + sub.remove() + }) + + it('configures, starts and stops a multi-cam session for every supported device combination', async () => { + if (!VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] multi-cam combinations: not supported on this platform', + ) + return + } + const combinations = factory.supportedMultiCamDeviceCombinations + if (combinations.length === 0) { + console.log( + '[SKIP] multi-cam combinations: no combinations reported on this device', + ) + return + } + + for (const combination of combinations) { + const session = await VisionCamera.createCameraSession(true) + const connections = combination.map((device) => ({ + input: device, + outputs: [ + { + output: VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg' as const, + quality: 0.8, + qualityPrioritization: 'balanced' as const, + }), + mirrorMode: 'auto' as const, + }, + ], + constraints: [], + })) + + const controllers = await session.configure(connections) + expect(controllers.length).toBe(combination.length) + for (let i = 0; i < combination.length; i++) { + expect(controllers[i]?.device.id).toBe(combination[i]?.id) + } + + let started = false + const sub = session.addOnStartedListener(() => { + started = true + }) + await session.start() + await waitUntil(() => started, { timeout: 15_000 }) + await session.stop() + sub.remove() + + const description = combination + .map((d) => `${d.position}:${d.id}`) + .join(', ') + console.log(`multi-cam session ok: [${description}]`) + } + }) +}) diff --git a/apps/simple-camera/__tests__/visioncamera.video.harness.ts b/apps/simple-camera/__tests__/visioncamera.video.harness.ts new file mode 100644 index 0000000000..91d0d733c0 --- /dev/null +++ b/apps/simple-camera/__tests__/visioncamera.video.harness.ts @@ -0,0 +1,433 @@ +import { Platform } from 'react-native' +import { + beforeAll, + describe, + expect, + it, + waitUntil, +} from 'react-native-harness' +import type { + CameraDevice, + CameraDeviceFactory, + RecordingFinishedReason, +} from 'react-native-vision-camera' +import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' + +const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) + +describe('VisionCamera - Video', () => { + let factory: CameraDeviceFactory + let backDevice: CameraDevice + + beforeAll(async () => { + expect(VisionCamera.cameraPermissionStatus).toBe('authorized') + expect(VisionCamera.microphonePermissionStatus).toBe('authorized') + factory = await VisionCamera.createDeviceFactory() + const back = factory.getDefaultCamera('back') + expect(back).toBeDefined() + if (back == null) throw new Error('no back camera') + backDevice = back + }) + + it('records a short clip and finishes with reason "stopped"', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let finishedPath: string | undefined + let finishedReason: RecordingFinishedReason | undefined + let recordingError: Error | undefined + + try { + await recorder.startRecording( + (filePath, reason) => { + finishedPath = filePath + finishedReason = reason + }, + (error) => { + recordingError = error + }, + ) + await sleep(1500) + await recorder.stopRecording() + await waitUntil(() => finishedPath != null, { timeout: 10_000 }) + + expect(recordingError).toBe(undefined) + expect(finishedReason).toBe('stopped') + expect(finishedPath?.length ?? 0).toBeGreaterThan(0) + } finally { + await session.stop() + } + }) + + it('records with audio enabled', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let finished = false + try { + await recorder.startRecording( + () => { + finished = true + }, + (error) => { + throw error + }, + ) + await sleep(1000) + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 10_000 }) + } finally { + await session.stop() + } + }) + + it('applies a custom targetBitRate', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + targetBitRate: 2_000_000, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const recorder = await videoOutput.createRecorder({}) + let finished = false + try { + await recorder.startRecording( + () => { + finished = true + }, + () => {}, + ) + await sleep(1000) + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 10_000 }) + } finally { + await session.stop() + } + }) + + it('stops automatically when maxDuration is reached', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({ maxDuration: 1 }) + let finishedReason: RecordingFinishedReason | undefined + try { + await recorder.startRecording( + (_path, reason) => { + finishedReason = reason + }, + () => {}, + ) + await waitUntil(() => finishedReason != null, { timeout: 15_000 }) + expect(finishedReason).toBe('max-duration-reached') + } finally { + await session.stop() + } + }) + + it('stops automatically when maxFileSize is reached', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + targetBitRate: 8_000_000, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({ maxFileSize: 200_000 }) + let finishedReason: RecordingFinishedReason | undefined + try { + await recorder.startRecording( + (_path, reason) => { + finishedReason = reason + }, + () => {}, + ) + await waitUntil(() => finishedReason != null, { timeout: 20_000 }) + expect(finishedReason).toBe('max-file-size-reached') + } finally { + await session.stop() + } + }) + + it('pauses and resumes a recording', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let paused = false + let resumed = false + let finished = false + try { + await recorder.startRecording( + () => { + finished = true + }, + () => {}, + () => { + paused = true + }, + () => { + resumed = true + }, + ) + await sleep(500) + await recorder.pauseRecording() + await waitUntil(() => paused, { timeout: 5_000 }) + + await recorder.resumeRecording() + await waitUntil(() => resumed, { timeout: 5_000 }) + await sleep(500) + + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 10_000 }) + + expect(paused).toBe(true) + expect(resumed).toBe(true) + } finally { + await session.stop() + } + }) + + it('cancels a recording and does not fire onRecordingFinished', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let finishedCount = 0 + let errorCount = 0 + try { + await recorder.startRecording( + () => { + finishedCount++ + }, + () => { + errorCount++ + }, + ) + await sleep(500) + await recorder.cancelRecording() + await sleep(1000) + expect(finishedCount).toBe(0) + expect(errorCount).toBe(0) + } finally { + await session.stop() + } + }) + + it('reports growing recordedDuration and recordedFileSize while recording', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let finished = false + try { + await recorder.startRecording( + () => { + finished = true + }, + () => {}, + ) + expect(recorder.filePath.length).toBeGreaterThan(0) + await sleep(300) + const midDuration = recorder.recordedDuration + const midSize = recorder.recordedFileSize + await sleep(900) + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 10_000 }) + console.log( + `recorded mid duration=${midDuration}s mid size=${midSize}B, final size=${recorder.recordedFileSize}B`, + ) + expect(midDuration).toBeGreaterThanOrEqual(0) + expect(midSize).toBeGreaterThanOrEqual(0) + } finally { + await session.stop() + } + }) + + it('records with a persistent recorder across a session stop/start cycle', async () => { + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + enablePersistentRecorder: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + + const recorder = await videoOutput.createRecorder({}) + let finished = false + let recordingError: Error | undefined + try { + await recorder.startRecording( + () => { + finished = true + }, + (error) => { + recordingError = error + }, + ) + await sleep(500) + + await session.stop() + await sleep(300) + await session.start() + await sleep(500) + + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 15_000 }) + expect(recordingError).toBe(undefined) + } finally { + await session.stop() + } + }) + + it('records with enableHigherResolutionCodecs on Android', async () => { + if (Platform.OS !== 'android') { + console.log('[SKIP] enableHigherResolutionCodecs: Android only') + return + } + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.FHD_16_9, + enableAudio: false, + enableHigherResolutionCodecs: true, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + await session.start() + const recorder = await videoOutput.createRecorder({}) + let finished = false + try { + await recorder.startRecording( + () => { + finished = true + }, + () => {}, + ) + await sleep(800) + await recorder.stopRecording() + await waitUntil(() => finished, { timeout: 10_000 }) + } finally { + await session.stop() + } + }) + + it('returns supported video codecs on iOS after the output is attached', async () => { + if (Platform.OS !== 'ios') { + console.log('[SKIP] getSupportedVideoCodecs: iOS only') + return + } + const session = await VisionCamera.createCameraSession(false) + const videoOutput = VisionCamera.createVideoOutput({ + targetResolution: CommonResolutions.HD_16_9, + enableAudio: false, + }) + await session.configure([ + { + input: backDevice, + outputs: [{ output: videoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + const codecs = videoOutput.getSupportedVideoCodecs() + expect(codecs.length).toBeGreaterThan(0) + console.log(`supported video codecs: ${codecs.join(', ')}`) + await session.stop() + }) +}) diff --git a/apps/simple-camera/android/app/build.gradle b/apps/simple-camera/android/app/build.gradle new file mode 100644 index 0000000000..4b26c0ab74 --- /dev/null +++ b/apps/simple-camera/android/app/build.gradle @@ -0,0 +1,119 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '..' + // root = file("../") + // The folder where the react-native NPM package is. Default is ../node_modules/react-native + reactNativeDir = file("../../../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + codegenDir = file("../../../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js + cliFile = file("../../../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + hermesCommand = "$rootDir/../../../node_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "com.margelo.nitro.camera.example.simple" + defaultConfig { + applicationId "com.margelo.nitro.camera.example.simple" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/package/example/android/app/debug.keystore b/apps/simple-camera/android/app/debug.keystore similarity index 100% rename from package/example/android/app/debug.keystore rename to apps/simple-camera/android/app/debug.keystore diff --git a/package/example/android/app/proguard-rules.pro b/apps/simple-camera/android/app/proguard-rules.pro similarity index 100% rename from package/example/android/app/proguard-rules.pro rename to apps/simple-camera/android/app/proguard-rules.pro diff --git a/apps/simple-camera/android/app/src/main/AndroidManifest.xml b/apps/simple-camera/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..82ff17d26a --- /dev/null +++ b/apps/simple-camera/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/example/android/app/src/main/java/com/mrousavy/camera/example/MainActivity.kt b/apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainActivity.kt similarity index 84% rename from package/example/android/app/src/main/java/com/mrousavy/camera/example/MainActivity.kt rename to apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainActivity.kt index cbe550b244..1737acd2e2 100644 --- a/package/example/android/app/src/main/java/com/mrousavy/camera/example/MainActivity.kt +++ b/apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainActivity.kt @@ -1,4 +1,4 @@ -package com.mrousavy.camera.example +package com.margelo.nitro.camera.example.simple import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate @@ -11,11 +11,11 @@ class MainActivity : ReactActivity() { * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. */ - override fun getMainComponentName(): String = "VisionCameraExample" + override fun getMainComponentName(): String = "SimpleCamera" /** * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] - * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + * which allows you to enable New Architecture with a single boolean flag [fabricEnabled] */ override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) diff --git a/apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainApplication.kt b/apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainApplication.kt new file mode 100644 index 0000000000..1cc5a7d8e7 --- /dev/null +++ b/apps/simple-camera/android/app/src/main/java/com/margelo/nitro/camera/example/simple/MainApplication.kt @@ -0,0 +1,27 @@ +package com.margelo.nitro.camera.example.simple + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost + +class MainApplication : Application(), ReactApplication { + + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + }, + ) + } + + override fun onCreate() { + super.onCreate() + loadReactNative(this) + } +} diff --git a/package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/apps/simple-camera/android/app/src/main/res/drawable/rn_edit_text_material.xml similarity index 99% rename from package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml rename to apps/simple-camera/android/app/src/main/res/drawable/rn_edit_text_material.xml index 73b37e4d99..5c25e728ea 100644 --- a/package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/apps/simple-camera/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -17,7 +17,8 @@ android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material" android:insetRight="@dimen/abc_edit_text_inset_horizontal_material" android:insetTop="@dimen/abc_edit_text_inset_top_material" - android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"> + android:insetBottom="@dimen/abc_edit_text_inset_bottom_material" + > + + + diff --git a/apps/simple-camera/android/build.gradle b/apps/simple-camera/android/build.gradle new file mode 100644 index 0000000000..859f764ce7 --- /dev/null +++ b/apps/simple-camera/android/build.gradle @@ -0,0 +1,21 @@ +buildscript { + ext { + buildToolsVersion = "36.0.0" + minSdkVersion = 26 + compileSdkVersion = 36 + targetSdkVersion = 36 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.1.20" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/apps/simple-camera/android/gradle.properties b/apps/simple-camera/android/gradle.properties new file mode 100644 index 0000000000..9afe61598f --- /dev/null +++ b/apps/simple-camera/android/gradle.properties @@ -0,0 +1,44 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Use this property to enable edge-to-edge display support. +# This allows your app to draw behind system bars for an immersive UI. +# Note: Only works with ReactActivity and should not be used with custom Activity. +edgeToEdgeEnabled=false diff --git a/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.jar b/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..61285a659d Binary files /dev/null and b/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.properties b/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..37f78a6af8 --- /dev/null +++ b/apps/simple-camera/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/simple-camera/android/gradlew b/apps/simple-camera/android/gradlew new file mode 100755 index 0000000000..adff685a03 --- /dev/null +++ b/apps/simple-camera/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/apps/simple-camera/android/gradlew.bat b/apps/simple-camera/android/gradlew.bat new file mode 100644 index 0000000000..e509b2dd8f --- /dev/null +++ b/apps/simple-camera/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apps/simple-camera/android/settings.gradle b/apps/simple-camera/android/settings.gradle new file mode 100644 index 0000000000..0d5c8d950e --- /dev/null +++ b/apps/simple-camera/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'SimpleCamera' +include ':app' +includeBuild('../../../node_modules/@react-native/gradle-plugin') diff --git a/apps/simple-camera/app.json b/apps/simple-camera/app.json new file mode 100644 index 0000000000..841a2fd83b --- /dev/null +++ b/apps/simple-camera/app.json @@ -0,0 +1,4 @@ +{ + "name": "SimpleCamera", + "displayName": "SimpleCamera" +} diff --git a/apps/simple-camera/babel.config.js b/apps/simple-camera/babel.config.js new file mode 100644 index 0000000000..bfd0b8d143 --- /dev/null +++ b/apps/simple-camera/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: ['react-native-worklets/plugin'], +} diff --git a/apps/simple-camera/device-farm-tests/AwsTestSpec.yml b/apps/simple-camera/device-farm-tests/AwsTestSpec.yml new file mode 100644 index 0000000000..708fe67bf4 --- /dev/null +++ b/apps/simple-camera/device-farm-tests/AwsTestSpec.yml @@ -0,0 +1,45 @@ +version: 0.1 + +android_test_host: amazon_linux_2 + +phases: + install: + commands: + - devicefarm-cli use node 20 + - node -v + - curl -fsSL https://bun.com/install | bash + - ~/.bun/bin/bun install + + - adb reverse tcp:8081 tcp:8081 + - adb wait-for-device + - adb devices -l + + # Grant all runtime permissions we need up-front. AWS Device Farm has no UI + # operator to tap through permission dialogs, so any runtime permission + # request would hang the test. The APK is already installed by Device Farm + # before the test spec runs, so `pm grant` targets the installed package. + - | + APP_PACKAGE="com.margelo.nitro.camera.example.simple" + for PERMISSION in \ + android.permission.CAMERA \ + android.permission.RECORD_AUDIO \ + android.permission.ACCESS_FINE_LOCATION \ + android.permission.ACCESS_COARSE_LOCATION \ + android.permission.READ_EXTERNAL_STORAGE \ + android.permission.WRITE_EXTERNAL_STORAGE; do + echo "Granting ${PERMISSION} to ${APP_PACKAGE}" + adb shell pm grant "${APP_PACKAGE}" "${PERMISSION}" || echo "Failed to grant ${PERMISSION} (may not be applicable on this API level)" + done + + test: + commands: + - MANUFACTURER="$(adb shell getprop ro.product.manufacturer | tr -d '\r')" + - MODEL="$(adb shell getprop ro.product.model | tr -d '\r')" + - 'echo "Resolved Device Farm device for harness: ${MANUFACTURER} ${MODEL}"' + - HARNESS_JUNIT="$WORKING_DIRECTORY/harness-results.junit.xml" + - HARNESS_LOG="$WORKING_DIRECTORY/harness-output.log" + - cd apps/simple-camera + - set -o pipefail; HARNESS_ANDROID_DEVICE_MANUFACTURER="$MANUFACTURER" HARNESS_ANDROID_DEVICE_MODEL="$MODEL" CI=true JEST_JUNIT_OUTPUT_FILE="$HARNESS_JUNIT" ~/.bun/bin/bun run test:harness:android -- --reporters=default --reporters=jest-junit 2>&1 | tee "$HARNESS_LOG" + +artifacts: + - $WORKING_DIRECTORY diff --git a/apps/simple-camera/device-farm-tests/AwsTestSpecIOS.yml b/apps/simple-camera/device-farm-tests/AwsTestSpecIOS.yml new file mode 100644 index 0000000000..f4c9a52c2a --- /dev/null +++ b/apps/simple-camera/device-farm-tests/AwsTestSpecIOS.yml @@ -0,0 +1,44 @@ +version: 0.1 + +ios_test_host: macos_sequoia + +phases: + install: + commands: + - devicefarm-cli use node 20 + - node -v + - curl -fsSL https://bun.com/install | bash + - ~/.bun/bin/bun install + - xcrun devicectl list devices + + test: + commands: + - HARNESS_JUNIT="$WORKING_DIRECTORY/harness-results.junit.xml" + - HARNESS_LOG="$WORKING_DIRECTORY/harness-output.log" + + # Resolve host IPv6 for Metro. On macOS runners ipv4 won't work, as confirmed by AWS support. + # AWS specifically recommends using the utun interfaces for this, which is what we do here. + # If we fail to resolve an IP address, we error out, as without this the tests won't be able to connect to Metro at all. + - | + METRO_HOST_IP="$(ifconfig utun1 2>/dev/null | awk '/inet6 / {ip=$2} END {print ip}')" + if [ -z "$METRO_HOST_IP" ]; then + METRO_HOST_IP="$(ifconfig utun0 2>/dev/null | awk '/inet6 / {ip=$2} END {print ip}')" + fi + METRO_HOST_IP="${METRO_HOST_IP%%%*}" + [ -n "$METRO_HOST_IP" ] || { echo "Failed to resolve Metro host IPv6." >&2; exit 1; } + echo "Using Metro host IPv6 for iOS harness: ${METRO_HOST_IP}" + echo "$METRO_HOST_IP" > "$WORKING_DIRECTORY/metro-host-ip.txt" + + # Find the physical device connected to this runner we want to run the test on: + - IOS_DEVICE_UDID="${DEVICEFARM_DEVICE_UDID:-${DEVICEFARM_DEVICE_NAME:-}}" + - IOS_DEVICES_JSON="$(mktemp)" + - xcrun devicectl list devices --json-output "$IOS_DEVICES_JSON" + - IOS_DEVICE_NAME="$(node -e "const fs=require('node:fs');const payload=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const devices=payload?.result?.devices ?? [];const udid=process.env.IOS_DEVICE_UDID || '';const matchedByUdid=devices.find((d)=>d?.identifier===udid);const selected=matchedByUdid ?? devices[0] ?? null;const name=selected?.deviceProperties?.name ?? '';if(!name){process.exit(1)};process.stdout.write(name);" "$IOS_DEVICES_JSON")" + - 'echo "Resolved Device Farm iOS device for harness: ${IOS_DEVICE_NAME} (${IOS_DEVICE_UDID:-unknown-udid})"' + + # Actually run the test: + - cd apps/simple-camera + - set -o pipefail; HARNESS_METRO_BIND_HOST="::" HARNESS_IOS_METRO_HOST="$(cat "$WORKING_DIRECTORY/metro-host-ip.txt")" HARNESS_IOS_DEVICE_NAME="$IOS_DEVICE_NAME" HARNESS_DETECT_NATIVE_CRASHES=false CI=true JEST_JUNIT_OUTPUT_FILE="$HARNESS_JUNIT" ~/.bun/bin/bun run test:harness -- --harnessRunner ios --reporters=default --reporters=jest-junit 2>&1 | tee "$HARNESS_LOG" + +artifacts: + - $WORKING_DIRECTORY diff --git a/apps/simple-camera/index.js b/apps/simple-camera/index.js new file mode 100644 index 0000000000..c6f88c403b --- /dev/null +++ b/apps/simple-camera/index.js @@ -0,0 +1,9 @@ +/** + * @format + */ + +import { AppRegistry } from 'react-native' +import { name as appName } from './app.json' +import App from './src/App' + +AppRegistry.registerComponent(appName, () => App) diff --git a/apps/simple-camera/ios/.xcode.env b/apps/simple-camera/ios/.xcode.env new file mode 100644 index 0000000000..3d5782c715 --- /dev/null +++ b/apps/simple-camera/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/apps/simple-camera/ios/Podfile b/apps/simple-camera/ios/Podfile new file mode 100644 index 0000000000..e956a9b149 --- /dev/null +++ b/apps/simple-camera/ios/Podfile @@ -0,0 +1,34 @@ +# Resolve react_native_pods.rb with node to allow for hoisting +require Pod::Executable.execute_command('node', ['-p', + 'require.resolve( + "react-native/scripts/react_native_pods.rb", + {paths: [process.argv[1]]}, + )', __dir__]).strip + +platform :ios, '15.5' +prepare_react_native_project! + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end + +target 'SimpleCamera' do + config = use_native_modules! + + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + # :ccache_enabled => true + ) + end +end diff --git a/apps/simple-camera/ios/Podfile.lock b/apps/simple-camera/ios/Podfile.lock new file mode 100644 index 0000000000..7559076e18 --- /dev/null +++ b/apps/simple-camera/ios/Podfile.lock @@ -0,0 +1,2851 @@ +PODS: + - FBLazyVector (0.84.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (8.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 7.0.0) + - GoogleMLKit/MLKitCore (8.0.0): + - MLKitCommon (~> 13.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (3.5.0) + - hermes-engine (250829098.0.7): + - hermes-engine/Pre-built (= 250829098.0.7) + - hermes-engine/Pre-built (250829098.0.7) + - MLImage (1.0.0-beta7) + - MLKitBarcodeScanning (7.0.0): + - MLKitCommon (~> 13.0) + - MLKitVision (~> 9.0) + - MLKitCommon (13.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (9.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta7) + - MLKitCommon (~> 13.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - NitroImage (0.14.0): + - hermes-engine + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - NitroModules (0.35.6): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - PromisesObjC (2.4.0) + - RCTDeprecation (0.84.0) + - RCTRequired (0.84.0) + - RCTSwiftUI (0.84.0) + - RCTSwiftUIWrapper (0.84.0): + - RCTSwiftUI + - RCTTypeSafety (0.84.0): + - FBLazyVector (= 0.84.0) + - RCTRequired (= 0.84.0) + - React-Core (= 0.84.0) + - React (0.84.0): + - React-Core (= 0.84.0) + - React-Core/DevSupport (= 0.84.0) + - React-Core/RCTWebSocket (= 0.84.0) + - React-RCTActionSheet (= 0.84.0) + - React-RCTAnimation (= 0.84.0) + - React-RCTBlob (= 0.84.0) + - React-RCTImage (= 0.84.0) + - React-RCTLinking (= 0.84.0) + - React-RCTNetwork (= 0.84.0) + - React-RCTSettings (= 0.84.0) + - React-RCTText (= 0.84.0) + - React-RCTVibration (= 0.84.0) + - React-callinvoker (0.84.0) + - React-Core (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.84.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core-prebuilt (0.84.0): + - ReactNativeDependencies + - React-Core/CoreModulesHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/Default (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/DevSupport (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.84.0) + - React-Core/RCTWebSocket (= 0.84.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTActionSheetHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTAnimationHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTBlobHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTImageHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTLinkingHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTNetworkHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTSettingsHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTTextHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTVibrationHeaders (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTWebSocket (0.84.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.84.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-CoreModules (0.84.0): + - RCTTypeSafety (= 0.84.0) + - React-Core-prebuilt + - React-Core/CoreModulesHeaders (= 0.84.0) + - React-debug + - React-jsi (= 0.84.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-NativeModulesApple + - React-RCTBlob + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.84.0) + - React-runtimeexecutor + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-cxxreact (0.84.0): + - hermes-engine + - React-callinvoker (= 0.84.0) + - React-Core-prebuilt + - React-debug (= 0.84.0) + - React-jsi (= 0.84.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-logger (= 0.84.0) + - React-perflogger (= 0.84.0) + - React-runtimeexecutor + - React-timing (= 0.84.0) + - React-utils + - ReactNativeDependencies + - React-debug (0.84.0) + - React-defaultsnativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-domnativemodule + - React-featureflags + - React-featureflagsnativemodule + - React-idlecallbacksnativemodule + - React-intersectionobservernativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-RCTFBReactNativeSpec + - React-webperformancenativemodule + - ReactNativeDependencies + - Yoga + - React-domnativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-FabricComponents + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animated (= 0.84.0) + - React-Fabric/animationbackend (= 0.84.0) + - React-Fabric/animations (= 0.84.0) + - React-Fabric/attributedstring (= 0.84.0) + - React-Fabric/bridging (= 0.84.0) + - React-Fabric/componentregistry (= 0.84.0) + - React-Fabric/componentregistrynative (= 0.84.0) + - React-Fabric/components (= 0.84.0) + - React-Fabric/consistency (= 0.84.0) + - React-Fabric/core (= 0.84.0) + - React-Fabric/dom (= 0.84.0) + - React-Fabric/imagemanager (= 0.84.0) + - React-Fabric/leakchecker (= 0.84.0) + - React-Fabric/mounting (= 0.84.0) + - React-Fabric/observers (= 0.84.0) + - React-Fabric/scheduler (= 0.84.0) + - React-Fabric/telemetry (= 0.84.0) + - React-Fabric/templateprocessor (= 0.84.0) + - React-Fabric/uimanager (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animated (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animationbackend (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animations (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/attributedstring (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/bridging (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistry (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistrynative (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.84.0) + - React-Fabric/components/root (= 0.84.0) + - React-Fabric/components/scrollview (= 0.84.0) + - React-Fabric/components/view (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/legacyviewmanagerinterop (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/root (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/scrollview (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/view (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric/consistency (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/core (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/dom (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/imagemanager (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/leakchecker (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/mounting (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.84.0) + - React-Fabric/observers/intersection (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/events (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/intersection (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/scheduler (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/observers/events + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancecdpmetrics + - React-performancetimeline + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/telemetry (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/templateprocessor (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager/consistency (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-FabricComponents (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.84.0) + - React-FabricComponents/textlayoutmanager (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.84.0) + - React-FabricComponents/components/iostextinput (= 0.84.0) + - React-FabricComponents/components/modal (= 0.84.0) + - React-FabricComponents/components/rncore (= 0.84.0) + - React-FabricComponents/components/safeareaview (= 0.84.0) + - React-FabricComponents/components/scrollview (= 0.84.0) + - React-FabricComponents/components/switch (= 0.84.0) + - React-FabricComponents/components/text (= 0.84.0) + - React-FabricComponents/components/textinput (= 0.84.0) + - React-FabricComponents/components/unimplementedview (= 0.84.0) + - React-FabricComponents/components/virtualview (= 0.84.0) + - React-FabricComponents/components/virtualviewexperimental (= 0.84.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/inputaccessory (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/iostextinput (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/modal (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/rncore (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/safeareaview (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/scrollview (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/switch (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/text (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/textinput (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/unimplementedview (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualview (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualviewexperimental (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/textlayoutmanager (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricImage (0.84.0): + - hermes-engine + - RCTRequired (= 0.84.0) + - RCTTypeSafety (= 0.84.0) + - React-Core-prebuilt + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.84.0) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-featureflags (0.84.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-featureflagsnativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-graphics (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-utils + - ReactNativeDependencies + - React-hermes (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.84.0) + - React-jsi + - React-jsiexecutor (= 0.84.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-oscompat + - React-perflogger (= 0.84.0) + - React-runtimeexecutor + - ReactNativeDependencies + - React-idlecallbacksnativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-ImageManager (0.84.0): + - React-Core-prebuilt + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-intersectionobservernativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-jserrorhandler (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - ReactCommon/turbomodule/bridging + - ReactNativeDependencies + - React-jsi (0.84.0): + - hermes-engine + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsiexecutor (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-jserrorhandler + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspector (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-jsinspectortracing + - React-oscompat + - React-perflogger (= 0.84.0) + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspectorcdp (0.84.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsinspectornetwork (0.84.0): + - React-Core-prebuilt + - React-jsinspectorcdp + - ReactNativeDependencies + - React-jsinspectortracing (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsinspectornetwork + - React-oscompat + - React-timing + - ReactNativeDependencies + - React-jsitooling (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.84.0) + - React-debug + - React-jsi (= 0.84.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsitracing (0.84.0): + - React-jsi + - React-logger (0.84.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-Mapbuffer (0.84.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-microtasksnativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - react-native-blur (4.4.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-menu (2.0.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context (5.7.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common (= 5.7.0) + - react-native-safe-area-context/fabric (= 5.7.0) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/common (5.7.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/fabric (5.7.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-skia (2.4.21): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-vector-icons-ionicons (12.4.1) + - react-native-video (6.19.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-video/Video (= 6.19.1) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-video/Fabric (6.19.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-video/Video (6.19.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-video/Fabric + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-NativeModulesApple (0.84.0): + - hermes-engine + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-networking (0.84.0): + - React-Core-prebuilt + - React-jsinspectornetwork + - React-jsinspectortracing + - React-performancetimeline + - React-timing + - ReactNativeDependencies + - React-oscompat (0.84.0) + - React-perflogger (0.84.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-performancecdpmetrics (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - ReactNativeDependencies + - React-performancetimeline (0.84.0): + - React-Core-prebuilt + - React-featureflags + - React-jsinspector + - React-jsinspectortracing + - React-perflogger + - React-timing + - ReactNativeDependencies + - React-RCTActionSheet (0.84.0): + - React-Core/RCTActionSheetHeaders (= 0.84.0) + - React-RCTAnimation (0.84.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTAnimationHeaders + - React-debug + - React-featureflags + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTAppDelegate (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-RCTBlob (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTFabric (0.84.0): + - hermes-engine + - RCTSwiftUIWrapper + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-networking + - React-performancecdpmetrics + - React-performancetimeline + - React-RCTAnimation + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-RCTFBReactNativeSpec (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.84.0) + - ReactCommon + - ReactNativeDependencies + - React-RCTFBReactNativeSpec/components (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-RCTImage (0.84.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTLinking (0.84.0): + - React-Core/RCTLinkingHeaders (= 0.84.0) + - React-jsi (= 0.84.0) + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactCommon/turbomodule/core (= 0.84.0) + - React-RCTNetwork (0.84.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTNetworkHeaders + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-NativeModulesApple + - React-networking + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTRuntime (0.84.0): + - hermes-engine + - React-Core + - React-Core-prebuilt + - React-debug + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-utils + - ReactNativeDependencies + - React-RCTSettings (0.84.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTText (0.84.0): + - React-Core/RCTTextHeaders (= 0.84.0) + - Yoga + - React-RCTVibration (0.84.0): + - React-Core-prebuilt + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-rendererconsistency (0.84.0) + - React-renderercss (0.84.0): + - React-debug + - React-utils + - React-rendererdebug (0.84.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-RuntimeApple (0.84.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-RuntimeCore (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-runtimeexecutor (0.84.0): + - React-Core-prebuilt + - React-debug + - React-featureflags + - React-jsi (= 0.84.0) + - React-utils + - ReactNativeDependencies + - React-RuntimeHermes (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-runtimescheduler (0.84.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - ReactNativeDependencies + - React-timing (0.84.0): + - React-debug + - React-utils (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-debug + - React-jsi (= 0.84.0) + - ReactNativeDependencies + - React-webperformancenativemodule (0.84.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactAppDependencyProvider (0.84.0): + - ReactCodegen + - ReactCodegen (0.84.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactCommon (0.84.0): + - React-Core-prebuilt + - ReactCommon/turbomodule (= 0.84.0) + - ReactNativeDependencies + - ReactCommon/turbomodule (0.84.0): + - hermes-engine + - React-callinvoker (= 0.84.0) + - React-Core-prebuilt + - React-cxxreact (= 0.84.0) + - React-jsi (= 0.84.0) + - React-logger (= 0.84.0) + - React-perflogger (= 0.84.0) + - ReactCommon/turbomodule/bridging (= 0.84.0) + - ReactCommon/turbomodule/core (= 0.84.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/bridging (0.84.0): + - hermes-engine + - React-callinvoker (= 0.84.0) + - React-Core-prebuilt + - React-cxxreact (= 0.84.0) + - React-jsi (= 0.84.0) + - React-logger (= 0.84.0) + - React-perflogger (= 0.84.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/core (0.84.0): + - hermes-engine + - React-callinvoker (= 0.84.0) + - React-Core-prebuilt + - React-cxxreact (= 0.84.0) + - React-debug (= 0.84.0) + - React-featureflags (= 0.84.0) + - React-jsi (= 0.84.0) + - React-logger (= 0.84.0) + - React-perflogger (= 0.84.0) + - React-utils (= 0.84.0) + - ReactNativeDependencies + - ReactNativeDependencies (0.84.0) + - RNGestureHandler (2.30.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNReanimated (4.3.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNReanimated/apple (= 4.3.0) + - RNReanimated/common (= 4.3.0) + - RNWorklets + - Yoga + - RNReanimated/apple (4.3.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - Yoga + - RNReanimated/common (4.3.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - Yoga + - RNScreens (4.24.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNScreens/common (= 4.24.0) + - Yoga + - RNScreens/common (4.24.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNVectorIcons (10.3.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNWorklets (0.8.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets/apple (= 0.8.1) + - RNWorklets/common (= 0.8.1) + - Yoga + - RNWorklets/apple (0.8.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNWorklets/common (0.8.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - VisionCamera (5.0.9): + - hermes-engine + - NitroImage + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - VisionCameraBarcodeScanner (5.0.9): + - GoogleMLKit/BarcodeScanning (= 8.0.0) + - hermes-engine + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - VisionCamera + - Yoga + - VisionCameraLocation (5.0.9): + - hermes-engine + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - VisionCamera + - Yoga + - VisionCameraResizer (5.0.9): + - hermes-engine + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - VisionCamera + - Yoga + - VisionCameraWorklets (5.0.9): + - hermes-engine + - NitroModules + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - VisionCamera + - Yoga + - Yoga (0.0.0) + +DEPENDENCIES: + - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) + - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - NitroImage (from `../../../node_modules/react-native-nitro-image`) + - NitroModules (from `../../../node_modules/react-native-nitro-modules`) + - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`) + - RCTSwiftUI (from `../../../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) + - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../../../node_modules/react-native/`) + - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../../../node_modules/react-native/`) + - React-Core-prebuilt (from `../../../node_modules/react-native/React-Core-prebuilt.podspec`) + - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`) + - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../../../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-intersectionobservernativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) + - React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - "react-native-blur (from `../../../node_modules/@react-native-community/blur`)" + - "react-native-menu (from `../../../node_modules/@react-native-menu/menu`)" + - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) + - "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)" + - "react-native-vector-icons-ionicons (from `../../../node_modules/@react-native-vector-icons/ionicons`)" + - react-native-video (from `../../../node_modules/react-native-video`) + - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../../../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../../../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`) + - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`) + - React-webperformancenativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) + - ReactCodegen (from `build/generated/ios/ReactCodegen`) + - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) + - ReactNativeDependencies (from `../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) + - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../../../node_modules/react-native-reanimated`) + - RNScreens (from `../../../node_modules/react-native-screens`) + - RNVectorIcons (from `../../../node_modules/react-native-vector-icons`) + - RNWorklets (from `../../../node_modules/react-native-worklets`) + - VisionCamera (from `../../../node_modules/react-native-vision-camera`) + - VisionCameraBarcodeScanner (from `../../../node_modules/react-native-vision-camera-barcode-scanner`) + - VisionCameraLocation (from `../../../node_modules/react-native-vision-camera-location`) + - VisionCameraResizer (from `../../../node_modules/react-native-vision-camera-resizer`) + - VisionCameraWorklets (from `../../../node_modules/react-native-vision-camera-worklets`) + - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + FBLazyVector: + :path: "../../../node_modules/react-native/Libraries/FBLazyVector" + hermes-engine: + :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-v250829098.0.8 + NitroImage: + :path: "../../../node_modules/react-native-nitro-image" + NitroModules: + :path: "../../../node_modules/react-native-nitro-modules" + RCTDeprecation: + :path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../../../node_modules/react-native/Libraries/Required" + RCTSwiftUI: + :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUI" + RCTSwiftUIWrapper: + :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" + RCTTypeSafety: + :path: "../../../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../../../node_modules/react-native/" + React-callinvoker: + :path: "../../../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../../../node_modules/react-native/" + React-Core-prebuilt: + :podspec: "../../../node_modules/react-native/React-Core-prebuilt.podspec" + React-CoreModules: + :path: "../../../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../../../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../../../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../../../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../../../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../../../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../../../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../../../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-intersectionobservernativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" + React-jserrorhandler: + :path: "../../../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../../../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectorcdp: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + React-jsinspectornetwork: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network" + React-jsinspectortracing: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../../../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../../../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../../../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-blur: + :path: "../../../node_modules/@react-native-community/blur" + react-native-menu: + :path: "../../../node_modules/@react-native-menu/menu" + react-native-safe-area-context: + :path: "../../../node_modules/react-native-safe-area-context" + react-native-skia: + :path: "../../../node_modules/@shopify/react-native-skia" + react-native-vector-icons-ionicons: + :path: "../../../node_modules/@react-native-vector-icons/ionicons" + react-native-video: + :path: "../../../node_modules/react-native-video" + React-NativeModulesApple: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-networking: + :path: "../../../node_modules/react-native/ReactCommon/react/networking" + React-oscompat: + :path: "../../../node_modules/react-native/ReactCommon/oscompat" + React-perflogger: + :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + React-performancetimeline: + :path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../../../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../../../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../../../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../../../node_modules/react-native/React" + React-RCTFBReactNativeSpec: + :path: "../../../node_modules/react-native/React" + React-RCTImage: + :path: "../../../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../../../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../../../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../../../node_modules/react-native/React/Runtime" + React-RCTSettings: + :path: "../../../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../../../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../../../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" + React-rendererdebug: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" + React-RuntimeApple: + :path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../../../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../../../node_modules/react-native/ReactCommon/react/utils" + React-webperformancenativemodule: + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + ReactAppDependencyProvider: + :path: build/generated/ios/ReactAppDependencyProvider + ReactCodegen: + :path: build/generated/ios/ReactCodegen + ReactCommon: + :path: "../../../node_modules/react-native/ReactCommon" + ReactNativeDependencies: + :podspec: "../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" + RNGestureHandler: + :path: "../../../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../../../node_modules/react-native-reanimated" + RNScreens: + :path: "../../../node_modules/react-native-screens" + RNVectorIcons: + :path: "../../../node_modules/react-native-vector-icons" + RNWorklets: + :path: "../../../node_modules/react-native-worklets" + VisionCamera: + :path: "../../../node_modules/react-native-vision-camera" + VisionCameraBarcodeScanner: + :path: "../../../node_modules/react-native-vision-camera-barcode-scanner" + VisionCameraLocation: + :path: "../../../node_modules/react-native-vision-camera-location" + VisionCameraResizer: + :path: "../../../node_modules/react-native-vision-camera-resizer" + VisionCameraWorklets: + :path: "../../../node_modules/react-native-vision-camera-worklets" + Yoga: + :path: "../../../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + FBLazyVector: c12d2108050e27952983d565a232f6f7b1ad5e69 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: ddd51d7dff36ff28defa69afedd9cdce684fd857 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + hermes-engine: 338e680ddbc265eedeca6897afcdeab60e0af645 + MLImage: 2ab9c968e75f57911c16f4c9d9e8a8e9604a86a1 + MLKitBarcodeScanning: 72c6437f13a900833b400136be53a8a5d86f42fa + MLKitCommon: 26b779f072a182c1603d4c88a101c350cac837b1 + MLKitVision: fa8dea9012ac59497c79ddbe9ebf32051047ac4c + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + NitroImage: bf3c19ea96629e5ab1665f61d720efc0df53377a + NitroModules: 1985a9de2cf4a6de1265c5936b7cc9d7f30e45d8 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RCTDeprecation: 3280799c14232a56e5a44f92981a8ee33bc69fd9 + RCTRequired: 9854a51b0f65ccf43ea0b744df4d70fce339db32 + RCTSwiftUI: 96986e49a4fdc2c2103929dee2641e1b57edf33d + RCTSwiftUIWrapper: e3eed9f50cad9f171e4487e2ff18a9caa4d46bfb + RCTTypeSafety: e9ba155357c236764934054ee2d393fd76e7b36b + React: 7ef36630d07638043a134a7dd2ec17e0be10fc3c + React-callinvoker: af4e8fe1d60ab63dd8d74c2a68988064c2848954 + React-Core: c0fb1df65eb0ed7a8633841831f05f93c3eb3aff + React-Core-prebuilt: 4978df8b63170d68b35f64f5e070d67e24e67184 + React-CoreModules: 7dfe7962360355f1547c85ab52e1fc4b57f17127 + React-cxxreact: 9e9c7f1710bc58abebf924813b5e825b99adb8e5 + React-debug: 38389b86e3570558ec73dd4cbc0cd2f2eec47a51 + React-defaultsnativemodule: a326ccbb71369762888a6be09a23fa5bce2bdb6a + React-domnativemodule: 8394c7b535d1b484b1eab677e00b086507cd906a + React-Fabric: 682dafd75455062590cd1f63c79199cf72ff27d9 + React-FabricComponents: 11b13a53213cd1aaca3bf7f4c61c669617b26b5f + React-FabricImage: 706c27e82f77b77db96ab3a19009ddb5e777967f + React-featureflags: c2898fb2f93ab92cfd9f294b4531d2884e7cfc7e + React-featureflagsnativemodule: 1edf93adfa12ba4f15d07079c1675b55ff579477 + React-graphics: 57d042385bfef5104aafeab189f43b8d6145013b + React-hermes: 96d2d439f0477a93fe8e801664088eccc07a16ff + React-idlecallbacksnativemodule: ab4dc6c3657f434f82c568ca83c963791e783f6a + React-ImageManager: f39057f375cf3f98255fb751df3865a91f2755c1 + React-intersectionobservernativemodule: 54ce679b183149fd9566a79211f2f54dc0a6fd1f + React-jserrorhandler: 2e92acff04ac815c6066c7cc08ea302610045db1 + React-jsi: dc97891e1ee7fa17cad01cd150c50f21e04bd51b + React-jsiexecutor: e1543ba5a8be761331c8158d91211079cc5b73a2 + React-jsinspector: 7a1d86673986db6666cacc8b95e92125397ab6ea + React-jsinspectorcdp: 38a0c116fd4965abf29261721db9b903923cb723 + React-jsinspectornetwork: cfeace6b40f13ba82980ba7cb730847a35675c7f + React-jsinspectortracing: 5507411117e51751dba0543cdee7916eb0388693 + React-jsitooling: e3a2df9043ab7b9ad11bbbfe4b33eb6762514f05 + React-jsitracing: ad179fab1c1e08a57fcdb840b7021b453f7a2b6d + React-logger: e40cc24a61d3a54c09bf4e83d5556b3b9d4c90aa + React-Mapbuffer: 53f28c81b84767a0b2fb4c0109dd7e4571226f76 + React-microtasksnativemodule: ddaf25a8d69f694bc880fb6055e34d79f1d50138 + react-native-blur: 4173cfa2b37358b2e767ca690a8afa2cb74b0aae + react-native-menu: 17321e8d0d15af018c56ae7e4e42268ef297f6c3 + react-native-safe-area-context: 29044d05d61f2c60d0828c373bd0ebe17eed58d0 + react-native-skia: a9c0ab55f18fa812b0db161c333e8bff4d739bb9 + react-native-vector-icons-ionicons: 7bc32eadb4d24c93252430b49f25b8be000e2340 + react-native-video: 8cfc70ca7f8f99906233b1a65a8b3c937fb32c1a + React-NativeModulesApple: 14a8919451154ede904f2bca84b27703a09028ba + React-networking: 46c0037f9202c1919493b78662a47cbe13022fdd + React-oscompat: b924b8609d06899f00ab1aa813b0cde9c5e12771 + React-perflogger: c3bb13800f795287e73a8c1991a2b8e5008ea3d0 + React-performancecdpmetrics: 851d2b18ba3d3d8cfb309bf468e5e93e46601122 + React-performancetimeline: 0a960aee139987151d2976813c47bef17dea3d3a + React-RCTActionSheet: 21fbcd85f552d5d6575453d2e8c149535d9c6f46 + React-RCTAnimation: 2c8cb9508864bb15e9f8fe86242d8918f05278e9 + React-RCTAppDelegate: 1d52e34d25f5f1bed5c07e0717c40dc572a80010 + React-RCTBlob: bc487ebb909c23920af75c842b1405edba61b8ea + React-RCTFabric: 7de87d2635b95171a06d9fffd907c4ac17823ef2 + React-RCTFBReactNativeSpec: b3936c48bf5262dc57ba28f8c8208cd1b570964c + React-RCTImage: a591fc9f08dc6c7b63b9fb34f51a7c1f32bd9595 + React-RCTLinking: cb9553b27de77a63beb4e3ce95f82aa8f3bed602 + React-RCTNetwork: 576ba853aef49628238b4840e969217b826af156 + React-RCTRuntime: e0aa5ea63ba4e06c9028da5ae8b05cf72bc8a1ea + React-RCTSettings: 8caa15edae452a5c4cd064569d5357a2bee8de15 + React-RCTText: af9a1c8d7c135c4d3ffa2de253ca95544234a521 + React-RCTVibration: c1dd36479ca1c1a59d16db81e5a994e9be06a68b + React-rendererconsistency: 32e7b98c05a3f237ecb524add21190036962e868 + React-renderercss: d65e9232e5033cd9c07b13fa429ce925b8143bd7 + React-rendererdebug: 25c6151116b7ea1f78af72afc64f2066ad29a61d + React-RuntimeApple: e036929884cc0d8088fe8a5a2d210e068d35e608 + React-RuntimeCore: 0c8a252051fe6b627f5147ac5b6a5298951472a8 + React-runtimeexecutor: 0765dddf1842e23e87ad13b2cb1bb72bb9005aeb + React-RuntimeHermes: 44cd4fdc4afa44fa782ddce8600e3cc90215fbc5 + React-runtimescheduler: 1966ff307933cdbafd480cb3aa1fdc90d9a6d539 + React-timing: 94c4a44dd2d10e4fc51fd42654fd5f67d68247ad + React-utils: 172d467a9c037d5ed51ee6eeaa6ad30ca1ebe1b1 + React-webperformancenativemodule: 9e3c5032dd30bf6418b741ab54ad26187b1c94c3 + ReactAppDependencyProvider: 625d2f6d9d5ef01acc9dfe2b5385504bbffd2ad0 + ReactCodegen: e6f176b40e56d6fa6d441baf3bc2e351172a41a6 + ReactCommon: cc0e38600f82487c5fe5d29150abb6fa9d981986 + ReactNativeDependencies: 15a6a8ab2c09e708aea8edc0a7f90a8a112aef63 + RNGestureHandler: 6d378fd1aa991c7ab62a4215ee6cc417895a6954 + RNReanimated: 9c65860a356274f39a796983d10ff58ce828ac9c + RNScreens: 088d923c4327c63c9f8c942cae17a9d038f47d97 + RNVectorIcons: af977c18ed27deba54ed038b439fca2911a08cfc + RNWorklets: e72cc4f0a1ffc40ca93479b191f0f33191d0dd97 + VisionCamera: 90f39b4f41552df1290551558e792cdcad4153a0 + VisionCameraBarcodeScanner: 63f6f5f3d75c843867d95645da82064ee3cf6100 + VisionCameraLocation: baa85bdac14b4c206d09367e876390086ba7a33a + VisionCameraResizer: 9822a79dc47192cb8bb9bbcec2ac405d79ceef10 + VisionCameraWorklets: f823f76d494c6836995d881cfbb573ac9f94c75f + Yoga: 0a3b1e85da3524bdc3e0161818c8881ad363f97b + +PODFILE CHECKSUM: 225e4ff5bb6719c47fa66724251c4ad2098c76da + +COCOAPODS: 1.15.2 diff --git a/apps/simple-camera/ios/SimpleCamera.xcodeproj/project.pbxproj b/apps/simple-camera/ios/SimpleCamera.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..38b1c24b2f --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera.xcodeproj/project.pbxproj @@ -0,0 +1,502 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; + 7942A3BA92A2F4C28D623278 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + CD5E53A548EFC1AEAC11C5AC /* libPods-SimpleCamera.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BECD76ABC304D3BDE643076A /* libPods-SimpleCamera.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0BC3913005B45C281440831B /* Pods-SimpleCamera.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleCamera.release.xcconfig"; path = "Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera.release.xcconfig"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* SimpleCamera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleCamera.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = SimpleCamera/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = SimpleCamera/Info.plist; sourceTree = ""; }; + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = SimpleCamera/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = SimpleCamera/AppDelegate.swift; sourceTree = ""; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SimpleCamera/LaunchScreen.storyboard; sourceTree = ""; }; + BECD76ABC304D3BDE643076A /* libPods-SimpleCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SimpleCamera.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DCAEECC560FB2F25AE78B25A /* Pods-SimpleCamera.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleCamera.debug.xcconfig"; path = "Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera.debug.xcconfig"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CD5E53A548EFC1AEAC11C5AC /* libPods-SimpleCamera.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* SimpleCamera */ = { + isa = PBXGroup; + children = ( + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 761780EC2CA45674006654EE /* AppDelegate.swift */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, + ); + name = SimpleCamera; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + BECD76ABC304D3BDE643076A /* libPods-SimpleCamera.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* SimpleCamera */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* SimpleCamera.app */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + DCAEECC560FB2F25AE78B25A /* Pods-SimpleCamera.debug.xcconfig */, + 0BC3913005B45C281440831B /* Pods-SimpleCamera.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* SimpleCamera */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SimpleCamera" */; + buildPhases = ( + FF8919CD419A2DDBF5FF89CE /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + ED316CA97AC37062F84C6F55 /* [CP] Embed Pods Frameworks */, + E66C89007D8B24BBB7F89A64 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SimpleCamera; + productName = SimpleCamera; + productReference = 13B07F961A680F5B00A75B9A /* SimpleCamera.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "SimpleCamera" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* SimpleCamera */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 7942A3BA92A2F4C28D623278 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n"; + }; + E66C89007D8B24BBB7F89A64 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + ED316CA97AC37062F84C6F55 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SimpleCamera/Pods-SimpleCamera-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FF8919CD419A2DDBF5FF89CE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SimpleCamera-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DCAEECC560FB2F25AE78B25A /* Pods-SimpleCamera.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CJW62Q77E7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = SimpleCamera/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.margelo.nitro.camera.example.simple; + PRODUCT_NAME = SimpleCamera; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0BC3913005B45C281440831B /* Pods-SimpleCamera.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CJW62Q77E7; + INFOPLIST_FILE = SimpleCamera/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.margelo.nitro.camera.example.simple; + PRODUCT_NAME = SimpleCamera; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SimpleCamera" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "SimpleCamera" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/apps/simple-camera/ios/SimpleCamera.xcodeproj/xcshareddata/xcschemes/SimpleCamera.xcscheme b/apps/simple-camera/ios/SimpleCamera.xcodeproj/xcshareddata/xcschemes/SimpleCamera.xcscheme new file mode 100644 index 0000000000..0dd76f9426 --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera.xcodeproj/xcshareddata/xcschemes/SimpleCamera.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/example/ios/VisionCameraExample.xcworkspace/contents.xcworkspacedata b/apps/simple-camera/ios/SimpleCamera.xcworkspace/contents.xcworkspacedata similarity index 76% rename from package/example/ios/VisionCameraExample.xcworkspace/contents.xcworkspacedata rename to apps/simple-camera/ios/SimpleCamera.xcworkspace/contents.xcworkspacedata index 880ce6a125..9bf6a00d2b 100644 --- a/package/example/ios/VisionCameraExample.xcworkspace/contents.xcworkspacedata +++ b/apps/simple-camera/ios/SimpleCamera.xcworkspace/contents.xcworkspacedata @@ -2,7 +2,7 @@ + location = "group:SimpleCamera.xcodeproj"> diff --git a/apps/simple-camera/ios/SimpleCamera/AppDelegate.swift b/apps/simple-camera/ios/SimpleCamera/AppDelegate.swift new file mode 100644 index 0000000000..4d3537cecf --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera/AppDelegate.swift @@ -0,0 +1,75 @@ +import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + private func valueForLaunchArgument(_ name: String) -> String? { + let args = ProcessInfo.processInfo.arguments + guard let index = args.firstIndex(of: name), index + 1 < args.count else { + return nil + } + return args[index + 1] + } + + private func configureMetroFromLaunchContext() { + let defaults = UserDefaults.standard + let launchArgJsLocation = valueForLaunchArgument("-RCT_jsLocation") + let launchArgPackagerScheme = valueForLaunchArgument("-RCT_packager_scheme") + + // react-native internally reads from UserDefaults when constructing the metro url, so we store it there. + // This is needed for harness e2e tests running on AWS device farm where we need to pass a custom metro ipv6. + // This is passed in apps/simple-camera/rn-harness.config.mjs as part of the launch args. + if let jsLocation = launchArgJsLocation { + defaults.set(jsLocation, forKey: "RCT_jsLocation") + } + + if let packagerScheme = launchArgPackagerScheme { + defaults.set(packagerScheme, forKey: "RCT_packager_scheme") + } + } + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + configureMetroFromLaunchContext() + + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + + window = UIWindow(frame: UIScreen.main.bounds) + + factory.startReactNative( + withModuleName: "SimpleCamera", + in: window, + launchOptions: launchOptions + ) + + return true + } +} + +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/apps/simple-camera/ios/SimpleCamera/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/simple-camera/ios/SimpleCamera/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..ddd7fca89e --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images": [ + { + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/apps/simple-camera/ios/SimpleCamera/Images.xcassets/Contents.json b/apps/simple-camera/ios/SimpleCamera/Images.xcassets/Contents.json new file mode 100644 index 0000000000..97a8662ebd --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/apps/simple-camera/ios/SimpleCamera/Info.plist b/apps/simple-camera/ios/SimpleCamera/Info.plist new file mode 100644 index 0000000000..781787d4a7 --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SimpleCamera + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSCameraUsageDescription + VisionCamera needs access to your Camera for very obvious reasons. + NSMicrophoneUsageDescription + VisionCamera needs access to your Microphone to record audio for video recordings. + NSLocationWhenInUseUsageDescription + VisionCamera needs access to your Location to add GPS tags to captured photos. + RCTNewArchEnabled + + UIAppFonts + + Ionicons.ttf + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/apps/simple-camera/ios/SimpleCamera/LaunchScreen.storyboard b/apps/simple-camera/ios/SimpleCamera/LaunchScreen.storyboard new file mode 100644 index 0000000000..689b7f7c60 --- /dev/null +++ b/apps/simple-camera/ios/SimpleCamera/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/example/ios/VisionCameraExample/PrivacyInfo.xcprivacy b/apps/simple-camera/ios/SimpleCamera/PrivacyInfo.xcprivacy similarity index 94% rename from package/example/ios/VisionCameraExample/PrivacyInfo.xcprivacy rename to apps/simple-camera/ios/SimpleCamera/PrivacyInfo.xcprivacy index 5b037f0c27..9029c2d13e 100644 --- a/package/example/ios/VisionCameraExample/PrivacyInfo.xcprivacy +++ b/apps/simple-camera/ios/SimpleCamera/PrivacyInfo.xcprivacy @@ -6,19 +6,20 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - C617.1 - 3B52.1 + CA92.1 + C56D.1 + 1C8F.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons - CA92.1 + C617.1 diff --git a/apps/simple-camera/jest.harness.config.mjs b/apps/simple-camera/jest.harness.config.mjs new file mode 100644 index 0000000000..da1398f600 --- /dev/null +++ b/apps/simple-camera/jest.harness.config.mjs @@ -0,0 +1,6 @@ +const config = { + preset: 'react-native-harness', + testMatch: ['/__tests__/**/*.harness.{js,jsx,ts,tsx}'], +} + +export default config diff --git a/apps/simple-camera/metro.config.js b/apps/simple-camera/metro.config.js new file mode 100644 index 0000000000..5480b7790d --- /dev/null +++ b/apps/simple-camera/metro.config.js @@ -0,0 +1,16 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') +const path = require('node:path') + +const root = path.resolve(__dirname, '..', '..') + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = { + watchFolders: [root], +} + +module.exports = mergeConfig(getDefaultConfig(__dirname), config) diff --git a/apps/simple-camera/package.json b/apps/simple-camera/package.json new file mode 100644 index 0000000000..0c7a798fd3 --- /dev/null +++ b/apps/simple-camera/package.json @@ -0,0 +1,66 @@ +{ + "name": "simple-camera", + "private": true, + "description": "Example app for developing and testing react-native-vision-camera.", + "author": "Marc Rousavy (https://github.com/mrousavy)", + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "bundle-install": "bundle install", + "pods": "cd ios && bundle exec pod install", + "start": "react-native start --client-logs", + "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain", + "test:harness": "react-native-harness", + "test:harness:android": "react-native-harness --harnessRunner android", + "test:harness:ios": "HARNESS_METRO_BIND_HOST=:: react-native-harness --harnessRunner ios" + }, + "dependencies": { + "@react-native-community/blur": "^4.4.1", + "@react-native-harness/platform-apple": "^1.1.0-rc.4", + "@react-native-menu/menu": "^2.0.0", + "@react-native-vector-icons/ionicons": "12.4.1", + "@react-navigation/native": "^7.1.31", + "@react-navigation/native-stack": "^7.14.2", + "@shopify/react-native-skia": "2.4.21", + "@types/jest": "^30.0.0", + "react": "19.2.3", + "react-native": "0.84.0", + "react-native-gesture-handler": "^2.30.0", + "react-native-nitro-image": "0.14.0", + "react-native-nitro-modules": "0.35.6", + "react-native-reanimated": "4.3.0", + "react-native-safe-area-context": "^5.7.0", + "react-native-screens": "^4.24.0", + "react-native-vector-icons": "^10.3.0", + "react-native-video": "6.19.1", + "react-native-vision-camera": "../../packages/react-native-vision-camera", + "react-native-vision-camera-barcode-scanner": "../../packages/react-native-vision-camera-barcode-scanner", + "react-native-vision-camera-location": "../../packages/react-native-vision-camera-location", + "react-native-vision-camera-resizer": "../../packages/react-native-vision-camera-resizer", + "react-native-vision-camera-skia": "../../packages/react-native-vision-camera-skia", + "react-native-vision-camera-worklets": "../../packages/react-native-vision-camera-worklets", + "react-native-worklets": "0.8.1" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "20.1.2", + "@react-native-community/cli-platform-android": "20.1.2", + "@react-native-community/cli-platform-ios": "20.1.2", + "@react-native-harness/platform-android": "1.1.0-rc.4", + "@react-native/babel-preset": "0.84.0", + "@react-native/metro-config": "0.84.0", + "@react-native/typescript-config": "0.84.0", + "@types/react": "19.2.14", + "react-native-harness": "1.1.0-rc.4", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20" + }, + "trustedDependencies": [ + "@shopify/react-native-skia" + ], + "version": "5.0.9" +} diff --git a/apps/simple-camera/rn-harness.config.mjs b/apps/simple-camera/rn-harness.config.mjs new file mode 100644 index 0000000000..83e2b962ff --- /dev/null +++ b/apps/simple-camera/rn-harness.config.mjs @@ -0,0 +1,120 @@ +import { + androidEmulator, + androidPlatform, + physicalAndroidDevice, +} from '@react-native-harness/platform-android' +import { + applePhysicalDevice, + applePlatform, + appleSimulator, +} from '@react-native-harness/platform-apple' + +const androidEmulatorName = + process.env.HARNESS_ANDROID_EMULATOR ?? 'Pixel_API_35' +const androidApiLevel = Number.parseInt( + process.env.HARNESS_ANDROID_API_LEVEL ?? '35', + 10, +) +const androidDeviceProfile = + process.env.HARNESS_ANDROID_DEVICE_PROFILE ?? 'pixel' +const androidDiskSize = process.env.HARNESS_ANDROID_DISK_SIZE ?? '1G' +const androidHeapSize = process.env.HARNESS_ANDROID_HEAP_SIZE ?? '1G' +const androidBundleId = + process.env.HARNESS_ANDROID_BUNDLE_ID ?? + 'com.margelo.nitro.camera.example.simple' +const androidPhysicalManufacturer = + process.env.HARNESS_ANDROID_DEVICE_MANUFACTURER ?? 'Pixel' +const androidPhysicalModel = process.env.HARNESS_ANDROID_DEVICE_MODEL ?? 'Pro 7' +const androidDeviceMode = + process.env.HARNESS_ANDROID_DEVICE_MODE?.trim().toLowerCase() ?? 'physical' + +const iosBundleId = + process.env.HARNESS_IOS_BUNDLE_ID ?? 'com.margelo.nitro.camera.example.simple' +const iosSimulatorName = process.env.HARNESS_IOS_SIMULATOR ?? 'iPhone 16 Pro' +const iosSimulatorVersion = process.env.HARNESS_IOS_SIMULATOR_VERSION ?? '18.0' +const iosPhysicalDeviceName = process.env.HARNESS_IOS_DEVICE_NAME ?? 'iPhone' +const iosMetroHostInput = process.env.HARNESS_IOS_METRO_HOST?.trim() ?? '' +const iosMetroPort = process.env.HARNESS_IOS_METRO_PORT ?? '8081' + +const formatIosMetroHostPort = (input, port) => { + if (input === '') { + return '' + } + + const bracketedIpv6Match = input.match(/^\[([^\]]+)\](?::(\d+))?$/) + if (bracketedIpv6Match != null) { + const [, host, explicitPort] = bracketedIpv6Match + return explicitPort == null ? `[${host}]:${port}` : input + } + + const colonCount = [...input].filter((char) => char === ':').length + if (colonCount > 1) { + return `[${input}]:${port}` + } + + return input.includes(':') ? input : `${input}:${port}` +} + +const iosMetroHostPort = formatIosMetroHostPort(iosMetroHostInput, iosMetroPort) +const metroBindHost = process.env.HARNESS_METRO_BIND_HOST?.trim() ?? '' +const iosAppLaunchOptions = iosMetroHostPort + ? { + arguments: [ + '-RCT_jsLocation', + iosMetroHostPort, + '-RCT_packager_scheme', + 'http', + ], + } + : undefined + +const isCI = process.env.CI === 'true' +const bundleStartTimeout = isCI ? 90_000 : 15_000 +const maxAppRestarts = isCI ? 4 : 2 + +// TODO: get libimobiledevice on AWS working +const detectNativeCrashes = + process.env.HARNESS_DETECT_NATIVE_CRASHES?.trim().toLowerCase() !== 'false' + +const useEmulator = androidDeviceMode === 'emulator' + +const androidDevice = useEmulator + ? androidEmulator(androidEmulatorName, { + apiLevel: androidApiLevel, + profile: androidDeviceProfile, + diskSize: androidDiskSize, + heapSize: androidHeapSize, + }) + : physicalAndroidDevice(androidPhysicalManufacturer, androidPhysicalModel) + +const iosDevice = isCI + ? applePhysicalDevice(iosPhysicalDeviceName) + : appleSimulator(iosSimulatorName, iosSimulatorVersion) + +const config = { + entryPoint: './index.js', + appRegistryComponentName: 'SimpleCamera', + host: metroBindHost === '' ? undefined : metroBindHost, + runners: [ + androidPlatform({ + name: 'android', + device: androidDevice, + bundleId: androidBundleId, + }), + applePlatform({ + name: 'ios', + device: iosDevice, + bundleId: iosBundleId, + appLaunchOptions: iosAppLaunchOptions, + }), + ], + defaultRunner: 'android', + bridgeTimeout: 45_000, + bundleStartTimeout, + maxAppRestarts, + detectNativeCrashes, + resetEnvironmentBetweenTestFiles: true, + forwardClientLogs: true, +} + +export default config diff --git a/apps/simple-camera/scripts/run-harness-android-ci.sh b/apps/simple-camera/scripts/run-harness-android-ci.sh new file mode 100644 index 0000000000..afb5272813 --- /dev/null +++ b/apps/simple-camera/scripts/run-harness-android-ci.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_APK_PATH="./android/app/build/outputs/apk/debug/app-debug.apk" +BUNDLE_ID="${HARNESS_ANDROID_BUNDLE_ID:?HARNESS_ANDROID_BUNDLE_ID is required}" +APP_ACTIVITY="${HARNESS_ANDROID_MAIN_ACTIVITY:-${BUNDLE_ID}/.MainActivity}" +STARTUP_TIMEOUT_SECONDS="${HARNESS_ANDROID_STARTUP_TIMEOUT_SECONDS:-60}" +HARNESS_TIMEOUT_SECONDS="${HARNESS_ANDROID_TEST_TIMEOUT_SECONDS:-720}" + +echo "Waiting for emulator..." +adb wait-for-device + +echo "Installing APK from ${APP_APK_PATH}..." +adb install -r "${APP_APK_PATH}" + +echo "Granting runtime permissions for ${BUNDLE_ID}..." +adb shell settings put secure location_mode 3 || true +adb shell pm grant "${BUNDLE_ID}" android.permission.CAMERA || true +adb shell pm grant "${BUNDLE_ID}" android.permission.RECORD_AUDIO || true +adb shell pm grant "${BUNDLE_ID}" android.permission.ACCESS_FINE_LOCATION || true +adb shell pm grant "${BUNDLE_ID}" android.permission.ACCESS_COARSE_LOCATION || true + +echo "Runtime permissions:" +adb shell dumpsys package "${BUNDLE_ID}" | awk '/runtime permissions:/{flag=1} flag{print} /^\s*$/{if(flag){exit}}' || true + +echo "Checking app startup..." +adb logcat -c || true +adb shell am force-stop "${BUNDLE_ID}" || true +adb shell am start -W -n "${APP_ACTIVITY}" + +launch_deadline=$((SECONDS + STARTUP_TIMEOUT_SECONDS)) +while true; do + if adb shell pidof "${BUNDLE_ID}" | tr -d '\r' | grep -Eq '[0-9]+'; then + break + fi + + if (( SECONDS >= launch_deadline )); then + echo "App ${BUNDLE_ID} did not start within ${STARTUP_TIMEOUT_SECONDS}s." + adb logcat -d -b crash || true + exit 1 + fi + + sleep 2 +done + +# Ensure the process does not die immediately after launch. +sleep 5 +if ! adb shell pidof "${BUNDLE_ID}" | tr -d '\r' | grep -Eq '[0-9]+'; then + echo "App ${BUNDLE_ID} crashed shortly after launch." + adb logcat -d -b crash || true + exit 1 +fi + +adb shell am force-stop "${BUNDLE_ID}" || true + +echo "Running harness tests (hard timeout: ${HARNESS_TIMEOUT_SECONDS}s)..." +set +e +timeout --foreground --kill-after=30s "${HARNESS_TIMEOUT_SECONDS}" bun run test:harness:android +exit_code=$? +set -e + +if [[ "${exit_code}" -eq 124 ]]; then + echo "Harness tests exceeded ${HARNESS_TIMEOUT_SECONDS}s and were aborted." + exit 1 +fi + +exit "${exit_code}" diff --git a/apps/simple-camera/src/App.tsx b/apps/simple-camera/src/App.tsx new file mode 100644 index 0000000000..dea154c2e6 --- /dev/null +++ b/apps/simple-camera/src/App.tsx @@ -0,0 +1,68 @@ +import { + createStaticNavigation, + type StaticParamList, +} from '@react-navigation/native' +import { createNativeStackNavigator } from '@react-navigation/native-stack' +import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { VisionCamera } from 'react-native-vision-camera' +import { CameraScreen } from './screens/CameraScreen' +import { PermissionsScreen } from './screens/PermissionsScreen' +import { PhotoScreen } from './screens/PhotoScreen' +import { VideoScreen } from './screens/VideoScreen' + +const RootStack = createNativeStackNavigator({ + initialRouteName: + VisionCamera.cameraPermissionStatus === 'authorized' + ? 'Camera' + : 'Permissions', + screens: { + Permissions: PermissionsScreen, + Camera: { + screen: CameraScreen, + options: { + orientation: 'portrait_up', + }, + }, + Photo: { + screen: PhotoScreen, + options: { + animation: 'none', + presentation: 'transparentModal', + }, + }, + Video: { + screen: VideoScreen, + options: { + animation: 'none', + presentation: 'transparentModal', + }, + }, + }, + screenOptions: { + navigationBarHidden: true, + headerShown: false, + contentStyle: { + backgroundColor: 'black', + }, + }, +}) + +type RootStackParamList = StaticParamList + +declare global { + namespace ReactNavigation { + interface RootParamList extends RootStackParamList {} + } +} + +const Navigation = createStaticNavigation(RootStack) + +function App() { + return ( + + + + ) +} + +export default App diff --git a/apps/simple-camera/src/components/BlurContainer.tsx b/apps/simple-camera/src/components/BlurContainer.tsx new file mode 100644 index 0000000000..25671c99af --- /dev/null +++ b/apps/simple-camera/src/components/BlurContainer.tsx @@ -0,0 +1,44 @@ +import { BlurView } from '@react-native-community/blur' +import type React from 'react' +import { Platform, StyleSheet, View, type ViewProps } from 'react-native' + +export interface BlurContainerProps extends ViewProps { + tint?: 'dark' | 'light' +} + +export function BlurContainer({ + tint = 'dark', + style, + children, + ...props +}: BlurContainerProps): React.ReactElement { + if (Platform.OS === 'ios') { + return ( + + + {children} + + ) + } else { + const bgStyle = tint === 'dark' ? styles.dark : styles.light + return ( + + {children} + + ) + } +} + +const styles = StyleSheet.create({ + dark: { + backgroundColor: 'rgba(0,0,0,0.8)', + }, + light: { + backgroundColor: 'rgba(255,255,255,0.8)', + }, +}) diff --git a/apps/simple-camera/src/components/CameraSelectorButton.tsx b/apps/simple-camera/src/components/CameraSelectorButton.tsx new file mode 100644 index 0000000000..21d186a483 --- /dev/null +++ b/apps/simple-camera/src/components/CameraSelectorButton.tsx @@ -0,0 +1,57 @@ +import { + type MenuAction, + MenuView, + type NativeActionEvent, +} from '@react-native-menu/menu' +import type React from 'react' +import { useCallback, useMemo } from 'react' +import type { CameraDevice, CameraPosition } from 'react-native-vision-camera' +import { IconButton } from './IconButton' + +interface Props { + devices: CameraDevice[] + setDevice: (device: CameraDevice) => void +} + +export function CameraSelectorButton({ + devices, + setDevice, +}: Props): React.ReactElement { + const menuActions = useMemo(() => { + const positions = ['back', 'front', 'external'].filter( + (p): p is CameraPosition => devices.some((d) => d.position === p), + ) + return positions.map((pos) => { + const devicesAtPosition = devices.filter((d) => d.position === pos) + return { + title: pos, + preferredElementSize: 'small', + displayInline: true, + subactions: devicesAtPosition.map((d) => { + return { + id: d.id, + subtitle: d.mediaTypes.join(' + '), + title: d.localizedName, + } + }), + } + }) + }, [devices]) + + const onMenuItemPressed = useCallback( + (event: NativeActionEvent) => { + const cameraId = event.nativeEvent.event + const targetDevice = devices.find((d) => d.id === cameraId) + if (targetDevice != null) { + setDevice(targetDevice) + } + }, + [devices, setDevice], + ) + + return ( + + {}} /> + + ) +} diff --git a/apps/simple-camera/src/components/CameraView.tsx b/apps/simple-camera/src/components/CameraView.tsx new file mode 100644 index 0000000000..cafd789764 --- /dev/null +++ b/apps/simple-camera/src/components/CameraView.tsx @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useRef } from 'react' +import { type GestureResponderEvent, StyleSheet, View } from 'react-native' +import { + Camera, + type CameraRef, + type CameraViewProps, +} from 'react-native-vision-camera' + +type Props = Omit< + CameraViewProps, + 'ref' | 'style' | 'enableNativeZoomGesture' | 'enableNativeTapToFocusGesture' +> + +export function CameraView({ device, constraints, ...props }: Props) { + const camera = useRef(null) + + useEffect(() => { + if (typeof device === 'string') { + console.log(`Device changed: "${device}"`) + } else { + console.log(`Device changed: ${device.localizedName}`) + console.log(` - Supported Pixel Formats:`, device.supportedPixelFormats) + console.log( + ` - Supported Photo Resolutions:`, + device.getSupportedResolutions('photo'), + ) + console.log( + ` - Supported Video Resolutions:`, + device.getSupportedResolutions('video'), + ) + console.log(` - Supported FPS Ranges:`, device.supportedFPSRanges) + console.log( + ` - Supported Dynamic Ranges:`, + device.supportedVideoDynamicRanges, + ) + } + }, [device]) + + const onPress = useCallback(async (event: GestureResponderEvent) => { + if (camera.current == null) throw new Error(`Camera ref is not yet ready!`) + + const point = { + x: event.nativeEvent.locationX, + y: event.nativeEvent.locationY, + } + + try { + const start = performance.now() + console.log(`Focusing to (${point.x}, ${point.y})...`) + await camera.current.focusTo(point, { + adaptiveness: 'continuous', + autoResetAfter: 3, + responsiveness: 'snappy', + }) + const end = performance.now() + console.log(`Focusing completed after ${(end - start).toFixed(2)}ms!`) + } catch (error) { + console.error(`Failed to focus!`, error) + } + }, []) + + return ( + + { + console.log(`Subject Area Changed! Resetting Focus...`) + camera.current?.resetFocus() + }} + onSessionConfigSelected={(config) => { + console.log(`Given Constraints:`, constraints) + console.log(`Resolved SessionConfig:`, config.toString()) + }} + /> + + ) +} + +const styles = StyleSheet.create({ + flex: { + flex: 1, + }, + camera: { + flex: 1, + borderRadius: 25, + overflow: 'hidden', + }, +}) diff --git a/apps/simple-camera/src/components/CaptureButton.tsx b/apps/simple-camera/src/components/CaptureButton.tsx new file mode 100644 index 0000000000..0dc69564e3 --- /dev/null +++ b/apps/simple-camera/src/components/CaptureButton.tsx @@ -0,0 +1,169 @@ +import type React from 'react' +import { useCallback, useRef } from 'react' +import { StyleSheet } from 'react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import Animated, { + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring, +} from 'react-native-reanimated' +import { BlurContainer } from './BlurContainer' + +export interface CaptureButtonProps { + takePhoto: () => Promise + startRecording: () => Promise + stopRecording: () => Promise +} + +const AnimatedBlurContainer = Animated.createAnimatedComponent(BlurContainer) +const LONG_PRESS_DURATION_MS = 300 + +export function CaptureButton({ + takePhoto, + startRecording, + stopRecording, +}: CaptureButtonProps): React.ReactElement { + const isPressed = useSharedValue(false) + const isCapturing = useSharedValue(false) + const didLongPressActivate = useRef(false) + const isRecording = useRef(false) + const recordingStartPromise = useRef | null>(null) + const shouldStopAfterStart = useRef(false) + + const outerScale = useDerivedValue(() => { + return withSpring(isCapturing.value ? 1.4 : 1.0, { + mass: 1, + stiffness: 1000, + damping: 500, + }) + }) + const innerScale = useDerivedValue(() => { + return withSpring(isPressed.value ? 0.7 : 1.0, { + mass: 1, + stiffness: 1500, + damping: 500, + }) + }) + + const tapGesture = Gesture.Tap() + .maxDuration(LONG_PRESS_DURATION_MS) + .maxDistance(20) + .onBegin(() => { + isPressed.set(true) + }) + .onFinalize(() => { + isPressed.set(false) + }) + .onEnd(async () => { + isCapturing.set(true) + try { + await takePhoto() + } finally { + isCapturing.set(false) + } + }) + .runOnJS(true) + + const stopRecordingNow = useCallback(async () => { + if (!isRecording.current) return + isRecording.current = false + try { + await stopRecording() + } catch (error) { + console.error(`Failed to stop recording!`, error) + } + }, [stopRecording]) + + const startRecordingSafely = useCallback(async () => { + if (recordingStartPromise.current != null || isRecording.current) return + shouldStopAfterStart.current = false + let startPromise: Promise | null = null + try { + startPromise = startRecording() + recordingStartPromise.current = startPromise + await startPromise + if (recordingStartPromise.current !== startPromise) return + recordingStartPromise.current = null + isRecording.current = true + if (shouldStopAfterStart.current) { + shouldStopAfterStart.current = false + await stopRecordingNow() + } + } catch (error) { + if ( + startPromise != null && + recordingStartPromise.current === startPromise + ) { + recordingStartPromise.current = null + } + isRecording.current = false + shouldStopAfterStart.current = false + console.error(`Failed to start recording!`, error) + } + }, [startRecording, stopRecordingNow]) + + const stopRecordingSafely = useCallback(async () => { + if (recordingStartPromise.current != null) { + shouldStopAfterStart.current = true + return + } + await stopRecordingNow() + }, [stopRecordingNow]) + + const longPressGesture = Gesture.LongPress() + .minDuration(LONG_PRESS_DURATION_MS) + .maxDistance(50) + .shouldCancelWhenOutside(false) + .onBegin(() => { + didLongPressActivate.current = false + isPressed.set(true) + }) + .onStart(() => { + didLongPressActivate.current = true + startRecordingSafely() + }) + .onFinalize(() => { + isPressed.set(false) + if (!didLongPressActivate.current) return + didLongPressActivate.current = false + stopRecordingSafely() + }) + .runOnJS(true) + + const captureGesture = Gesture.Exclusive(longPressGesture, tapGesture) + + const outerStyle = useAnimatedStyle(() => ({ + transform: [{ scale: outerScale.value }], + })) + const innerStyle = useAnimatedStyle(() => ({ + transform: [{ scale: innerScale.value }], + })) + + return ( + + + + + + ) +} + +const styles = StyleSheet.create({ + circle: { + borderRadius: 999, + overflow: 'hidden', + }, + outer: { + padding: 10, + }, + inner: { + padding: 30, + }, +}) diff --git a/apps/simple-camera/src/components/FullOverlay.tsx b/apps/simple-camera/src/components/FullOverlay.tsx new file mode 100644 index 0000000000..c9915c01fa --- /dev/null +++ b/apps/simple-camera/src/components/FullOverlay.tsx @@ -0,0 +1,17 @@ +import type React from 'react' +import { StyleSheet, View, type ViewProps } from 'react-native' + +export function FullOverlay({ + style, + ...props +}: ViewProps): React.ReactElement { + return +} + +const styles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFill, + marginTop: 15, + marginBottom: 25, + }, +}) diff --git a/apps/simple-camera/src/components/IconButton.tsx b/apps/simple-camera/src/components/IconButton.tsx new file mode 100644 index 0000000000..0573073695 --- /dev/null +++ b/apps/simple-camera/src/components/IconButton.tsx @@ -0,0 +1,35 @@ +import Ionicons, { + type IoniconsIconName, +} from '@react-native-vector-icons/ionicons' +import type React from 'react' +import { Pressable, StyleSheet } from 'react-native' +import { BlurContainer, type BlurContainerProps } from './BlurContainer' + +interface Props extends BlurContainerProps { + iconName: IoniconsIconName + onPress: () => void +} + +export function IconButton({ + iconName, + children, + onPress, + ...props +}: Props): React.ReactElement { + return ( + + + {children} + + + + ) +} + +const styles = StyleSheet.create({ + container: { + borderRadius: 9999, + overflow: 'hidden', + padding: 10, + }, +}) diff --git a/apps/simple-camera/src/components/Row.tsx b/apps/simple-camera/src/components/Row.tsx new file mode 100644 index 0000000000..3fdb97f673 --- /dev/null +++ b/apps/simple-camera/src/components/Row.tsx @@ -0,0 +1,14 @@ +import type React from 'react' +import { StyleSheet, View, type ViewProps } from 'react-native' + +export function Row({ style, ...props }: ViewProps): React.ReactElement { + return +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + paddingHorizontal: 15, + gap: 15, + }, +}) diff --git a/apps/simple-camera/src/globals.d.ts b/apps/simple-camera/src/globals.d.ts new file mode 100644 index 0000000000..2213462ef6 --- /dev/null +++ b/apps/simple-camera/src/globals.d.ts @@ -0,0 +1,9 @@ +declare global { + var gc: () => void + var performance: { + now: () => number + } +} + +// export so this is treated as a module +export {} diff --git a/apps/simple-camera/src/hooks/useIsActive.ts b/apps/simple-camera/src/hooks/useIsActive.ts new file mode 100644 index 0000000000..fb486c5078 --- /dev/null +++ b/apps/simple-camera/src/hooks/useIsActive.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' +import { AppState } from 'react-native' + +export function useIsActive(): boolean { + const [isActive, setIsActive] = useState( + () => AppState.currentState === 'active', + ) + + useEffect(() => { + const listener = AppState.addEventListener('change', (state) => { + setIsActive(state === 'active') + }) + return () => listener.remove() + }, []) + + return isActive +} diff --git a/apps/simple-camera/src/hooks/useSafeAreaPadding.ts b/apps/simple-camera/src/hooks/useSafeAreaPadding.ts new file mode 100644 index 0000000000..87f0840be9 --- /dev/null +++ b/apps/simple-camera/src/hooks/useSafeAreaPadding.ts @@ -0,0 +1,16 @@ +import { useMemo } from 'react' +import type { StyleProp, ViewStyle } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +export function useSafeAreaPadding() { + const safeArea = useSafeAreaInsets() + + return useMemo>(() => { + return { + paddingTop: safeArea.top, + paddingLeft: safeArea.left, + paddingRight: safeArea.right, + paddingBlock: safeArea.bottom, + } + }, [safeArea.bottom, safeArea.left, safeArea.right, safeArea.top]) +} diff --git a/apps/simple-camera/src/icons/CameraIcon.tsx b/apps/simple-camera/src/icons/CameraIcon.tsx new file mode 100644 index 0000000000..5b76297ddc --- /dev/null +++ b/apps/simple-camera/src/icons/CameraIcon.tsx @@ -0,0 +1,9 @@ +import Ionicons from '@react-native-vector-icons/ionicons' + +interface Props { + size: number +} + +export function CameraIcon({ size }: Props) { + return +} diff --git a/apps/simple-camera/src/logDevices.ts b/apps/simple-camera/src/logDevices.ts new file mode 100644 index 0000000000..4fb093855d --- /dev/null +++ b/apps/simple-camera/src/logDevices.ts @@ -0,0 +1,7 @@ +import type { CameraDevice } from 'react-native-vision-camera' + +export function logDevices(devices: CameraDevice[]): void { + for (const d of devices) { + console.log(`${d.id}: ${d.type} ${d.position} ("${d.localizedName}")`) + } +} diff --git a/apps/simple-camera/src/screens/CameraScreen.tsx b/apps/simple-camera/src/screens/CameraScreen.tsx new file mode 100644 index 0000000000..6813809c3c --- /dev/null +++ b/apps/simple-camera/src/screens/CameraScreen.tsx @@ -0,0 +1,297 @@ +import { useIsFocused, useNavigation } from '@react-navigation/native' +import { useCallback, useEffect, useRef, useState } from 'react' +import { StatusBar, StyleSheet, Text, View } from 'react-native' +import { + type Recorder, + useCameraDeviceExtensions, + useCameraDevices, + useDepthOutput, + useFrameOutput, + usePhotoOutput, + useVideoOutput, +} from 'react-native-vision-camera' +import { useLocation } from 'react-native-vision-camera-location' +import { useResizer } from 'react-native-vision-camera-resizer' +import { CameraSelectorButton } from '../components/CameraSelectorButton' +import { CameraView } from '../components/CameraView' +import { CaptureButton } from '../components/CaptureButton' +import { FullOverlay } from '../components/FullOverlay' +import { Row } from '../components/Row' +import { useIsActive } from '../hooks/useIsActive' +import { useSafeAreaPadding } from '../hooks/useSafeAreaPadding' +import { logDevices } from '../logDevices' + +export function CameraScreen() { + const navigation = useNavigation() + const isAppActive = useIsActive() + const isScreenFocused = useIsFocused() + const safePadding = useSafeAreaPadding() + const [enablePhoto, setEnablePhoto] = useState(true) + const [enableVideo, setEnableVideo] = useState(false) + const [enableFrameStream, setEnableFrameStream] = useState(false) + const [enableDepthStream, setEnableDepthStream] = useState(false) + + const devices = useCameraDevices() + const defaultDevice = devices[0] + const [device, setDevice] = useState(defaultDevice) + + useEffect(() => { + setDevice(defaultDevice) + }, [defaultDevice]) + + useEffect(() => logDevices(devices), [devices]) + + const location = useLocation({ + accuracy: 'balanced', + distanceFilter: 10, + }) + useEffect(() => { + if (!location.hasPermission) { + ;(async () => { + console.log(`requesting location permission...`) + const has = await location.requestPermission() + console.log(`location permssion: ${has}`) + })() + } + }, [location.hasPermission, location.requestPermission]) + useEffect(() => { + const l = location.currentLocation + if (l == null) return + console.log(`Location: ${l.latitude} ${l.longitude}`) + }, [location.currentLocation]) + + const photoOutput = usePhotoOutput({}) + const videoOutput = useVideoOutput({ + enableAudio: true, + }) + const { resizer, error } = useResizer({ + width: 192, + height: 192, + channelOrder: 'rgb', + dataType: 'float32', + scaleMode: 'cover', + pixelLayout: 'interleaved', + }) + useEffect(() => { + if (error != null) console.error('Failed to prepare Resizer!', error) + }, [error]) + const frameOutput1 = useFrameOutput({ + pixelFormat: 'yuv', + onFrame(frame) { + 'worklet' + if (resizer != null) { + const start = performance.now() + const resized = resizer.resize(frame) + const end = performance.now() + const time = `${(end - start).toFixed(2)}ms` + console.log( + `Resized ${frame.width}x${frame.height} ${frame.pixelFormat} -> ${resized.width}x${resized.height} rgb-float32 in ${time}`, + ) + const buffer = resized.getPixelBuffer() + const view = new Float32Array(buffer) + for (let i = 0; i < 3 * 10; i += 3) { + console.log( + ` Pixel [${i}] = [${view[i]}, ${view[i + 1]}, ${view[i + 2]}]`, + ) + } + resized.dispose() + } else { + console.log(`Resizer isn't ready yet...`) + } + frame.dispose() + }, + }) + const frameOutput2 = useFrameOutput({ + pixelFormat: 'native', + onFrame(frame) { + 'worklet' + console.log( + `frame output #2: ${frame.width}x${frame.height} in ${frame.pixelFormat}`, + ) + try { + const data = frame.getPixelBuffer() + console.log(`Pixels: ${data.byteLength}`) + } catch {} + frame.dispose() + }, + }) + const depthOutput = useDepthOutput({ + onDepth(depth) { + 'worklet' + console.log(`${depth.width}x${depth.height} depth frame.`) + const calibrationData = depth.cameraCalibrationData + if (calibrationData != null) { + console.log( + `.cameraExtrinsicsMatrix: ${calibrationData.cameraExtrinsicsMatrix.join(', ')}`, + ) + console.log( + `.cameraIntrinsicMatrix: ${calibrationData.cameraIntrinsicMatrix.join(', ')}`, + ) + console.log( + `.intrinsicMatrixReferenceDimensions: ${calibrationData.intrinsicMatrixReferenceDimensions.width}x${calibrationData.intrinsicMatrixReferenceDimensions.height}`, + ) + console.log(`.pixelSize: ${calibrationData.pixelSize}`) + console.log( + `.lensDistortionCenter: ${calibrationData.lensDistortionCenter}`, + ) + } else { + console.log(`no calibration data!`) + } + depth.dispose() + }, + }) + + const extensions = useCameraDeviceExtensions(device) + useEffect(() => { + if (extensions == null) return + console.log( + 'Available Camera Extensions:', + extensions.map((e) => e.type), + ) + }, [extensions]) + + const takePhoto = useCallback(async () => { + try { + console.log(`Capturing Photo...`) + const start = performance.now() + const photo = await photoOutput.capturePhoto( + { + location: location.currentLocation, + }, + {}, + ) + const end = performance.now() + const duration = (end - start).toFixed(2) + console.log( + `Captured ${photo.width}x${photo.height} ${photo.containerFormat} Photo in ${duration}ms!`, + ) + navigation.navigate('Photo', { photo: photo }) + } catch (e) { + console.error(`Failed to take Photo!`, e) + } + }, [navigation, photoOutput, location.currentLocation]) + + const preparedRecorder = useRef(undefined) + const activeRecorder = useRef(undefined) + const startRecording = useCallback(async () => { + console.log(`Starting Recording...`) + // get previously prepared recorder (cached) + let recorder = preparedRecorder.current + if (recorder == null) { + console.log(`No prepared Recorder available, creating one...`) + recorder = await videoOutput.createRecorder({}) + } + if (activeRecorder.current != null) { + // currently recording - abort + console.error(`Cannot start recording - already actively recording!`) + return + } + // setting it as actively recording + activeRecorder.current = recorder + // start recording + await recorder.startRecording( + (path) => { + console.log(`Recording finished! Path:`, path) + navigation.navigate('Video', { videoURL: path }) + activeRecorder.current = undefined + }, + (error) => { + console.error(`Failed to record!`, error) + activeRecorder.current = undefined + }, + () => console.log(`Recording paused`), + () => console.log(`Recording resumed.`), + ) + console.log(`Recording started!`) + // prepare a new recorder for the next call + preparedRecorder.current = await videoOutput.createRecorder({}) + }, [navigation.navigate, videoOutput.createRecorder]) + const stopRecording = useCallback(async () => { + console.log(`Stopping Recording...`) + const recorder = activeRecorder.current + if (recorder == null) { + console.error(`Not actively recording - cannot stop recording!`) + return + } + activeRecorder.current = undefined + await recorder.stopRecording() + console.log(`Recording stopped!`) + }, []) + + if (device == null) { + return ( + + No Camera Device! + + ) + } + return ( + + + + + console.log(`Camera interrupted! Reason: ${reason}`) + } + onInterruptionEnded={() => console.log(`Camera interruption over.`)} + onError={(error) => console.error(`Camera error:`, error)} + /> + + + + + { + setDevice(d) + }} + /> + + + + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + flex: { + flex: 1, + }, + textContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + selectedDevice: { + paddingHorizontal: 20, + paddingVertical: 10, + backgroundColor: 'white', + }, + text: { + color: 'white', + }, + captureButtonRow: { + flexDirection: 'row', + justifyContent: 'center', + }, +}) diff --git a/apps/simple-camera/src/screens/PermissionsScreen.tsx b/apps/simple-camera/src/screens/PermissionsScreen.tsx new file mode 100644 index 0000000000..6ff8186478 --- /dev/null +++ b/apps/simple-camera/src/screens/PermissionsScreen.tsx @@ -0,0 +1,44 @@ +import { useNavigation } from '@react-navigation/native' +import type React from 'react' +import { useEffect } from 'react' +import { Button, StyleSheet, Text, View } from 'react-native' +import { + useCameraPermission, + useMicrophonePermission, +} from 'react-native-vision-camera' + +export function PermissionsScreen(): React.ReactElement { + const navigation = useNavigation() + const cameraPermission = useCameraPermission() + const microphonePermission = useMicrophonePermission() + + useEffect(() => { + if (cameraPermission.hasPermission) { + navigation.navigate('Camera') + } + }, [cameraPermission.hasPermission, navigation]) + + return ( + + No Camera Permission! +