Compare commits
No commits in common. "main" and "ivan/addOnVideoChunkReadyCallback" have entirely different histories.
main
...
ivan/addOn
8
.github/workflows/build-android.yml
vendored
8
.github/workflows/build-android.yml
vendored
@ -30,11 +30,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 17
|
java-version: 11
|
||||||
java-package: jdk
|
java-package: jdk
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
@ -74,11 +74,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 17
|
java-version: 11
|
||||||
java-package: jdk
|
java-package: jdk
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
|
4
.github/workflows/build-ios.yml
vendored
4
.github/workflows/build-ios.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
|||||||
working-directory: package/example/ios
|
working-directory: package/example/ios
|
||||||
|
|
||||||
- name: Restore Pods cache
|
- name: Restore Pods cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: package/example/ios/Pods
|
path: package/example/ios/Pods
|
||||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||||
@ -113,7 +113,7 @@ jobs:
|
|||||||
working-directory: package/example/ios
|
working-directory: package/example/ios
|
||||||
|
|
||||||
- name: Restore Pods cache
|
- name: Restore Pods cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: package/example/ios/Pods
|
path: package/example/ios/Pods
|
||||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||||
|
33
.github/workflows/compress-images.yml
vendored
33
.github/workflows/compress-images.yml
vendored
@ -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"
|
|
@ -47,15 +47,6 @@ cd ios && pod install
|
|||||||
|
|
||||||
To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/projects/shadowlens)!
|
To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/projects/shadowlens)!
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href="https://apps.apple.com/app/shadowlens/id6471849004">
|
|
||||||
<img height="40" src="docs/static/img/appstore.svg" />
|
|
||||||
</a>
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.mrousavy.shadowlens">
|
|
||||||
<img height="40" src="docs/static/img/googleplay.svg" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
@ -130,23 +130,4 @@ The Code Scanner will call your [`onCodeScanned`](/docs/api/interfaces/CodeScann
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## UPC-A vs EAN-13 codes
|
|
||||||
|
|
||||||
UPC-A is a special case to handle if you need to cater for it. Android's SDK officially supports UPC-A but iOS does not, instead they handle the code as EAN-13. Since EAN-13 is a superset of UPC-A, with an extra 0 digit at the front.
|
|
||||||
|
|
||||||
This means, the `upc-a` types are reported under the `ean-13` umbrella type on iOS:
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const codeScanner = useCodeScanner({
|
|
||||||
codeTypes: ['upc-a'], // <-- ✅ We configure for 'upc-a' types
|
|
||||||
onCodeScanned: (codes) => {
|
|
||||||
for (const code of codes) {
|
|
||||||
console.log(code.type); // <-- ❌ On iOS, we receive 'ean-13'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
You will need to keep this in mind and do the conversion from EAN-13 to UPC-A yourself. This can be done by removing the front `0` digit from the code to get a UPC-A code.
|
|
||||||
|
|
||||||
#### 🚀 Next section: [Frame Processors](frame-processors)
|
#### 🚀 Next section: [Frame Processors](frame-processors)
|
||||||
|
@ -34,13 +34,12 @@ To understand a bit more about camera formats, you first need to understand a fe
|
|||||||
* 4k Photos, 1080p Videos, 240 FPS (ultra high FPS/slow motion)
|
* 4k Photos, 1080p Videos, 240 FPS (ultra high FPS/slow motion)
|
||||||
* 720p Photos, 720p Videos, 30 FPS (smaller buffers/e.g. faster face detection)
|
* 720p Photos, 720p Videos, 30 FPS (smaller buffers/e.g. faster face detection)
|
||||||
* Each app has different requirements, so the format filtering is up to you.
|
* Each app has different requirements, so the format filtering is up to you.
|
||||||
* The `videoResolution` and `videoAspectRatio` options also affect the preview, as preview is also running in the video stream.
|
|
||||||
|
|
||||||
To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props:
|
To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props:
|
||||||
|
|
||||||
- [`photoHeight`](/docs/api/interfaces/CameraDeviceFormat#photoheight)/[`photoWidth`](/docs/api/interfaces/CameraDeviceFormat#photoWidth): The resolution that will be used for taking photos. Choose a format with your desired resolution.
|
- [`photoHeight`](/docs/api/interfaces/CameraDeviceFormat#photoheight)/[`photoWidth`](/docs/api/interfaces/CameraDeviceFormat#photoWidth): The resolution that will be used for taking photos. Choose a format with your desired resolution.
|
||||||
- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoheight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos and streaming into frame processors. This also affects the preview's aspect ratio. Choose a format with your desired resolution.
|
- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoheight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos. Choose a format with your desired resolution.
|
||||||
- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minfps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxfps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos and streaming into frame processors.
|
- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minfps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxfps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos.
|
||||||
- [`videoStabilizationModes`](/docs/api/interfaces/CameraDeviceFormat#videostabilizationmodes): All supported Video Stabilization Modes, digital and optical. If this specific format contains your desired [`VideoStabilizationMode`](/docs/api/#videostabilizationmode), you can pass it to your `<Camera>` via the [`videoStabilizationMode` property](/docs/api/interfaces/CameraProps#videoStabilizationMode).
|
- [`videoStabilizationModes`](/docs/api/interfaces/CameraDeviceFormat#videostabilizationmodes): All supported Video Stabilization Modes, digital and optical. If this specific format contains your desired [`VideoStabilizationMode`](/docs/api/#videostabilizationmode), you can pass it to your `<Camera>` via the [`videoStabilizationMode` property](/docs/api/interfaces/CameraProps#videoStabilizationMode).
|
||||||
- [`pixelFormats`](/docs/api/interfaces/CameraDeviceFormat#pixelformats): All supported Pixel Formats. If this specific format contains your desired [`PixelFormat`](/docs/api/#PixelFormat), you can pass it to your `<Camera>` via the [`pixelFormat` property](/docs/api/interfaces/CameraProps#pixelFormat).
|
- [`pixelFormats`](/docs/api/interfaces/CameraDeviceFormat#pixelformats): All supported Pixel Formats. If this specific format contains your desired [`PixelFormat`](/docs/api/#PixelFormat), you can pass it to your `<Camera>` via the [`pixelFormat` property](/docs/api/interfaces/CameraProps#pixelFormat).
|
||||||
- [`supportsVideoHdr`](/docs/api/interfaces/CameraDeviceFormat#supportsvideohdr): Whether this specific format supports true 10-bit HDR for video capture. If this is `true`, you can enable `videoHdr` on your `<Camera>`.
|
- [`supportsVideoHdr`](/docs/api/interfaces/CameraDeviceFormat#supportsvideohdr): Whether this specific format supports true 10-bit HDR for video capture. If this is `true`, you can enable `videoHdr` on your `<Camera>`.
|
||||||
@ -62,7 +61,6 @@ You can either find a matching format manually by looping through your `CameraDe
|
|||||||
```ts
|
```ts
|
||||||
const device = ...
|
const device = ...
|
||||||
const format = useCameraFormat(device, [
|
const format = useCameraFormat(device, [
|
||||||
{ videoAspectRatio: 16 / 9 },
|
|
||||||
{ videoResolution: { width: 3048, height: 2160 } },
|
{ videoResolution: { width: 3048, height: 2160 } },
|
||||||
{ fps: 60 }
|
{ fps: 60 }
|
||||||
])
|
])
|
||||||
@ -74,7 +72,6 @@ const format = useCameraFormat(device, [
|
|||||||
```ts
|
```ts
|
||||||
const device = ...
|
const device = ...
|
||||||
const format = getCameraFormat(device, [
|
const format = getCameraFormat(device, [
|
||||||
{ videoAspectRatio: 16 / 9 },
|
|
||||||
{ videoResolution: { width: 3048, height: 2160 } },
|
{ videoResolution: { width: 3048, height: 2160 } },
|
||||||
{ fps: 60 }
|
{ fps: 60 }
|
||||||
])
|
])
|
||||||
|
@ -89,8 +89,7 @@ Additionally, you can also directly access the Frame's pixel data using [`toArra
|
|||||||
const frameProcessor = useFrameProcessor((frame) => {
|
const frameProcessor = useFrameProcessor((frame) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
if (frame.pixelFormat === 'rgb') {
|
if (frame.pixelFormat === 'rgb') {
|
||||||
const buffer = frame.toArrayBuffer()
|
const data = frame.toArrayBuffer()
|
||||||
const data = new Uint8Array(buffer)
|
|
||||||
console.log(`Pixel at 0,0: RGB(${data[0]}, ${data[1]}, ${data[2]})`)
|
console.log(`Pixel at 0,0: RGB(${data[0]}, ${data[1]}, ${data[2]})`)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -33,7 +33,6 @@ cd ios && pod install
|
|||||||
* [xulihang/**vision-camera-dynamsoft-barcode-reader**](https://github.com/xulihang/vision-camera-dynamsoft-barcode-reader): A plugin to read barcodes using Dynamsoft Barcode Reader.
|
* [xulihang/**vision-camera-dynamsoft-barcode-reader**](https://github.com/xulihang/vision-camera-dynamsoft-barcode-reader): A plugin to read barcodes using Dynamsoft Barcode Reader.
|
||||||
* [xulihang/**vision-camera-dynamsoft-label-recognizer**](https://github.com/xulihang/vision-camera-dynamsoft-label-recognizer): A plugin to recognize text on labels, MRZ passports, etc. using Dynamsoft Label Recognizer.
|
* [xulihang/**vision-camera-dynamsoft-label-recognizer**](https://github.com/xulihang/vision-camera-dynamsoft-label-recognizer): A plugin to recognize text on labels, MRZ passports, etc. using Dynamsoft Label Recognizer.
|
||||||
* [tony-xlh/**vision-camera-dynamsoft-document-normalizer**](https://github.com/tony-xlh/vision-camera-dynamsoft-document-normalizer): A plugin to scan documents using Dynamsoft Document Normalizer with features like document border detection and perspective transformation.
|
* [tony-xlh/**vision-camera-dynamsoft-document-normalizer**](https://github.com/tony-xlh/vision-camera-dynamsoft-document-normalizer): A plugin to scan documents using Dynamsoft Document Normalizer with features like document border detection and perspective transformation.
|
||||||
* [tony-xlh/**vision-camera-cropper**](https://github.com/tony-xlh/vision-camera-cropper): A plugin to crop frames and save frames to files or as base64.
|
|
||||||
* [aarongrider/**vision-camera-ocr**](https://github.com/aarongrider/vision-camera-ocr): A plugin to detect text in real time using MLKit Text Detector (OCR).
|
* [aarongrider/**vision-camera-ocr**](https://github.com/aarongrider/vision-camera-ocr): A plugin to detect text in real time using MLKit Text Detector (OCR).
|
||||||
* [yasintorun/**vision-camera-base64**](https://github.com/yasintorun/vision-camera-base64): A plugin to convert a Frame to a base64 string.
|
* [yasintorun/**vision-camera-base64**](https://github.com/yasintorun/vision-camera-base64): A plugin to convert a Frame to a base64 string.
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ cd ios && pod install
|
|||||||
|
|
||||||
|
|
||||||
<!-- Add your Frame Processor Plugin here! -->
|
<!-- Add your Frame Processor Plugin here! -->
|
||||||
* [nonam4/**react-native-vision-camera-face-detector**](https://github.com/nonam4/react-native-vision-camera-face-detector): A V3 frame processor plugin to detect faces using MLKit Vision Face Detector.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ cd ios && pod install
|
|||||||
<br />
|
<br />
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<b>
|
<b>
|
||||||
<a href="https://github.com/mrousavy/react-native-vision-camera/edit/main/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx">Click here</a> to add your Frame Processor Plugin to this list!
|
<a href="https://github.com/mrousavy/react-native-vision-camera/edit/main/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx">Click here</a> to add your Frame Processor Plugin to this list!
|
||||||
</b>
|
</b>
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
@ -70,13 +70,6 @@ Enable Buffer Compression ([`enableBufferCompression`](/docs/api/interfaces/Came
|
|||||||
|
|
||||||
Note: When not using a `frameProcessor`, buffer compression is automatically enabled.
|
Note: When not using a `frameProcessor`, buffer compression is automatically enabled.
|
||||||
|
|
||||||
### GPU buffers
|
|
||||||
|
|
||||||
Enable GPU Buffer flags ([`enableGpuBuffers`](/docs/api/interfaces/CameraProps#enablegpubuffers)) to optimize the Video Pipeline for zero-copy buffer forwarding.
|
|
||||||
If this is enabled, the Video Pipeline can avoid an additional CPU -> GPU copy, resulting in better performance and more efficiency.
|
|
||||||
|
|
||||||
Note: This only takes effect when using a `frameProcessor`.
|
|
||||||
|
|
||||||
### Video Stabilization
|
### Video Stabilization
|
||||||
|
|
||||||
Video Stabilization requires additional overhead to start the algorithm, so disabling [`videoStabilizationMode`](/docs/api/interfaces/CameraProps#videostabilizationmode) can significantly speed up the Camera initialization time.
|
Video Stabilization requires additional overhead to start the algorithm, so disabling [`videoStabilizationMode`](/docs/api/interfaces/CameraProps#videostabilizationmode) can significantly speed up the Camera initialization time.
|
||||||
|
@ -21,10 +21,10 @@ import useBaseUrl from '@docusaurus/useBaseUrl'
|
|||||||
**Download now:**
|
**Download now:**
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<a href="https://apps.apple.com/app/shadowlens/id6471849004">
|
<a href="https://apps.apple.com/at/app/shadowlens/id6471849004">
|
||||||
<img height={40} src={useBaseUrl("img/appstore.svg")} />
|
<img height={40} src={useBaseUrl("img/appstore.svg")} />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.mrousavy.shadowlens">
|
<a href="https://play.google.com/store/apps/details?id=com.mrousavy.shadowlens">
|
||||||
<img height={40} src={useBaseUrl("img/googleplay.svg")} />
|
<img height={58} src={useBaseUrl("img/playstore.png")} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +112,7 @@ If you're experiencing build issues or runtime issues in VisionCamera, make sure
|
|||||||
2. If a camera device is not being returned by [`Camera.getAvailableCameraDevices()`](/docs/api/classes/Camera#getavailablecameradevices), make sure it is a Camera2 compatible device. See [this section in the Android docs](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#reprocessing) for more information.
|
2. If a camera device is not being returned by [`Camera.getAvailableCameraDevices()`](/docs/api/classes/Camera#getavailablecameradevices), make sure it is a Camera2 compatible device. See [this section in the Android docs](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#reprocessing) for more information.
|
||||||
3. If your Frame Processor is not running, make sure you check the native Android Studio/Logcat logs. There is useful information about the Frame Processor Runtime that will tell you if something goes wrong.
|
3. If your Frame Processor is not running, make sure you check the native Android Studio/Logcat logs. There is useful information about the Frame Processor Runtime that will tell you if something goes wrong.
|
||||||
4. If your Frame Processor is not running, make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI.
|
4. If your Frame Processor is not running, make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI.
|
||||||
5. If you are experiencing black-screens, try removing all properties such as `fps`, `videoHdr`, `enableGpuBuffers` or `format` on the `<Camera>` component except for the required ones:
|
5. If you are experiencing black-screens, try removing all properties such as `fps`, `videoHdr` or `format` on the `<Camera>` component except for the required ones:
|
||||||
```tsx
|
```tsx
|
||||||
<Camera device={device} isActive={true} style={{ width: 500, height: 500 }} />
|
<Camera device={device} isActive={true} style={{ width: 500, height: 500 }} />
|
||||||
```
|
```
|
||||||
|
@ -25,7 +25,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
announcementBar: {
|
announcementBar: {
|
||||||
id: 'shadowlens',
|
id: 'shadowlens',
|
||||||
content: '<b>ShadowLens is out!!! 🥳🥳</b> See VisionCamera in action: <a target="_blank" rel="noopener noreferrer" href="https://apps.apple.com/app/shadowlens/id6471849004">Download ShadowLens for iOS</a> or <a target="_blank" rel="noopener noreferrer" href="https://play.google.com/store/apps/details?id=com.mrousavy.shadowlens">Download ShadowLens for Android</a>',
|
content: '<b>ShadowLens is out!!! 🥳🥳</b> Download the iOS app to see VisionCamera in action: <a target="_blank" rel="noopener noreferrer" href="https://apps.apple.com/at/app/shadowlens/id6471849004?l=en-GB">apps.apple.com/shadowlens</a>',
|
||||||
backgroundColor: '#e39600',
|
backgroundColor: '#e39600',
|
||||||
textColor: '#ffffff',
|
textColor: '#ffffff',
|
||||||
isCloseable: false,
|
isCloseable: false,
|
||||||
|
2
docs/static/img/googleplay.svg
vendored
2
docs/static/img/googleplay.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.0 KiB |
BIN
docs/static/img/playstore.png
vendored
Normal file
BIN
docs/static/img/playstore.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
@ -46,10 +46,6 @@ def safeExtGet(prop, fallback) {
|
|||||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
def safeExtGetBool(prop, fallback) {
|
|
||||||
Boolean.parseBoolean("${safeExtGet(prop, fallback)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
def reactNativeArchitectures() {
|
def reactNativeArchitectures() {
|
||||||
def value = project.getProperties().get("reactNativeArchitectures")
|
def value = project.getProperties().get("reactNativeArchitectures")
|
||||||
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||||
@ -72,10 +68,10 @@ static def findNodeModules(baseDir) {
|
|||||||
|
|
||||||
def nodeModules = findNodeModules(projectDir)
|
def nodeModules = findNodeModules(projectDir)
|
||||||
|
|
||||||
def hasWorklets = !safeExtGetBool('VisionCamera_disableFrameProcessors', false) && findProject(":react-native-worklets-core") != null
|
def hasWorklets = !safeExtGet("VisionCamera_disableFrameProcessors", false) && findProject(":react-native-worklets-core") != null
|
||||||
logger.warn("[VisionCamera] react-native-worklets-core ${hasWorklets ? "found" : "not found"}, Frame Processors ${hasWorklets ? "enabled" : "disabled"}!")
|
logger.warn("[VisionCamera] react-native-worklets-core ${hasWorklets ? "found" : "not found"}, Frame Processors ${hasWorklets ? "enabled" : "disabled"}!")
|
||||||
|
|
||||||
def enableCodeScanner = safeExtGetBool('VisionCamera_enableCodeScanner', false)
|
def enableCodeScanner = safeExtGet("VisionCamera_enableCodeScanner", false)
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@ -14,7 +14,7 @@ MutableJByteBuffer::MutableJByteBuffer(jni::alias_ref<jni::JByteBuffer> byteBuff
|
|||||||
_byteBuffer = jni::make_global(byteBuffer);
|
_byteBuffer = jni::make_global(byteBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
MutableJByteBuffer::~MutableJByteBuffer() {
|
MutableJByteBuffer::~MutableJByteBuffer() noexcept {
|
||||||
// Hermes GC might destroy HostObjects on an arbitrary Thread which might not be
|
// Hermes GC might destroy HostObjects on an arbitrary Thread which might not be
|
||||||
// connected to the JNI environment. To make sure fbjni can properly destroy
|
// connected to the JNI environment. To make sure fbjni can properly destroy
|
||||||
// the Java method, we connect to a JNI environment first.
|
// the Java method, we connect to a JNI environment first.
|
||||||
|
@ -29,11 +29,10 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWi
|
|||||||
}
|
}
|
||||||
|
|
||||||
OpenGLRenderer::~OpenGLRenderer() {
|
OpenGLRenderer::~OpenGLRenderer() {
|
||||||
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGLRenderer...");
|
|
||||||
destroy();
|
|
||||||
if (_outputSurface != nullptr) {
|
if (_outputSurface != nullptr) {
|
||||||
ANativeWindow_release(_outputSurface);
|
ANativeWindow_release(_outputSurface);
|
||||||
}
|
}
|
||||||
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderer::destroy() {
|
void OpenGLRenderer::destroy() {
|
||||||
|
@ -34,7 +34,7 @@ JSharedArray::JSharedArray(const jni::alias_ref<jhybridobject>& javaThis, const
|
|||||||
#else
|
#else
|
||||||
jsi::Runtime& runtime = *proxy->cthis()->getJSRuntime();
|
jsi::Runtime& runtime = *proxy->cthis()->getJSRuntime();
|
||||||
#endif
|
#endif
|
||||||
__android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %zu...", byteBuffer->getDirectSize());
|
__android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %i...", byteBuffer->getDirectSize());
|
||||||
_byteBuffer = jni::make_global(byteBuffer);
|
_byteBuffer = jni::make_global(byteBuffer);
|
||||||
_size = _byteBuffer->getDirectSize();
|
_size = _byteBuffer->getDirectSize();
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ TSelf JVisionCameraScheduler::initHybrid(jni::alias_ref<jhybridobject> jThis) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void JVisionCameraScheduler::dispatchAsync(const std::function<void()>& job) {
|
void JVisionCameraScheduler::dispatchAsync(const std::function<void()>& job) {
|
||||||
std::unique_lock<std::mutex> lock(_mutex);
|
|
||||||
// 1. add job to queue
|
// 1. add job to queue
|
||||||
_jobs.push(job);
|
_jobs.push(job);
|
||||||
scheduleTrigger();
|
scheduleTrigger();
|
||||||
|
@ -5,8 +5,6 @@ import com.facebook.react.bridge.Arguments
|
|||||||
import com.facebook.react.bridge.ReactContext
|
import com.facebook.react.bridge.ReactContext
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
import com.facebook.react.uimanager.events.RCTEventEmitter
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
||||||
import com.facebook.react.uimanager.UIManagerHelper
|
|
||||||
import com.facebook.react.uimanager.events.Event
|
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.mrousavy.camera.core.CameraError
|
import com.mrousavy.camera.core.CameraError
|
||||||
import com.mrousavy.camera.core.CodeScannerFrame
|
import com.mrousavy.camera.core.CodeScannerFrame
|
||||||
@ -14,30 +12,26 @@ import com.mrousavy.camera.core.UnknownCameraError
|
|||||||
import com.mrousavy.camera.core.code
|
import com.mrousavy.camera.core.code
|
||||||
import com.mrousavy.camera.types.CodeType
|
import com.mrousavy.camera.types.CodeType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import com.mrousavy.camera.types.*
|
|
||||||
|
|
||||||
fun CameraView.invokeOnInitialized() {
|
fun CameraView.invokeOnInitialized() {
|
||||||
Log.i(CameraView.TAG, "invokeOnInitialized()")
|
Log.i(CameraView.TAG, "invokeOnInitialized()")
|
||||||
|
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
val reactContext = context as ReactContext
|
||||||
val event = CameraInitializedEvent(surfaceId, id)
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null)
|
||||||
this.sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraView.invokeOnStarted() {
|
fun CameraView.invokeOnStarted() {
|
||||||
Log.i(CameraView.TAG, "invokeOnStarted()")
|
Log.i(CameraView.TAG, "invokeOnStarted()")
|
||||||
|
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
val reactContext = context as ReactContext
|
||||||
val event = CameraStartedEvent(surfaceId, id)
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStarted", null)
|
||||||
this.sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraView.invokeOnStopped() {
|
fun CameraView.invokeOnStopped() {
|
||||||
Log.i(CameraView.TAG, "invokeOnStopped()")
|
Log.i(CameraView.TAG, "invokeOnStopped()")
|
||||||
|
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
val reactContext = context as ReactContext
|
||||||
val event = CameraStoppedEvent(surfaceId, id)
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStopped", null)
|
||||||
this.sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraView.invokeOnChunkReady(filepath: File, index: Int) {
|
fun CameraView.invokeOnChunkReady(filepath: File, index: Int) {
|
||||||
@ -53,27 +47,24 @@ fun CameraView.invokeOnError(error: Throwable) {
|
|||||||
Log.e(CameraView.TAG, "invokeOnError(...):")
|
Log.e(CameraView.TAG, "invokeOnError(...):")
|
||||||
error.printStackTrace()
|
error.printStackTrace()
|
||||||
|
|
||||||
val cameraError =
|
val cameraError = when (error) {
|
||||||
when (error) {
|
is CameraError -> error
|
||||||
is CameraError -> error
|
else -> UnknownCameraError(error)
|
||||||
else -> UnknownCameraError(error)
|
|
||||||
}
|
|
||||||
val data = Arguments.createMap()
|
|
||||||
data.putString("code", cameraError.code)
|
|
||||||
data.putString("message", cameraError.message)
|
|
||||||
cameraError.cause?.let { cause ->
|
|
||||||
data.putMap("cause", errorToMap(cause))
|
|
||||||
}
|
}
|
||||||
|
val event = Arguments.createMap()
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
event.putString("code", cameraError.code)
|
||||||
val event = CameraErrorEvent(surfaceId, id, data)
|
event.putString("message", cameraError.message)
|
||||||
this.sendEvent(event)
|
cameraError.cause?.let { cause ->
|
||||||
|
event.putMap("cause", errorToMap(cause))
|
||||||
|
}
|
||||||
|
val reactContext = context as ReactContext
|
||||||
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraError", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraView.invokeOnViewReady() {
|
fun CameraView.invokeOnViewReady() {
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
val event = Arguments.createMap()
|
||||||
val event = CameraViewReadyEvent(surfaceId, id)
|
val reactContext = context as ReactContext
|
||||||
this.sendEvent(event)
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraViewReady", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeScannerFrame) {
|
fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeScannerFrame) {
|
||||||
@ -106,23 +97,14 @@ fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeSc
|
|||||||
codes.pushMap(code)
|
codes.pushMap(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = Arguments.createMap()
|
val event = Arguments.createMap()
|
||||||
data.putArray("codes", codes)
|
event.putArray("codes", codes)
|
||||||
val codeScannerFrame = Arguments.createMap()
|
val codeScannerFrame = Arguments.createMap()
|
||||||
codeScannerFrame.putInt("width", scannerFrame.width)
|
codeScannerFrame.putInt("width", scannerFrame.width)
|
||||||
codeScannerFrame.putInt("height", scannerFrame.height)
|
codeScannerFrame.putInt("height", scannerFrame.height)
|
||||||
data.putMap("frame", codeScannerFrame)
|
event.putMap("frame", codeScannerFrame)
|
||||||
|
|
||||||
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
||||||
val event = CameraCodeScannedEvent(surfaceId, id, data)
|
|
||||||
this.sendEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CameraView.sendEvent(event: Event<*>) {
|
|
||||||
val reactContext = context as ReactContext
|
val reactContext = context as ReactContext
|
||||||
val dispatcher =
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraCodeScanned", event)
|
||||||
UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
||||||
dispatcher?.dispatchEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun errorToMap(error: Throwable): WritableMap {
|
private fun errorToMap(error: Throwable): WritableMap {
|
||||||
|
@ -13,7 +13,7 @@ import com.mrousavy.camera.types.RecordVideoOptions
|
|||||||
import com.mrousavy.camera.utils.makeErrorMap
|
import com.mrousavy.camera.utils.makeErrorMap
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
suspend fun CameraView.startRecording(options: RecordVideoOptions, filePath: String, onRecordCallback: Callback) {
|
suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) {
|
||||||
// check audio permission
|
// check audio permission
|
||||||
if (audio == true) {
|
if (audio == true) {
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
@ -33,7 +33,7 @@ suspend fun CameraView.startRecording(options: RecordVideoOptions, filePath: Str
|
|||||||
val errorMap = makeErrorMap(error.code, error.message)
|
val errorMap = makeErrorMap(error.code, error.message)
|
||||||
onRecordCallback(null, errorMap)
|
onRecordCallback(null, errorMap)
|
||||||
}
|
}
|
||||||
cameraSession.startRecording(audio == true, options, filePath, callback, onError)
|
cameraSession.startRecording(audio == true, options, callback, onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
@ -30,12 +30,9 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
|
|||||||
|
|
||||||
val qualityPrioritization = options["qualityPrioritization"] as? String ?: "balanced"
|
val qualityPrioritization = options["qualityPrioritization"] as? String ?: "balanced"
|
||||||
val flash = options["flash"] as? String ?: "off"
|
val flash = options["flash"] as? String ?: "off"
|
||||||
|
val enableAutoRedEyeReduction = options["enableAutoRedEyeReduction"] == true
|
||||||
val enableAutoStabilization = options["enableAutoStabilization"] == true
|
val enableAutoStabilization = options["enableAutoStabilization"] == true
|
||||||
val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true
|
val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true
|
||||||
val enablePrecapture = options["enablePrecapture"] as? Boolean ?: false
|
|
||||||
|
|
||||||
// TODO: Implement Red Eye Reduction
|
|
||||||
options["enableAutoRedEyeReduction"]
|
|
||||||
|
|
||||||
val flashMode = Flash.fromUnionValue(flash)
|
val flashMode = Flash.fromUnionValue(flash)
|
||||||
val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization)
|
val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization)
|
||||||
@ -44,8 +41,8 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
|
|||||||
qualityPrioritizationMode,
|
qualityPrioritizationMode,
|
||||||
flashMode,
|
flashMode,
|
||||||
enableShutterSound,
|
enableShutterSound,
|
||||||
|
enableAutoRedEyeReduction,
|
||||||
enableAutoStabilization,
|
enableAutoStabilization,
|
||||||
enablePrecapture,
|
|
||||||
orientation
|
orientation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.mrousavy.camera.core.CameraConfiguration
|
import com.mrousavy.camera.core.CameraConfiguration
|
||||||
import com.mrousavy.camera.core.CameraQueues
|
import com.mrousavy.camera.core.CameraQueues
|
||||||
@ -48,23 +48,23 @@ class CameraView(context: Context) :
|
|||||||
// props that require reconfiguring
|
// props that require reconfiguring
|
||||||
var cameraId: String? = null
|
var cameraId: String? = null
|
||||||
var enableDepthData = false
|
var enableDepthData = false
|
||||||
|
var enableHighQualityPhotos: Boolean? = null
|
||||||
var enablePortraitEffectsMatteDelivery = false
|
var enablePortraitEffectsMatteDelivery = false
|
||||||
|
|
||||||
// use-cases
|
// use-cases
|
||||||
var photo = false
|
var photo: Boolean? = null
|
||||||
var video = false
|
var video: Boolean? = null
|
||||||
var audio = false
|
var audio: Boolean? = null
|
||||||
var enableFrameProcessor = false
|
var enableFrameProcessor = false
|
||||||
var pixelFormat: PixelFormat = PixelFormat.NATIVE
|
var pixelFormat: PixelFormat = PixelFormat.NATIVE
|
||||||
|
|
||||||
// props that require format reconfiguring
|
// props that require format reconfiguring
|
||||||
var format: CameraDeviceFormat? = null
|
var format: ReadableMap? = null
|
||||||
var fps: Int? = null
|
var fps: Int? = null
|
||||||
var videoStabilizationMode: VideoStabilizationMode? = null
|
var videoStabilizationMode: VideoStabilizationMode? = null
|
||||||
var videoHdr = false
|
var videoHdr = false
|
||||||
var photoHdr = false
|
var photoHdr = false
|
||||||
var lowLightBoost = false
|
var lowLightBoost: Boolean? = null // nullable bool
|
||||||
var enableGpuBuffers = false
|
|
||||||
|
|
||||||
// other props
|
// other props
|
||||||
var isActive = false
|
var isActive = false
|
||||||
@ -72,11 +72,7 @@ class CameraView(context: Context) :
|
|||||||
var zoom: Float = 1f // in "factor"
|
var zoom: Float = 1f // in "factor"
|
||||||
var exposure: Double = 1.0
|
var exposure: Double = 1.0
|
||||||
var orientation: Orientation = Orientation.PORTRAIT
|
var orientation: Orientation = Orientation.PORTRAIT
|
||||||
set(value) {
|
var enableZoomGesture: Boolean = false
|
||||||
field = value
|
|
||||||
previewView.orientation = value
|
|
||||||
}
|
|
||||||
var enableZoomGesture = false
|
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
updateZoomGesture()
|
updateZoomGesture()
|
||||||
@ -86,7 +82,7 @@ class CameraView(context: Context) :
|
|||||||
previewView.resizeMode = value
|
previewView.resizeMode = value
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var enableFpsGraph = false
|
var enableFpsGraph: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
updateFpsGraph()
|
updateFpsGraph()
|
||||||
@ -114,26 +110,21 @@ class CameraView(context: Context) :
|
|||||||
clipToOutline = true
|
clipToOutline = true
|
||||||
cameraSession = CameraSession(context, cameraManager, this)
|
cameraSession = CameraSession(context, cameraManager, this)
|
||||||
previewView = cameraSession.createPreviewView(context)
|
previewView = cameraSession.createPreviewView(context)
|
||||||
previewView.layoutParams = LayoutParams(
|
|
||||||
LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.MATCH_PARENT,
|
|
||||||
Gravity.CENTER
|
|
||||||
)
|
|
||||||
addView(previewView)
|
addView(previewView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
isMounted = true
|
isMounted = true
|
||||||
invokeOnViewReady()
|
invokeOnViewReady()
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
|
super.onAttachedToWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
|
||||||
update()
|
update()
|
||||||
|
super.onDetachedFromWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
@ -158,20 +149,19 @@ class CameraView(context: Context) :
|
|||||||
config.cameraId = cameraId
|
config.cameraId = cameraId
|
||||||
|
|
||||||
// Photo
|
// Photo
|
||||||
if (photo) {
|
if (photo == true) {
|
||||||
config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr))
|
config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr))
|
||||||
} else {
|
} else {
|
||||||
config.photo = CameraConfiguration.Output.Disabled.create()
|
config.photo = CameraConfiguration.Output.Disabled.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video/Frame Processor
|
// Video/Frame Processor
|
||||||
if (video || enableFrameProcessor) {
|
if (video == true || enableFrameProcessor) {
|
||||||
config.video = CameraConfiguration.Output.Enabled.create(
|
config.video = CameraConfiguration.Output.Enabled.create(
|
||||||
CameraConfiguration.Video(
|
CameraConfiguration.Video(
|
||||||
videoHdr,
|
videoHdr,
|
||||||
pixelFormat,
|
pixelFormat,
|
||||||
enableFrameProcessor,
|
enableFrameProcessor
|
||||||
enableGpuBuffers
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -179,7 +169,7 @@ class CameraView(context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
if (audio) {
|
if (audio == true) {
|
||||||
config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit))
|
config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit))
|
||||||
} else {
|
} else {
|
||||||
config.audio = CameraConfiguration.Output.Disabled.create()
|
config.audio = CameraConfiguration.Output.Disabled.create()
|
||||||
@ -199,7 +189,12 @@ class CameraView(context: Context) :
|
|||||||
config.orientation = orientation
|
config.orientation = orientation
|
||||||
|
|
||||||
// Format
|
// Format
|
||||||
config.format = format
|
val format = format
|
||||||
|
if (format != null) {
|
||||||
|
config.format = CameraDeviceFormat.fromJSValue(format)
|
||||||
|
} else {
|
||||||
|
config.format = null
|
||||||
|
}
|
||||||
|
|
||||||
// Side-Props
|
// Side-Props
|
||||||
config.fps = fps
|
config.fps = fps
|
||||||
|
@ -5,12 +5,10 @@ import com.facebook.react.common.MapBuilder
|
|||||||
import com.facebook.react.uimanager.ThemedReactContext
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
import com.facebook.react.uimanager.ViewGroupManager
|
import com.facebook.react.uimanager.ViewGroupManager
|
||||||
import com.facebook.react.uimanager.annotations.ReactProp
|
import com.facebook.react.uimanager.annotations.ReactProp
|
||||||
import com.mrousavy.camera.types.CameraDeviceFormat
|
|
||||||
import com.mrousavy.camera.types.CodeScannerOptions
|
import com.mrousavy.camera.types.CodeScannerOptions
|
||||||
import com.mrousavy.camera.types.Orientation
|
import com.mrousavy.camera.types.Orientation
|
||||||
import com.mrousavy.camera.types.PixelFormat
|
import com.mrousavy.camera.types.PixelFormat
|
||||||
import com.mrousavy.camera.types.ResizeMode
|
import com.mrousavy.camera.types.ResizeMode
|
||||||
import android.util.Log
|
|
||||||
import com.mrousavy.camera.types.Torch
|
import com.mrousavy.camera.types.Torch
|
||||||
import com.mrousavy.camera.types.VideoStabilizationMode
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
||||||
|
|
||||||
@ -47,17 +45,17 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "photo")
|
@ReactProp(name = "photo")
|
||||||
fun setPhoto(view: CameraView, photo: Boolean) {
|
fun setPhoto(view: CameraView, photo: Boolean?) {
|
||||||
view.photo = photo
|
view.photo = photo
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "video")
|
@ReactProp(name = "video")
|
||||||
fun setVideo(view: CameraView, video: Boolean) {
|
fun setVideo(view: CameraView, video: Boolean?) {
|
||||||
view.video = video
|
view.video = video
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "audio")
|
@ReactProp(name = "audio")
|
||||||
fun setAudio(view: CameraView, audio: Boolean) {
|
fun setAudio(view: CameraView, audio: Boolean?) {
|
||||||
view.audio = audio
|
view.audio = audio
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,12 +66,8 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
|
|
||||||
@ReactProp(name = "pixelFormat")
|
@ReactProp(name = "pixelFormat")
|
||||||
fun setPixelFormat(view: CameraView, pixelFormat: String?) {
|
fun setPixelFormat(view: CameraView, pixelFormat: String?) {
|
||||||
if (pixelFormat != null) {
|
val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat)
|
||||||
val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat)
|
view.pixelFormat = newPixelFormat
|
||||||
view.pixelFormat = newPixelFormat
|
|
||||||
} else {
|
|
||||||
view.pixelFormat = PixelFormat.NATIVE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableDepthData")
|
@ReactProp(name = "enableDepthData")
|
||||||
@ -91,19 +85,15 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
view.enableFpsGraph = enableFpsGraph
|
view.enableFpsGraph = enableFpsGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableGpuBuffers")
|
|
||||||
fun setEnableGpuBuffers(view: CameraView, enableGpuBuffers: Boolean) {
|
|
||||||
view.enableGpuBuffers = enableGpuBuffers
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactProp(name = "videoStabilizationMode")
|
@ReactProp(name = "videoStabilizationMode")
|
||||||
fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) {
|
fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) {
|
||||||
if (videoStabilizationMode != null) {
|
val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode)
|
||||||
val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode)
|
view.videoStabilizationMode = newMode
|
||||||
view.videoStabilizationMode = newMode
|
}
|
||||||
} else {
|
|
||||||
view.videoStabilizationMode = null
|
@ReactProp(name = "enableHighQualityPhotos")
|
||||||
}
|
fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) {
|
||||||
|
view.enableHighQualityPhotos = enableHighQualityPhotos
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enablePortraitEffectsMatteDelivery")
|
@ReactProp(name = "enablePortraitEffectsMatteDelivery")
|
||||||
@ -113,22 +103,13 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
|
|
||||||
@ReactProp(name = "format")
|
@ReactProp(name = "format")
|
||||||
fun setFormat(view: CameraView, format: ReadableMap?) {
|
fun setFormat(view: CameraView, format: ReadableMap?) {
|
||||||
if (format != null) {
|
view.format = format
|
||||||
val newFormat = CameraDeviceFormat.fromJSValue(format)
|
|
||||||
view.format = newFormat
|
|
||||||
} else {
|
|
||||||
view.format = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "resizeMode")
|
@ReactProp(name = "resizeMode")
|
||||||
fun setResizeMode(view: CameraView, resizeMode: String?) {
|
fun setResizeMode(view: CameraView, resizeMode: String) {
|
||||||
if (resizeMode != null) {
|
val newMode = ResizeMode.fromUnionValue(resizeMode)
|
||||||
val newMode = ResizeMode.fromUnionValue(resizeMode)
|
view.resizeMode = newMode
|
||||||
view.resizeMode = newMode
|
|
||||||
} else {
|
|
||||||
view.resizeMode = ResizeMode.COVER
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Change when TurboModules release.
|
// TODO: Change when TurboModules release.
|
||||||
@ -139,34 +120,30 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
view.fps = if (fps > 0) fps else null
|
view.fps = if (fps > 0) fps else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "photoHdr")
|
@ReactProp(name = "photoHdr", defaultBoolean = false)
|
||||||
fun setPhotoHdr(view: CameraView, photoHdr: Boolean) {
|
fun setPhotoHdr(view: CameraView, photoHdr: Boolean) {
|
||||||
view.photoHdr = photoHdr
|
view.photoHdr = photoHdr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "videoHdr")
|
@ReactProp(name = "videoHdr", defaultBoolean = false)
|
||||||
fun setVideoHdr(view: CameraView, videoHdr: Boolean) {
|
fun setVideoHdr(view: CameraView, videoHdr: Boolean) {
|
||||||
view.videoHdr = videoHdr
|
view.videoHdr = videoHdr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "lowLightBoost")
|
@ReactProp(name = "lowLightBoost")
|
||||||
fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean) {
|
fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) {
|
||||||
view.lowLightBoost = lowLightBoost
|
view.lowLightBoost = lowLightBoost
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "isActive")
|
@ReactProp(name = "isActive", defaultBoolean = false)
|
||||||
fun setIsActive(view: CameraView, isActive: Boolean) {
|
fun setIsActive(view: CameraView, isActive: Boolean) {
|
||||||
view.isActive = isActive
|
view.isActive = isActive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "torch")
|
@ReactProp(name = "torch")
|
||||||
fun setTorch(view: CameraView, torch: String?) {
|
fun setTorch(view: CameraView, torch: String) {
|
||||||
if (torch != null) {
|
val newMode = Torch.fromUnionValue(torch)
|
||||||
val newMode = Torch.fromUnionValue(torch)
|
view.torch = newMode
|
||||||
view.torch = newMode
|
|
||||||
} else {
|
|
||||||
view.torch = Torch.OFF
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "zoom")
|
@ReactProp(name = "zoom")
|
||||||
@ -181,23 +158,14 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
|
|||||||
|
|
||||||
@ReactProp(name = "orientation")
|
@ReactProp(name = "orientation")
|
||||||
fun setOrientation(view: CameraView, orientation: String?) {
|
fun setOrientation(view: CameraView, orientation: String?) {
|
||||||
if (orientation != null) {
|
val newMode = Orientation.fromUnionValue(orientation)
|
||||||
val newMode = Orientation.fromUnionValue(orientation)
|
view.orientation = newMode
|
||||||
Log.i(TAG, "Orientation set to: $newMode")
|
|
||||||
view.orientation = newMode
|
|
||||||
} else {
|
|
||||||
view.orientation = Orientation.PORTRAIT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "codeScannerOptions")
|
@ReactProp(name = "codeScannerOptions")
|
||||||
fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap?) {
|
fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) {
|
||||||
if (codeScannerOptions != null) {
|
val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions)
|
||||||
val newCodeScannerOptions = CodeScannerOptions.fromJSValue(codeScannerOptions)
|
view.codeScannerOptions = newCodeScannerOptions
|
||||||
view.codeScannerOptions = newCodeScannerOptions
|
|
||||||
} else {
|
|
||||||
view.codeScannerOptions = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -95,12 +95,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
|
|
||||||
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
|
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun startRecording(viewTag: Int, jsOptions: ReadableMap, filePath: String, onRecordCallback: Callback) {
|
fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val view = findCameraView(viewTag)
|
val view = findCameraView(viewTag)
|
||||||
try {
|
try {
|
||||||
val options = RecordVideoOptions(jsOptions)
|
val options = RecordVideoOptions(jsOptions)
|
||||||
view.startRecording(options, filePath, onRecordCallback)
|
view.startRecording(options, onRecordCallback)
|
||||||
} catch (error: CameraError) {
|
} catch (error: CameraError) {
|
||||||
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
|
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
|
||||||
onRecordCallback(null, map)
|
onRecordCallback(null, map)
|
||||||
|
@ -44,7 +44,7 @@ data class CameraConfiguration(
|
|||||||
// Output<T> types, those need to be comparable
|
// Output<T> types, those need to be comparable
|
||||||
data class CodeScanner(val codeTypes: List<CodeType>)
|
data class CodeScanner(val codeTypes: List<CodeType>)
|
||||||
data class Photo(val enableHdr: Boolean)
|
data class Photo(val enableHdr: Boolean)
|
||||||
data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean, val enableGpuBuffers: Boolean)
|
data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean)
|
||||||
data class Audio(val nothing: Unit)
|
data class Audio(val nothing: Unit)
|
||||||
data class Preview(val surface: Surface)
|
data class Preview(val surface: Surface)
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ data class CameraConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Difference(
|
data class Difference(
|
||||||
// Input Camera (cameraId)
|
// Input Camera (cameraId, isActive)
|
||||||
val deviceChanged: Boolean,
|
val deviceChanged: Boolean,
|
||||||
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
|
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
|
||||||
val outputsChanged: Boolean,
|
val outputsChanged: Boolean,
|
||||||
@ -75,17 +75,14 @@ data class CameraConfiguration(
|
|||||||
val sidePropsChanged: Boolean,
|
val sidePropsChanged: Boolean,
|
||||||
// (isActive) changed
|
// (isActive) changed
|
||||||
val isActiveChanged: Boolean
|
val isActiveChanged: Boolean
|
||||||
) {
|
)
|
||||||
val hasChanges: Boolean
|
|
||||||
get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
|
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
|
||||||
|
|
||||||
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
|
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
|
||||||
// input device
|
// input device
|
||||||
val deviceChanged = left?.cameraId != right.cameraId
|
val deviceChanged = left?.cameraId != right.cameraId || left?.isActive != right.isActive
|
||||||
|
|
||||||
// outputs
|
// outputs
|
||||||
val outputsChanged = deviceChanged ||
|
val outputsChanged = deviceChanged ||
|
||||||
@ -104,7 +101,7 @@ data class CameraConfiguration(
|
|||||||
left.videoStabilizationMode != right.videoStabilizationMode ||
|
left.videoStabilizationMode != right.videoStabilizationMode ||
|
||||||
left.exposure != right.exposure
|
left.exposure != right.exposure
|
||||||
|
|
||||||
val isActiveChanged = sidePropsChanged || left?.isActive != right.isActive
|
val isActiveChanged = left?.isActive != right.isActive
|
||||||
|
|
||||||
return Difference(
|
return Difference(
|
||||||
deviceChanged,
|
deviceChanged,
|
||||||
|
@ -1,24 +1,17 @@
|
|||||||
package com.mrousavy.camera.core
|
package com.mrousavy.camera.core
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CameraExtensionCharacteristics
|
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
import android.hardware.camera2.CameraMetadata
|
import android.hardware.camera2.CameraMetadata
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import android.util.Range
|
import android.util.Range
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.util.SizeF
|
|
||||||
import android.view.SurfaceHolder
|
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.ReadableArray
|
import com.facebook.react.bridge.ReadableArray
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.mrousavy.camera.extensions.bigger
|
|
||||||
import com.mrousavy.camera.extensions.getPhotoSizes
|
import com.mrousavy.camera.extensions.getPhotoSizes
|
||||||
import com.mrousavy.camera.extensions.getVideoSizes
|
import com.mrousavy.camera.extensions.getVideoSizes
|
||||||
import com.mrousavy.camera.extensions.smaller
|
|
||||||
import com.mrousavy.camera.extensions.toJSValue
|
import com.mrousavy.camera.extensions.toJSValue
|
||||||
import com.mrousavy.camera.types.AutoFocusSystem
|
import com.mrousavy.camera.types.AutoFocusSystem
|
||||||
import com.mrousavy.camera.types.DeviceType
|
import com.mrousavy.camera.types.DeviceType
|
||||||
@ -27,127 +20,65 @@ import com.mrousavy.camera.types.LensFacing
|
|||||||
import com.mrousavy.camera.types.Orientation
|
import com.mrousavy.camera.types.Orientation
|
||||||
import com.mrousavy.camera.types.PixelFormat
|
import com.mrousavy.camera.types.PixelFormat
|
||||||
import com.mrousavy.camera.types.VideoStabilizationMode
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
||||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
|
class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) {
|
||||||
companion object {
|
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
||||||
private const val TAG = "CameraDeviceDetails"
|
val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics)
|
||||||
|
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0)
|
||||||
fun getMaximumPreviewSize(): Size {
|
val extensions = getSupportedExtensions()
|
||||||
// See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
|
|
||||||
// According to the Android Developer documentation, PREVIEW streams can have a resolution
|
|
||||||
// of up to the phone's display's resolution, with a maximum of 1920x1080.
|
|
||||||
val display1080p = Size(1920, 1080)
|
|
||||||
val displaySize = Size(
|
|
||||||
Resources.getSystem().displayMetrics.widthPixels,
|
|
||||||
Resources.getSystem().displayMetrics.heightPixels
|
|
||||||
)
|
|
||||||
val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller
|
|
||||||
|
|
||||||
return if (isHighResScreen) display1080p else displaySize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
|
|
||||||
val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) }
|
|
||||||
val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) }
|
|
||||||
val extensions by lazy { getSupportedExtensions() }
|
|
||||||
|
|
||||||
// device characteristics
|
// device characteristics
|
||||||
val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) }
|
val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
|
||||||
val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) }
|
val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT
|
||||||
val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) }
|
val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
|
||||||
val supportsLowLightBoost by lazy {
|
val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT
|
||||||
extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) &&
|
val lensFacing = LensFacing.fromCameraCharacteristics(characteristics)
|
||||||
modes.contains(CameraCharacteristics.CONTROL_MODE_USE_SCENE_MODE)
|
val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
|
||||||
}
|
val focalLengths =
|
||||||
val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) }
|
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
||||||
val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false }
|
// 35mm is the film standard sensor size
|
||||||
val focalLengths by lazy {
|
?: floatArrayOf(35f)
|
||||||
// 35mm is the film standard sensor size
|
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
|
||||||
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f)
|
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
|
||||||
}
|
val minFocusDistance = getMinFocusDistanceCm()
|
||||||
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) ?: SizeF(0f, 0f) }
|
val name = (
|
||||||
val activeSize
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
characteristics.get(CameraCharacteristics.INFO_VERSION)
|
||||||
val sensorOrientation by lazy {
|
} else {
|
||||||
val degrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
null
|
||||||
return@lazy Orientation.fromRotationDegrees(degrees)
|
}
|
||||||
}
|
) ?: "$lensFacing ($cameraId)"
|
||||||
val minFocusDistance by lazy { getMinFocusDistanceCm() }
|
|
||||||
val name by lazy {
|
|
||||||
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
|
|
||||||
return@lazy info ?: "$lensFacing ($cameraId)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// "formats" (all possible configurations for this device)
|
// "formats" (all possible configurations for this device)
|
||||||
val maxDigitalZoom by lazy { characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f }
|
val zoomRange = (
|
||||||
val zoomRange by lazy {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val range = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
return@lazy range ?: Range(1f, maxDigitalZoom)
|
) ?: Range(1f, characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f)
|
||||||
|
val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
|
||||||
|
characteristics.physicalCameraIds
|
||||||
|
} else {
|
||||||
|
setOf(cameraId)
|
||||||
}
|
}
|
||||||
val physicalDevices by lazy {
|
val minZoom = zoomRange.lower.toDouble()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
|
val maxZoom = zoomRange.upper.toDouble()
|
||||||
characteristics.physicalCameraIds
|
|
||||||
} else {
|
|
||||||
setOf(cameraId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val minZoom by lazy { zoomRange.lower.toDouble() }
|
|
||||||
val maxZoom by lazy { zoomRange.upper.toDouble() }
|
|
||||||
|
|
||||||
val cameraConfig by lazy { characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! }
|
val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
||||||
val isoRange by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) }
|
val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0)
|
||||||
val exposureRange by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) }
|
val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0)
|
||||||
val digitalStabilizationModes by lazy {
|
val digitalStabilizationModes =
|
||||||
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
|
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
|
||||||
}
|
val opticalStabilizationModes =
|
||||||
val opticalStabilizationModes by lazy {
|
|
||||||
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0)
|
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0)
|
||||||
}
|
val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR
|
||||||
val supportsPhotoHdr by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_HDR) }
|
val supportsVideoHdr = getHasVideoHdr()
|
||||||
val supportsVideoHdr by lazy { getHasVideoHdr() }
|
val autoFocusSystem = getAutoFocusSystemMode()
|
||||||
val autoFocusSystem by lazy { getAutoFocusSystemMode() }
|
|
||||||
|
|
||||||
val supportsYuvProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING) }
|
|
||||||
val supportsPrivateProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING) }
|
|
||||||
val supportsZsl by lazy { supportsYuvProcessing || supportsPrivateProcessing }
|
|
||||||
|
|
||||||
val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) }
|
|
||||||
val supportsSnapshotCapture by lazy { supportsSnapshotCapture() }
|
|
||||||
|
|
||||||
val supportsFocusRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 }
|
|
||||||
val supportsExposureRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 }
|
|
||||||
val supportsWhiteBalanceRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 }
|
|
||||||
|
|
||||||
val modes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_MODES)?.toList() ?: emptyList() }
|
|
||||||
val afModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList() }
|
|
||||||
val aeModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)?.toList() ?: emptyList() }
|
|
||||||
val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() }
|
|
||||||
|
|
||||||
val availableAberrationModes by lazy {
|
|
||||||
characteristics.get(CameraCharacteristics.COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES)
|
|
||||||
?: intArrayOf()
|
|
||||||
}
|
|
||||||
val availableHotPixelModes by lazy { characteristics.get(CameraCharacteristics.HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES) ?: intArrayOf() }
|
|
||||||
val availableEdgeModes by lazy { characteristics.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES) ?: intArrayOf() }
|
|
||||||
val availableDistortionCorrectionModes by lazy { getAvailableDistortionCorrectionModesOrEmptyArray() }
|
|
||||||
val availableShadingModes by lazy { characteristics.get(CameraCharacteristics.SHADING_AVAILABLE_MODES) ?: intArrayOf() }
|
|
||||||
val availableToneMapModes by lazy { characteristics.get(CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES) ?: intArrayOf() }
|
|
||||||
val availableNoiseReductionModes by lazy {
|
|
||||||
characteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)
|
|
||||||
?: intArrayOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Also add 10-bit YUV here?
|
|
||||||
val videoFormat = ImageFormat.YUV_420_888
|
val videoFormat = ImageFormat.YUV_420_888
|
||||||
val photoFormat = ImageFormat.JPEG
|
|
||||||
|
|
||||||
// get extensions (HDR, Night Mode, ..)
|
// get extensions (HDR, Night Mode, ..)
|
||||||
private fun getSupportedExtensions(): List<Int> =
|
private fun getSupportedExtensions(): List<Int> =
|
||||||
@ -158,13 +89,6 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAvailableDistortionCorrectionModesOrEmptyArray(): IntArray =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
characteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES) ?: intArrayOf()
|
|
||||||
} else {
|
|
||||||
intArrayOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHasVideoHdr(): Boolean {
|
private fun getHasVideoHdr(): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
|
if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
|
||||||
@ -178,19 +102,10 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
private fun getMinFocusDistanceCm(): Double {
|
private fun getMinFocusDistanceCm(): Double {
|
||||||
val distance = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)
|
val distance = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)
|
||||||
if (distance == null || distance == 0f) return 0.0
|
if (distance == null || distance == 0f) return 0.0
|
||||||
if (distance.isNaN() || distance.isInfinite()) return 0.0
|
|
||||||
// distance is in "diopters", meaning 1/meter. Convert to meters, then centi-meters
|
// distance is in "diopters", meaning 1/meter. Convert to meters, then centi-meters
|
||||||
return 1.0 / distance * 100.0
|
return 1.0 / distance * 100.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("RedundantIf")
|
|
||||||
private fun supportsSnapshotCapture(): Boolean {
|
|
||||||
// As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation:
|
|
||||||
if (hardwareLevel == HardwareLevel.LEGACY) return false
|
|
||||||
if (supportsDepthCapture && !isBackwardsCompatible) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createStabilizationModes(): ReadableArray {
|
private fun createStabilizationModes(): ReadableArray {
|
||||||
val array = Arguments.createArray()
|
val array = Arguments.createArray()
|
||||||
digitalStabilizationModes.forEach { videoStabilizationMode ->
|
digitalStabilizationModes.forEach { videoStabilizationMode ->
|
||||||
@ -231,9 +146,6 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getFieldOfView(focalLength: Float): Double {
|
private fun getFieldOfView(focalLength: Float): Double {
|
||||||
if ((sensorSize.width == 0f) || (sensorSize.height == 0f)) {
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
val sensorDiagonal = sqrt((sensorSize.width * sensorSize.width + sensorSize.height * sensorSize.height).toDouble())
|
val sensorDiagonal = sqrt((sensorSize.width * sensorSize.width + sensorSize.height * sensorSize.height).toDouble())
|
||||||
val fovRadians = 2.0 * atan2(sensorDiagonal, (2.0 * focalLength))
|
val fovRadians = 2.0 * atan2(sensorDiagonal, (2.0 * focalLength))
|
||||||
return Math.toDegrees(fovRadians)
|
return Math.toDegrees(fovRadians)
|
||||||
@ -244,31 +156,18 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
return getFieldOfView(smallestFocalLength)
|
return getFieldOfView(smallestFocalLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVideoSizes(format: Int): List<Size> = characteristics.getVideoSizes(cameraId, format)
|
private fun getVideoSizes(): List<Size> = characteristics.getVideoSizes(cameraId, videoFormat)
|
||||||
fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(photoFormat)
|
private fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(ImageFormat.JPEG)
|
||||||
fun getPreviewSizes(): List<Size> {
|
|
||||||
val maximumPreviewSize = getMaximumPreviewSize()
|
|
||||||
return cameraConfig.getOutputSizes(SurfaceHolder::class.java)
|
|
||||||
.filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFormats(): ReadableArray {
|
private fun getFormats(): ReadableArray {
|
||||||
val array = Arguments.createArray()
|
val array = Arguments.createArray()
|
||||||
|
|
||||||
val videoSizes = getVideoSizes(videoFormat)
|
val videoSizes = getVideoSizes()
|
||||||
val photoSizes = getPhotoSizes()
|
val photoSizes = getPhotoSizes()
|
||||||
|
|
||||||
videoSizes.forEach { videoSize ->
|
videoSizes.forEach { videoSize ->
|
||||||
val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize)
|
val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize)
|
||||||
var maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt()
|
val maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt()
|
||||||
val maxEncoderFps = CamcorderProfileUtils.getMaximumFps(cameraId, videoSize)
|
|
||||||
if (maxEncoderFps != null && maxEncoderFps < maxFps) {
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"Camera could do $maxFps FPS at $videoSize, but Media Encoder can only do $maxEncoderFps FPS. Clamping to $maxEncoderFps FPS..."
|
|
||||||
)
|
|
||||||
maxFps = maxEncoderFps
|
|
||||||
}
|
|
||||||
|
|
||||||
photoSizes.forEach { photoSize ->
|
photoSizes.forEach { photoSize ->
|
||||||
val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps))
|
val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps))
|
||||||
@ -276,6 +175,8 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add high-speed video ranges (high-fps / slow-motion)
|
||||||
|
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,14 +223,14 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
map.putBoolean("isMultiCam", isMultiCam)
|
map.putBoolean("isMultiCam", isMultiCam)
|
||||||
map.putBoolean("supportsRawCapture", supportsRawCapture)
|
map.putBoolean("supportsRawCapture", supportsRawCapture)
|
||||||
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
|
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
|
||||||
map.putBoolean("supportsFocus", supportsFocusRegions)
|
map.putBoolean("supportsFocus", true) // I believe every device here supports focussing
|
||||||
map.putDouble("minZoom", minZoom)
|
map.putDouble("minZoom", minZoom)
|
||||||
map.putDouble("maxZoom", maxZoom)
|
map.putDouble("maxZoom", maxZoom)
|
||||||
map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android
|
map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android
|
||||||
map.putDouble("minExposure", exposureRange.lower.toDouble())
|
map.putDouble("minExposure", exposureRange.lower.toDouble())
|
||||||
map.putDouble("maxExposure", exposureRange.upper.toDouble())
|
map.putDouble("maxExposure", exposureRange.upper.toDouble())
|
||||||
map.putString("hardwareLevel", hardwareLevel.unionValue)
|
map.putString("hardwareLevel", hardwareLevel.unionValue)
|
||||||
map.putString("sensorOrientation", sensorOrientation.unionValue)
|
map.putString("sensorOrientation", Orientation.fromRotationDegrees(sensorOrientation).unionValue)
|
||||||
map.putArray("formats", getFormats())
|
map.putArray("formats", getFormats())
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
@ -62,8 +62,6 @@ class FlashUnavailableError :
|
|||||||
"flash-unavailable",
|
"flash-unavailable",
|
||||||
"The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true."
|
"The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true."
|
||||||
)
|
)
|
||||||
class FocusNotSupportedError :
|
|
||||||
CameraError("device", "focus-not-supported", "The currently selected camera device does not support focusing!")
|
|
||||||
|
|
||||||
class CameraNotReadyError :
|
class CameraNotReadyError :
|
||||||
CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
|
CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
|
||||||
@ -73,8 +71,6 @@ class CameraSessionCannotBeConfiguredError(cameraId: String) :
|
|||||||
CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!")
|
CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!")
|
||||||
class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) :
|
class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) :
|
||||||
CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error")
|
CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error")
|
||||||
class NoOutputsError :
|
|
||||||
CameraError("session", "no-outputs", "Cannot create a CameraCaptureSession without any outputs! (PREVIEW, PHOTO, VIDEO, ...)")
|
|
||||||
|
|
||||||
class PropRequiresFormatToBeNonNullError(propName: String) :
|
class PropRequiresFormatToBeNonNullError(propName: String) :
|
||||||
CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!")
|
CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!")
|
||||||
@ -104,8 +100,6 @@ class PhotoNotEnabledError :
|
|||||||
CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
|
CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
|
||||||
class CaptureAbortedError(wasImageCaptured: Boolean) :
|
class CaptureAbortedError(wasImageCaptured: Boolean) :
|
||||||
CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
|
CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
|
||||||
class FocusCanceledError : CameraError("capture", "focus-canceled", "The focus operation was canceled.")
|
|
||||||
class CaptureTimedOutError : CameraError("capture", "timed-out", "The image capture was aborted because it timed out.")
|
|
||||||
class UnknownCaptureError(wasImageCaptured: Boolean) :
|
class UnknownCaptureError(wasImageCaptured: Boolean) :
|
||||||
CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
|
CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
|
||||||
class RecorderError(name: String, extra: Int) :
|
class RecorderError(name: String, extra: Int) :
|
||||||
@ -119,16 +113,6 @@ class RecordingInProgressError :
|
|||||||
"recording-in-progress",
|
"recording-in-progress",
|
||||||
"There is already an active video recording in progress! Did you call startRecording() twice?"
|
"There is already an active video recording in progress! Did you call startRecording() twice?"
|
||||||
)
|
)
|
||||||
class FrameInvalidError :
|
|
||||||
CameraError(
|
|
||||||
"capture",
|
|
||||||
"frame-invalid",
|
|
||||||
"Trying to access an already closed Frame! " +
|
|
||||||
"Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" +
|
|
||||||
"- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" +
|
|
||||||
"- If you want to do async processing, use `runAsync(...)` instead.\n" +
|
|
||||||
"- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`"
|
|
||||||
)
|
|
||||||
|
|
||||||
class CodeTypeNotSupportedError(codeType: String) :
|
class CodeTypeNotSupportedError(codeType: String) :
|
||||||
CameraError(
|
CameraError(
|
||||||
|
@ -5,33 +5,50 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.hardware.camera2.CameraDevice
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CameraMetadata
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
import android.hardware.camera2.CaptureResult
|
import android.hardware.camera2.CaptureResult
|
||||||
import android.hardware.camera2.TotalCaptureResult
|
import android.hardware.camera2.TotalCaptureResult
|
||||||
|
import android.hardware.camera2.params.MeteringRectangle
|
||||||
import android.media.Image
|
import android.media.Image
|
||||||
import android.media.ImageReader
|
import android.media.ImageReader
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Range
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
|
|
||||||
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
|
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
|
||||||
import com.mrousavy.camera.core.outputs.PhotoOutput
|
import com.mrousavy.camera.core.outputs.PhotoOutput
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
||||||
import com.mrousavy.camera.core.outputs.VideoPipelineOutput
|
import com.mrousavy.camera.core.outputs.VideoPipelineOutput
|
||||||
|
import com.mrousavy.camera.extensions.capture
|
||||||
import com.mrousavy.camera.extensions.closestToOrMax
|
import com.mrousavy.camera.extensions.closestToOrMax
|
||||||
|
import com.mrousavy.camera.extensions.createCaptureSession
|
||||||
|
import com.mrousavy.camera.extensions.createPhotoCaptureRequest
|
||||||
|
import com.mrousavy.camera.extensions.getPhotoSizes
|
||||||
|
import com.mrousavy.camera.extensions.getPreviewTargetSize
|
||||||
|
import com.mrousavy.camera.extensions.getVideoSizes
|
||||||
|
import com.mrousavy.camera.extensions.openCamera
|
||||||
|
import com.mrousavy.camera.extensions.setZoom
|
||||||
import com.mrousavy.camera.frameprocessor.Frame
|
import com.mrousavy.camera.frameprocessor.Frame
|
||||||
|
import com.mrousavy.camera.frameprocessor.FrameProcessor
|
||||||
import com.mrousavy.camera.types.Flash
|
import com.mrousavy.camera.types.Flash
|
||||||
import com.mrousavy.camera.types.LensFacing
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
import com.mrousavy.camera.types.Orientation
|
||||||
import com.mrousavy.camera.types.QualityPrioritization
|
import com.mrousavy.camera.types.QualityPrioritization
|
||||||
import com.mrousavy.camera.types.RecordVideoOptions
|
import com.mrousavy.camera.types.RecordVideoOptions
|
||||||
|
import com.mrousavy.camera.types.Torch
|
||||||
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
||||||
import com.mrousavy.camera.utils.ImageFormatUtils
|
import com.mrousavy.camera.utils.ImageFormatUtils
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import java.lang.IllegalStateException
|
||||||
|
import java.util.concurrent.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -40,8 +57,8 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) :
|
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) :
|
||||||
Closeable,
|
CameraManager.AvailabilityCallback(),
|
||||||
PersistentCameraCaptureSession.Callback {
|
Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CameraSession"
|
private const val TAG = "CameraSession"
|
||||||
}
|
}
|
||||||
@ -50,7 +67,14 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
private var configuration: CameraConfiguration? = null
|
private var configuration: CameraConfiguration? = null
|
||||||
|
|
||||||
// Camera State
|
// Camera State
|
||||||
private val captureSession = PersistentCameraCaptureSession(cameraManager, this)
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
cameraDeviceDetails = if (value != null) CameraDeviceDetails(cameraManager, value.id) else null
|
||||||
|
}
|
||||||
|
private var cameraDeviceDetails: CameraDeviceDetails? = null
|
||||||
|
private var captureSession: CameraCaptureSession? = null
|
||||||
|
private var previewRequest: CaptureRequest.Builder? = null
|
||||||
private var photoOutput: PhotoOutput? = null
|
private var photoOutput: PhotoOutput? = null
|
||||||
private var videoOutput: VideoPipelineOutput? = null
|
private var videoOutput: VideoPipelineOutput? = null
|
||||||
private var codeScannerOutput: BarcodeScannerOutput? = null
|
private var codeScannerOutput: BarcodeScannerOutput? = null
|
||||||
@ -78,6 +102,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
field = value
|
field = value
|
||||||
updateVideoOutputs()
|
updateVideoOutputs()
|
||||||
}
|
}
|
||||||
|
var frameProcessor: FrameProcessor? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
updateVideoOutputs()
|
||||||
|
}
|
||||||
|
|
||||||
val orientation: Orientation
|
val orientation: Orientation
|
||||||
get() {
|
get() {
|
||||||
@ -87,9 +116,14 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
return Orientation.fromRotationDegrees(sensorRotation)
|
return Orientation.fromRotationDegrees(sensorRotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler)
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Log.i(TAG, "Closing CameraSession...")
|
Log.i(TAG, "Closing CameraSession...")
|
||||||
isDestroyed = true
|
isDestroyed = true
|
||||||
|
cameraManager.unregisterAvailabilityCallback(this)
|
||||||
runBlocking {
|
runBlocking {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
destroy()
|
destroy()
|
||||||
@ -99,6 +133,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
Log.i(TAG, "CameraSession closed!")
|
Log.i(TAG, "CameraSession closed!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCameraAvailable(cameraId: String) {
|
||||||
|
super.onCameraAvailable(cameraId)
|
||||||
|
if (this.configuration?.cameraId == cameraId && cameraDevice == null && configuration?.isActive == true) {
|
||||||
|
Log.i(TAG, "Camera #$cameraId is now available again, trying to re-open it now...")
|
||||||
|
coroutineScope.launch {
|
||||||
|
configure {
|
||||||
|
// re-open CameraDevice if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
|
suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
|
||||||
Log.i(TAG, "configure { ... }: Waiting for lock...")
|
Log.i(TAG, "configure { ... }: Waiting for lock...")
|
||||||
|
|
||||||
@ -107,12 +153,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val config = CameraConfiguration.copyOf(this.configuration)
|
val config = CameraConfiguration.copyOf(this.configuration)
|
||||||
lambda(config)
|
lambda(config)
|
||||||
val diff = CameraConfiguration.difference(this.configuration, config)
|
val diff = CameraConfiguration.difference(this.configuration, config)
|
||||||
this.configuration = config
|
|
||||||
|
|
||||||
if (!diff.hasChanges) {
|
|
||||||
Log.i(TAG, "Nothing changed, aborting configure { ... }")
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDestroyed) {
|
if (isDestroyed) {
|
||||||
Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }")
|
Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }")
|
||||||
@ -122,11 +162,29 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff")
|
Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
captureSession.withConfiguration {
|
val needsRebuild = cameraDevice == null || captureSession == null
|
||||||
|
if (needsRebuild) {
|
||||||
|
Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since cameraDevice and captureSession are OS resources, we have three possible paths here:
|
||||||
|
if (needsRebuild) {
|
||||||
|
if (config.isActive) {
|
||||||
|
// A: The Camera has been torn down by the OS and we want it to be active - rebuild everything
|
||||||
|
Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...")
|
||||||
|
configureCameraDevice(config)
|
||||||
|
configureOutputs(config)
|
||||||
|
configureCaptureRequest(config)
|
||||||
|
} else {
|
||||||
|
// B: The Camera has been torn down by the OS but it's currently in the background - ignore this
|
||||||
|
Log.i(TAG, "CameraDevice and CameraCaptureSession is torn down but Camera is not active, skipping update...")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// C: The Camera has not been torn down and we just want to update some props - update incrementally
|
||||||
// Build up session or update any props
|
// Build up session or update any props
|
||||||
if (diff.deviceChanged) {
|
if (diff.deviceChanged) {
|
||||||
// 1. cameraId changed, open device
|
// 1. cameraId changed, open device
|
||||||
configureInput(config)
|
configureCameraDevice(config)
|
||||||
}
|
}
|
||||||
if (diff.outputsChanged) {
|
if (diff.outputsChanged) {
|
||||||
// 2. outputs changed, build new session
|
// 2. outputs changed, build new session
|
||||||
@ -136,18 +194,10 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
// 3. zoom etc changed, update repeating request
|
// 3. zoom etc changed, update repeating request
|
||||||
configureCaptureRequest(config)
|
configureCaptureRequest(config)
|
||||||
}
|
}
|
||||||
if (diff.isActiveChanged) {
|
|
||||||
// 4. Either start or stop the session
|
|
||||||
val isActive = config.isActive && config.preview.isEnabled
|
|
||||||
captureSession.setIsActive(isActive)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(
|
Log.i(TAG, "Successfully updated CameraSession Configuration! isActive: ${config.isActive}")
|
||||||
TAG,
|
this.configuration = config
|
||||||
"configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})"
|
|
||||||
)
|
|
||||||
isRunning = captureSession.isRunning
|
|
||||||
|
|
||||||
// Notify about Camera initialization
|
// Notify about Camera initialization
|
||||||
if (diff.deviceChanged) {
|
if (diff.deviceChanged) {
|
||||||
@ -162,7 +212,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
|
|
||||||
private fun destroy() {
|
private fun destroy() {
|
||||||
Log.i(TAG, "Destroying session..")
|
Log.i(TAG, "Destroying session..")
|
||||||
captureSession.close()
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
|
||||||
photoOutput?.close()
|
photoOutput?.close()
|
||||||
photoOutput = null
|
photoOutput = null
|
||||||
@ -218,20 +269,66 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
Log.i(TAG, "Preview Output destroyed!")
|
Log.i(TAG, "Preview Output destroyed!")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureInput(configuration: CameraConfiguration) {
|
/**
|
||||||
Log.i(TAG, "Configuring inputs for CameraSession...")
|
* Set up the `CameraDevice` (`cameraId`)
|
||||||
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
|
*/
|
||||||
val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
private suspend fun configureCameraDevice(configuration: CameraConfiguration) {
|
||||||
if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
|
if (!configuration.isActive) {
|
||||||
|
// If isActive=false, we don't care if the device is opened or closed.
|
||||||
|
// Android OS can close the CameraDevice if it needs it, otherwise we keep it warm.
|
||||||
|
Log.i(TAG, "isActive is false, skipping CameraDevice configuration.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraDevice != null) {
|
||||||
|
// Close existing device
|
||||||
|
Log.i(TAG, "Closing previous Camera #${cameraDevice?.id}...")
|
||||||
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
isRunning = false
|
isRunning = false
|
||||||
captureSession.setInput(cameraId)
|
|
||||||
|
// Check Camera Permission
|
||||||
|
val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||||
|
if (cameraPermission != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
|
||||||
|
|
||||||
|
// Open new device
|
||||||
|
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
|
||||||
|
Log.i(TAG, "Configuring Camera #$cameraId...")
|
||||||
|
cameraDevice = cameraManager.openCamera(cameraId, { device, error ->
|
||||||
|
if (cameraDevice != device) {
|
||||||
|
// a previous device has been disconnected, but we already have a new one.
|
||||||
|
// this is just normal behavior
|
||||||
|
return@openCamera
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cameraDevice = null
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
Log.e(TAG, "Camera #${device.id} has been unexpectedly disconnected!", error)
|
||||||
|
callback.onError(error)
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Camera #${device.id} has been gracefully disconnected!")
|
||||||
|
}
|
||||||
|
}, CameraQueues.cameraQueue)
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully configured Camera #$cameraId!")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
|
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
|
||||||
*/
|
*/
|
||||||
private suspend fun configureOutputs(configuration: CameraConfiguration) {
|
private suspend fun configureOutputs(configuration: CameraConfiguration) {
|
||||||
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
|
if (!configuration.isActive) {
|
||||||
|
Log.i(TAG, "isActive is false, skipping CameraCaptureSession configuration.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val cameraDevice = cameraDevice
|
||||||
|
if (cameraDevice == null) {
|
||||||
|
Log.i(TAG, "CameraSession hasn't configured a CameraDevice, skipping session configuration...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy previous outputs
|
// Destroy previous outputs
|
||||||
Log.i(TAG, "Destroying previous outputs...")
|
Log.i(TAG, "Destroying previous outputs...")
|
||||||
@ -243,20 +340,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
codeScannerOutput = null
|
codeScannerOutput = null
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
||||||
val deviceDetails = CameraDeviceDetails(cameraManager, cameraId)
|
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
|
||||||
val format = configuration.format
|
val format = configuration.format
|
||||||
|
|
||||||
Log.i(TAG, "Creating outputs for Camera #$cameraId...")
|
Log.i(TAG, "Creating outputs for Camera #${cameraDevice.id}...")
|
||||||
|
|
||||||
val isSelfie = deviceDetails.lensFacing == LensFacing.FRONT
|
val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||||
|
|
||||||
val outputs = mutableListOf<SurfaceOutput>()
|
val outputs = mutableListOf<SurfaceOutput>()
|
||||||
|
|
||||||
// Photo Output
|
// Photo Output
|
||||||
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
|
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
|
||||||
if (photo != null) {
|
if (photo != null) {
|
||||||
val imageFormat = deviceDetails.photoFormat
|
val imageFormat = ImageFormat.JPEG
|
||||||
val sizes = deviceDetails.getPhotoSizes()
|
val sizes = characteristics.getPhotoSizes(imageFormat)
|
||||||
val size = sizes.closestToOrMax(format?.photoSize)
|
val size = sizes.closestToOrMax(format?.photoSize)
|
||||||
val maxImages = 10
|
val maxImages = 10
|
||||||
|
|
||||||
@ -276,7 +373,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
|
val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
val imageFormat = video.config.pixelFormat.toImageFormat()
|
val imageFormat = video.config.pixelFormat.toImageFormat()
|
||||||
val sizes = deviceDetails.getVideoSizes(imageFormat)
|
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
||||||
val size = sizes.closestToOrMax(format?.videoSize)
|
val size = sizes.closestToOrMax(format?.videoSize)
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
||||||
@ -286,7 +383,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
video.config.pixelFormat,
|
video.config.pixelFormat,
|
||||||
isSelfie,
|
isSelfie,
|
||||||
video.config.enableFrameProcessor,
|
video.config.enableFrameProcessor,
|
||||||
video.config.enableGpuBuffers,
|
|
||||||
callback
|
callback
|
||||||
)
|
)
|
||||||
val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr)
|
val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr)
|
||||||
@ -299,8 +395,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
if (preview != null) {
|
if (preview != null) {
|
||||||
// Compute Preview Size based on chosen video size
|
// Compute Preview Size based on chosen video size
|
||||||
val videoSize = videoOutput?.size ?: format?.videoSize
|
val videoSize = videoOutput?.size ?: format?.videoSize
|
||||||
val sizes = deviceDetails.getPreviewSizes()
|
val size = characteristics.getPreviewTargetSize(videoSize)
|
||||||
val size = sizes.closestToOrMax(videoSize)
|
|
||||||
|
|
||||||
val enableHdr = video?.config?.enableHdr ?: false
|
val enableHdr = video?.config?.enableHdr ?: false
|
||||||
|
|
||||||
@ -312,8 +407,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
enableHdr
|
enableHdr
|
||||||
)
|
)
|
||||||
outputs.add(output)
|
outputs.add(output)
|
||||||
|
// Size is usually landscape, so we flip it here
|
||||||
previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation)
|
previewView?.size = Size(size.height, size.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeScanner Output
|
// CodeScanner Output
|
||||||
@ -326,7 +421,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
}
|
}
|
||||||
|
|
||||||
val imageFormat = ImageFormat.YUV_420_888
|
val imageFormat = ImageFormat.YUV_420_888
|
||||||
val sizes = deviceDetails.getVideoSizes(imageFormat)
|
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
||||||
val size = sizes.closestToOrMax(Size(1280, 720))
|
val size = sizes.closestToOrMax(Size(1280, 720))
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
||||||
@ -337,63 +432,175 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
captureSession.setOutputs(outputs)
|
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
|
||||||
|
if (this.captureSession != session) {
|
||||||
|
// a previous session has been closed, but we already have a new one.
|
||||||
|
// this is just normal behavior
|
||||||
|
return@createCaptureSession
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!")
|
// onClosed
|
||||||
|
this.captureSession = null
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
Log.i(TAG, "Camera Session $session has been closed.")
|
||||||
|
}, CameraQueues.cameraQueue)
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!")
|
||||||
|
|
||||||
// Update Frame Processor and RecordingSession for newly changed output
|
// Update Frame Processor and RecordingSession for newly changed output
|
||||||
updateVideoOutputs()
|
updateVideoOutputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureCaptureRequest(config: CameraConfiguration) {
|
private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest {
|
||||||
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
|
val deviceDetails = cameraDeviceDetails ?: CameraDeviceDetails(cameraManager, device.id)
|
||||||
val enableVideo = video != null
|
|
||||||
val enableVideoHdr = video?.config?.enableHdr == true
|
|
||||||
|
|
||||||
captureSession.setRepeatingRequest(
|
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
|
||||||
RepeatingCaptureRequest(
|
val captureRequest = device.createCaptureRequest(template)
|
||||||
enableVideo,
|
|
||||||
config.torch,
|
targets.forEach { t -> captureRequest.addTarget(t) }
|
||||||
config.fps,
|
|
||||||
config.videoStabilizationMode,
|
val format = config.format
|
||||||
enableVideoHdr,
|
|
||||||
config.enableLowLightBoost,
|
// Set FPS
|
||||||
config.exposure,
|
val fps = config.fps
|
||||||
config.zoom,
|
if (fps != null) {
|
||||||
config.format
|
if (format == null) throw PropRequiresFormatToBeNonNullError("fps")
|
||||||
)
|
if (format.maxFps < fps) throw InvalidFpsError(fps)
|
||||||
)
|
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Video Stabilization
|
||||||
|
if (config.videoStabilizationMode != VideoStabilizationMode.OFF) {
|
||||||
|
if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode")
|
||||||
|
if (!format.videoStabilizationModes.contains(
|
||||||
|
config.videoStabilizationMode
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw InvalidVideoStabilizationMode(config.videoStabilizationMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (config.videoStabilizationMode) {
|
||||||
|
VideoStabilizationMode.OFF -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
VideoStabilizationMode.STANDARD -> {
|
||||||
|
val mode = if (Build.VERSION.SDK_INT >=
|
||||||
|
Build.VERSION_CODES.TIRAMISU
|
||||||
|
) {
|
||||||
|
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
|
||||||
|
} else {
|
||||||
|
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON
|
||||||
|
}
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode)
|
||||||
|
}
|
||||||
|
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
|
||||||
|
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set HDR
|
||||||
|
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
|
||||||
|
val videoHdr = video?.config?.enableHdr
|
||||||
|
if (videoHdr == true) {
|
||||||
|
if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr")
|
||||||
|
if (!format.supportsVideoHdr) throw InvalidVideoHdrError()
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
||||||
|
} else if (config.enableLowLightBoost) {
|
||||||
|
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Exposure Bias
|
||||||
|
val exposure = config.exposure?.toInt()
|
||||||
|
if (exposure != null) {
|
||||||
|
val clamped = deviceDetails.exposureRange.clamp(exposure)
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Zoom
|
||||||
|
// TODO: Cache camera characteristics? Check perf.
|
||||||
|
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
|
||||||
|
captureRequest.setZoom(config.zoom, cameraCharacteristics)
|
||||||
|
|
||||||
|
// Set Torch
|
||||||
|
if (config.torch == Torch.ON) {
|
||||||
|
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
|
||||||
|
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start repeating request if the Camera is active
|
||||||
|
return captureRequest.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureCaptureRequest(config: CameraConfiguration) {
|
||||||
|
val captureSession = captureSession
|
||||||
|
|
||||||
|
if (!config.isActive) {
|
||||||
|
isRunning = false
|
||||||
|
try {
|
||||||
|
captureSession?.stopRepeating()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// ignore - captureSession is already closed.
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (captureSession == null) {
|
||||||
|
Log.i(TAG, "CameraSession hasn't configured the capture session, skipping CaptureRequest...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val preview = config.preview as? CameraConfiguration.Output.Enabled<CameraConfiguration.Preview>
|
||||||
|
val previewSurface = preview?.config?.surface
|
||||||
|
val targets = listOfNotNull(previewSurface, videoOutput?.surface, codeScannerOutput?.surface)
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
Log.i(TAG, "CameraSession has no repeating outputs (Preview, Video, CodeScanner), skipping CaptureRequest...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = createRepeatingRequest(captureSession.device, targets, config)
|
||||||
|
captureSession.setRepeatingRequest(request, null, null)
|
||||||
|
isRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun takePhoto(
|
suspend fun takePhoto(
|
||||||
qualityPrioritization: QualityPrioritization,
|
qualityPrioritization: QualityPrioritization,
|
||||||
flash: Flash,
|
flashMode: Flash,
|
||||||
enableShutterSound: Boolean,
|
enableShutterSound: Boolean,
|
||||||
|
enableRedEyeReduction: Boolean,
|
||||||
enableAutoStabilization: Boolean,
|
enableAutoStabilization: Boolean,
|
||||||
enablePrecapture: Boolean,
|
|
||||||
outputOrientation: Orientation
|
outputOrientation: Orientation
|
||||||
): CapturedPhoto {
|
): CapturedPhoto {
|
||||||
|
val captureSession = captureSession ?: throw CameraNotReadyError()
|
||||||
val photoOutput = photoOutput ?: throw PhotoNotEnabledError()
|
val photoOutput = photoOutput ?: throw PhotoNotEnabledError()
|
||||||
|
|
||||||
Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...")
|
Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...")
|
||||||
val result = captureSession.capture(
|
|
||||||
|
val zoom = configuration?.zoom ?: 1f
|
||||||
|
|
||||||
|
val cameraCharacteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
|
||||||
|
val orientation = outputOrientation.toSensorRelativeOrientation(cameraCharacteristics)
|
||||||
|
val captureRequest = captureSession.device.createPhotoCaptureRequest(
|
||||||
|
cameraManager,
|
||||||
|
photoOutput.surface,
|
||||||
|
zoom,
|
||||||
qualityPrioritization,
|
qualityPrioritization,
|
||||||
flash,
|
flashMode,
|
||||||
|
enableRedEyeReduction,
|
||||||
enableAutoStabilization,
|
enableAutoStabilization,
|
||||||
photoOutput.enableHdr,
|
photoOutput.enableHdr,
|
||||||
outputOrientation,
|
orientation
|
||||||
enableShutterSound,
|
|
||||||
enablePrecapture
|
|
||||||
)
|
)
|
||||||
|
Log.i(TAG, "Photo capture 1/3 - starting capture...")
|
||||||
|
val result = captureSession.capture(captureRequest, enableShutterSound)
|
||||||
|
val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!!
|
||||||
|
Log.i(TAG, "Photo capture 2/3 complete - received metadata with timestamp $timestamp")
|
||||||
try {
|
try {
|
||||||
val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!!
|
|
||||||
Log.i(TAG, "Photo capture 2/3 - waiting for image with timestamp $timestamp now...")
|
|
||||||
val image = photoOutputSynchronizer.await(timestamp)
|
val image = photoOutputSynchronizer.await(timestamp)
|
||||||
|
|
||||||
Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...")
|
val isMirrored = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||||
val deviceDetails = captureSession.getActiveDeviceDetails()
|
|
||||||
val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT
|
Log.i(TAG, "Photo capture 3/3 complete - received ${image.width} x ${image.height} image.")
|
||||||
return CapturedPhoto(image, result, orientation, isMirrored, image.format)
|
return CapturedPhoto(image, result, orientation, isMirrored, image.format)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw CaptureAbortedError(false)
|
throw CaptureAbortedError(false)
|
||||||
@ -414,27 +621,25 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
suspend fun startRecording(
|
suspend fun startRecording(
|
||||||
enableAudio: Boolean,
|
enableAudio: Boolean,
|
||||||
options: RecordVideoOptions,
|
options: RecordVideoOptions,
|
||||||
filePath: String,
|
|
||||||
callback: (video: RecordingSession.Video) -> Unit,
|
callback: (video: RecordingSession.Video) -> Unit,
|
||||||
onError: (error: CameraError) -> Unit
|
onError: (error: CameraError) -> Unit
|
||||||
) {
|
) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
if (recording != null) throw RecordingInProgressError()
|
if (recording != null) throw RecordingInProgressError()
|
||||||
val videoOutput = videoOutput ?: throw VideoNotEnabledError()
|
val videoOutput = videoOutput ?: throw VideoNotEnabledError()
|
||||||
val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError()
|
val cameraDevice = cameraDevice ?: throw CameraNotReadyError()
|
||||||
|
|
||||||
val fps = configuration?.fps ?: 30
|
val fps = configuration?.fps ?: 30
|
||||||
|
|
||||||
val recording = RecordingSession(
|
val recording = RecordingSession(
|
||||||
context,
|
context,
|
||||||
cameraId,
|
cameraDevice.id,
|
||||||
videoOutput.size,
|
videoOutput.size,
|
||||||
enableAudio,
|
enableAudio,
|
||||||
fps,
|
fps,
|
||||||
videoOutput.enableHdr,
|
videoOutput.enableHdr,
|
||||||
orientation,
|
orientation,
|
||||||
options,
|
options,
|
||||||
filePath,
|
|
||||||
callback,
|
callback,
|
||||||
onError,
|
onError,
|
||||||
this.callback,
|
this.callback,
|
||||||
@ -467,16 +672,40 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(error: Throwable) {
|
suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!")
|
||||||
callback.onError(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun focus(x: Int, y: Int) {
|
private suspend fun focus(point: Point) {
|
||||||
val previewView = previewView ?: throw CameraNotReadyError()
|
mutex.withLock {
|
||||||
val deviceDetails = captureSession.getActiveDeviceDetails() ?: throw CameraNotReadyError()
|
// TODO: Fix this method
|
||||||
|
val captureSession = captureSession ?: throw CameraNotReadyError()
|
||||||
|
val request = previewRequest ?: throw CameraNotReadyError()
|
||||||
|
|
||||||
val cameraPoint = previewView.convertLayerPointToCameraCoordinates(Point(x, y), deviceDetails)
|
val weight = MeteringRectangle.METERING_WEIGHT_MAX - 1
|
||||||
captureSession.focus(cameraPoint)
|
val focusAreaTouch = MeteringRectangle(point, Size(150, 150), weight)
|
||||||
|
|
||||||
|
// Quickly pause preview
|
||||||
|
captureSession.stopRepeating()
|
||||||
|
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
|
||||||
|
captureSession.capture(request.build(), null, null)
|
||||||
|
|
||||||
|
// Add AF trigger with focus region
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
|
||||||
|
val maxSupportedFocusRegions = characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0
|
||||||
|
if (maxSupportedFocusRegions >= 1) {
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(focusAreaTouch))
|
||||||
|
}
|
||||||
|
request.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START)
|
||||||
|
|
||||||
|
captureSession.capture(request.build(), false)
|
||||||
|
|
||||||
|
// Resume preview
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
|
||||||
|
captureSession.setRepeatingRequest(request.build(), null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CapturedPhoto(
|
data class CapturedPhoto(
|
||||||
|
@ -27,11 +27,10 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
bitRate: Int,
|
bitRate: Int,
|
||||||
options: RecordVideoOptions,
|
options: RecordVideoOptions,
|
||||||
outputDirectory: File,
|
outputDirectory: File,
|
||||||
iFrameInterval: Int = 5
|
iFrameInterval: Int = 3
|
||||||
): ChunkedRecordingManager {
|
): ChunkedRecordingManager {
|
||||||
val mimeType = options.videoCodec.toMimeType()
|
val mimeType = options.videoCodec.toMimeType()
|
||||||
val cameraOrientationDegrees = cameraOrientation.toDegrees()
|
val orientationDegrees = cameraOrientation.toDegrees()
|
||||||
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees();
|
|
||||||
val (width, height) = if (cameraOrientation.isLandscape()) {
|
val (width, height) = if (cameraOrientation.isLandscape()) {
|
||||||
size.height to size.width
|
size.height to size.width
|
||||||
} else {
|
} else {
|
||||||
@ -55,13 +54,11 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
||||||
|
|
||||||
Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees")
|
Log.i(TAG, "Video Format: $format, orientation $cameraOrientation")
|
||||||
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
|
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
|
||||||
// we can use for input and wrap it with a class that handles the EGL work.
|
// we can use for input and wrap it with a class that handles the EGL work.
|
||||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
return ChunkedRecordingManager(
|
return ChunkedRecordingManager(codec, outputDirectory, 0, iFrameInterval, callbacks)
|
||||||
codec, outputDirectory, recordingOrientationDegrees, iFrameInterval, callbacks
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +80,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Muxer specific
|
// Muxer specific
|
||||||
private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat, val callbacks: CameraSession.Callback,) {
|
private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat) {
|
||||||
val videoTrack: Int = muxer.addTrack(encodedFormat)
|
val videoTrack: Int = muxer.addTrack(encodedFormat)
|
||||||
val startTimeUs: Long = startTimeUs
|
val startTimeUs: Long = startTimeUs
|
||||||
|
|
||||||
@ -95,14 +92,16 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
fun finish() {
|
fun finish() {
|
||||||
muxer.stop()
|
muxer.stop()
|
||||||
muxer.release()
|
muxer.release()
|
||||||
callbacks.onVideoChunkReady(filepath, chunkIndex)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var muxerContext: MuxerContext? = null
|
private var muxerContext: MuxerContext? = null
|
||||||
|
|
||||||
private fun createNextMuxer(bufferInfo: BufferInfo) {
|
private fun createNextMuxer(bufferInfo: BufferInfo) {
|
||||||
muxerContext?.finish()
|
muxerContext?.let {
|
||||||
|
it.finish()
|
||||||
|
this.callbacks.onVideoChunkReady(it.filepath, it.chunkIndex)
|
||||||
|
}
|
||||||
chunkIndex++
|
chunkIndex++
|
||||||
|
|
||||||
val newFileName = "$chunkIndex.mp4"
|
val newFileName = "$chunkIndex.mp4"
|
||||||
@ -114,7 +113,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
)
|
)
|
||||||
muxer.setOrientationHint(orientationHint)
|
muxer.setOrientationHint(orientationHint)
|
||||||
muxerContext = MuxerContext(
|
muxerContext = MuxerContext(
|
||||||
muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!, this.callbacks
|
muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class CodeScannerPipeline(
|
|||||||
var isBusy = false
|
var isBusy = false
|
||||||
imageReader = ImageReader.newInstance(size.width, size.height, format, MAX_IMAGES)
|
imageReader = ImageReader.newInstance(size.width, size.height, format, MAX_IMAGES)
|
||||||
imageReader.setOnImageAvailableListener({ reader ->
|
imageReader.setOnImageAvailableListener({ reader ->
|
||||||
val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
|
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
||||||
|
|
||||||
if (isBusy) {
|
if (isBusy) {
|
||||||
// We're currently executing on a previous Frame, so we skip this one.
|
// We're currently executing on a previous Frame, so we skip this one.
|
||||||
|
@ -1,378 +0,0 @@
|
|||||||
package com.mrousavy.camera.core
|
|
||||||
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.hardware.camera2.CameraAccessException
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CameraManager
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import android.hardware.camera2.TotalCaptureResult
|
|
||||||
import android.util.Log
|
|
||||||
import com.mrousavy.camera.core.capture.PhotoCaptureRequest
|
|
||||||
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
|
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
|
||||||
import com.mrousavy.camera.extensions.PrecaptureOptions
|
|
||||||
import com.mrousavy.camera.extensions.PrecaptureTrigger
|
|
||||||
import com.mrousavy.camera.extensions.capture
|
|
||||||
import com.mrousavy.camera.extensions.createCaptureSession
|
|
||||||
import com.mrousavy.camera.extensions.isValid
|
|
||||||
import com.mrousavy.camera.extensions.openCamera
|
|
||||||
import com.mrousavy.camera.extensions.precapture
|
|
||||||
import com.mrousavy.camera.extensions.tryAbortCaptures
|
|
||||||
import com.mrousavy.camera.extensions.tryStopRepeating
|
|
||||||
import com.mrousavy.camera.types.Flash
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
|
||||||
import com.mrousavy.camera.types.QualityPrioritization
|
|
||||||
import java.io.Closeable
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [CameraCaptureSession] wrapper that safely handles interruptions and remains open whenever available.
|
|
||||||
*
|
|
||||||
* This class aims to be similar to Apple's `AVCaptureSession`.
|
|
||||||
*/
|
|
||||||
class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable {
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "PersistentCameraCaptureSession"
|
|
||||||
private const val FOCUS_RESET_TIMEOUT = 3000L
|
|
||||||
private const val PRECAPTURE_LOCK_TIMEOUT = 5000L
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inputs/Dependencies
|
|
||||||
private var cameraId: String? = null
|
|
||||||
private var outputs: List<SurfaceOutput> = emptyList()
|
|
||||||
private var repeatingRequest: RepeatingCaptureRequest? = null
|
|
||||||
private var isActive = false
|
|
||||||
|
|
||||||
// State/Dependants
|
|
||||||
private var device: CameraDevice? = null // depends on [cameraId]
|
|
||||||
private var session: CameraCaptureSession? = null // depends on [device, surfaceOutputs]
|
|
||||||
private var cameraDeviceDetails: CameraDeviceDetails? = null // depends on [device]
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private var didDestroyFromOutside = false
|
|
||||||
private var focusJob: Job? = null
|
|
||||||
private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher)
|
|
||||||
|
|
||||||
val isRunning: Boolean
|
|
||||||
get() = isActive && session != null && device != null && !didDestroyFromOutside
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
focusJob?.cancel()
|
|
||||||
session?.tryAbortCaptures()
|
|
||||||
device?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertLocked(method: String) {
|
|
||||||
if (!mutex.isLocked) {
|
|
||||||
throw SessionIsNotLockedError("Failed to call $method, session is not locked! Call beginConfiguration() first.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun withConfiguration(block: suspend () -> Unit) {
|
|
||||||
// Cancel any ongoing focus jobs
|
|
||||||
focusJob?.cancel()
|
|
||||||
focusJob = null
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
block()
|
|
||||||
configure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setInput(cameraId: String) {
|
|
||||||
Log.d(TAG, "--> setInput($cameraId)")
|
|
||||||
assertLocked("setInput")
|
|
||||||
if (this.cameraId != cameraId || device?.id != cameraId) {
|
|
||||||
this.cameraId = cameraId
|
|
||||||
|
|
||||||
// Abort any captures in the session so we get the onCaptureFailed handler for any outstanding photos
|
|
||||||
session?.tryAbortCaptures()
|
|
||||||
session = null
|
|
||||||
// Closing the device will also close the session above - even faster than manually closing it.
|
|
||||||
device?.close()
|
|
||||||
device = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOutputs(outputs: List<SurfaceOutput>) {
|
|
||||||
Log.d(TAG, "--> setOutputs($outputs)")
|
|
||||||
assertLocked("setOutputs")
|
|
||||||
if (this.outputs != outputs) {
|
|
||||||
this.outputs = outputs
|
|
||||||
|
|
||||||
if (outputs.isNotEmpty()) {
|
|
||||||
// Outputs have changed to something else, we don't wanna destroy the session directly
|
|
||||||
// so the outputs can be kept warm. The session that gets created next will take over the outputs.
|
|
||||||
session?.tryAbortCaptures()
|
|
||||||
} else {
|
|
||||||
// Just stop it, we don't have any outputs
|
|
||||||
session?.close()
|
|
||||||
}
|
|
||||||
session = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRepeatingRequest(request: RepeatingCaptureRequest) {
|
|
||||||
assertLocked("setRepeatingRequest")
|
|
||||||
Log.d(TAG, "--> setRepeatingRequest(...)")
|
|
||||||
if (this.repeatingRequest != request) {
|
|
||||||
this.repeatingRequest = request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIsActive(isActive: Boolean) {
|
|
||||||
assertLocked("setIsActive")
|
|
||||||
Log.d(TAG, "--> setIsActive($isActive)")
|
|
||||||
if (this.isActive != isActive) {
|
|
||||||
this.isActive = isActive
|
|
||||||
}
|
|
||||||
if (isActive && didDestroyFromOutside) {
|
|
||||||
didDestroyFromOutside = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun capture(
|
|
||||||
qualityPrioritization: QualityPrioritization,
|
|
||||||
flash: Flash,
|
|
||||||
enableAutoStabilization: Boolean,
|
|
||||||
enablePhotoHdr: Boolean,
|
|
||||||
orientation: Orientation,
|
|
||||||
enableShutterSound: Boolean,
|
|
||||||
enablePrecapture: Boolean
|
|
||||||
): TotalCaptureResult {
|
|
||||||
// Cancel any ongoing focus jobs
|
|
||||||
focusJob?.cancel()
|
|
||||||
focusJob = null
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
Log.i(TAG, "Capturing photo...")
|
|
||||||
val session = session ?: throw CameraNotReadyError()
|
|
||||||
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
|
|
||||||
val photoRequest = PhotoCaptureRequest(
|
|
||||||
repeatingRequest,
|
|
||||||
qualityPrioritization,
|
|
||||||
enableAutoStabilization,
|
|
||||||
enablePhotoHdr,
|
|
||||||
orientation
|
|
||||||
)
|
|
||||||
val device = session.device
|
|
||||||
val deviceDetails = getOrCreateCameraDeviceDetails(device)
|
|
||||||
|
|
||||||
// Submit a single high-res capture to photo output as well as all preview outputs
|
|
||||||
val outputs = outputs
|
|
||||||
val repeatingOutputs = outputs.filter { it.isRepeating }
|
|
||||||
|
|
||||||
val skipPrecapture = !enablePrecapture || qualityPrioritization == QualityPrioritization.SPEED
|
|
||||||
if (skipPrecapture && flash == Flash.OFF) {
|
|
||||||
// 0. We want to take a picture as fast as possible, so skip any precapture sequence and just capture one Frame.
|
|
||||||
Log.i(TAG, "Using fast capture path without pre-capture sequence...")
|
|
||||||
val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
|
|
||||||
return session.capture(singleRequest.build(), enableShutterSound)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Locking AF/AE/AWB...")
|
|
||||||
|
|
||||||
// 1. Run precapture sequence
|
|
||||||
var needsFlash: Boolean
|
|
||||||
try {
|
|
||||||
val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs)
|
|
||||||
val skipIfPassivelyFocused = flash == Flash.OFF
|
|
||||||
val options = PrecaptureOptions(
|
|
||||||
listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB),
|
|
||||||
flash,
|
|
||||||
emptyList(),
|
|
||||||
skipIfPassivelyFocused,
|
|
||||||
PRECAPTURE_LOCK_TIMEOUT
|
|
||||||
)
|
|
||||||
val result = session.precapture(precaptureRequest, deviceDetails, options)
|
|
||||||
needsFlash = result.needsFlash
|
|
||||||
} catch (e: CaptureTimedOutError) {
|
|
||||||
// the precapture just timed out after 5 seconds, take picture anyways without focus.
|
|
||||||
needsFlash = false
|
|
||||||
} catch (e: FocusCanceledError) {
|
|
||||||
throw CaptureAbortedError(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo
|
|
||||||
val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
|
|
||||||
if (needsFlash) {
|
|
||||||
singleRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
|
||||||
singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE)
|
|
||||||
}
|
|
||||||
return session.capture(singleRequest.build(), enableShutterSound)
|
|
||||||
} finally {
|
|
||||||
// 3. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again
|
|
||||||
val idleRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs)
|
|
||||||
session.setRepeatingRequest(idleRequest.build(), null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun focus(point: Point) {
|
|
||||||
// Cancel any previous focus jobs
|
|
||||||
focusJob?.cancel()
|
|
||||||
focusJob = null
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
Log.i(TAG, "Focusing to $point...")
|
|
||||||
val session = session ?: throw CameraNotReadyError()
|
|
||||||
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
|
|
||||||
val device = session.device
|
|
||||||
val deviceDetails = getOrCreateCameraDeviceDetails(device)
|
|
||||||
if (!deviceDetails.supportsFocusRegions) {
|
|
||||||
throw FocusNotSupportedError()
|
|
||||||
}
|
|
||||||
val outputs = outputs.filter { it.isRepeating }
|
|
||||||
|
|
||||||
// 1. Run a precapture sequence for AF, AE and AWB.
|
|
||||||
focusJob = coroutineScope.launch {
|
|
||||||
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
|
|
||||||
val options =
|
|
||||||
PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT)
|
|
||||||
session.precapture(request, deviceDetails, options)
|
|
||||||
}
|
|
||||||
focusJob?.join()
|
|
||||||
|
|
||||||
// 2. Reset AF/AE/AWB again after 3 seconds timeout
|
|
||||||
focusJob = coroutineScope.launch {
|
|
||||||
delay(FOCUS_RESET_TIMEOUT)
|
|
||||||
if (!this.isActive) {
|
|
||||||
// this job got canceled from the outside
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (!isRunning || this@PersistentCameraCaptureSession.session != session) {
|
|
||||||
// the view/session has already been destroyed in the meantime
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
Log.i(TAG, "Resetting focus to auto-focus...")
|
|
||||||
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
|
|
||||||
session.setRepeatingRequest(request.build(), null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActiveDeviceDetails(): CameraDeviceDetails? {
|
|
||||||
val device = device ?: return null
|
|
||||||
return getOrCreateCameraDeviceDetails(device)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun configure() {
|
|
||||||
if (didDestroyFromOutside && !isActive) {
|
|
||||||
Log.d(TAG, "CameraCaptureSession has been destroyed by Android, skipping configuration until isActive is set to `true` again.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Configure() with isActive: $isActive, ID: $cameraId, device: $device, session: $session")
|
|
||||||
val cameraId = cameraId ?: throw NoCameraDeviceError()
|
|
||||||
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
|
|
||||||
val outputs = outputs
|
|
||||||
|
|
||||||
try {
|
|
||||||
didDestroyFromOutside = false
|
|
||||||
|
|
||||||
val device = getOrCreateDevice(cameraId)
|
|
||||||
if (didDestroyFromOutside) return
|
|
||||||
|
|
||||||
if (outputs.isEmpty()) return
|
|
||||||
val session = getOrCreateSession(device, outputs)
|
|
||||||
if (didDestroyFromOutside) return
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
Log.d(TAG, "Updating repeating request...")
|
|
||||||
val details = getOrCreateCameraDeviceDetails(device)
|
|
||||||
val repeatingOutputs = outputs.filter { it.isRepeating }
|
|
||||||
val builder = repeatingRequest.createCaptureRequest(device, details, repeatingOutputs)
|
|
||||||
session.setRepeatingRequest(builder.build(), null, null)
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Stopping repeating request...")
|
|
||||||
session.tryStopRepeating()
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Configure() done! isActive: $isActive, ID: $cameraId, device: $device, session: $session")
|
|
||||||
} catch (e: CameraAccessException) {
|
|
||||||
if (didDestroyFromOutside) {
|
|
||||||
// Camera device has been destroyed in the meantime, that's fine.
|
|
||||||
Log.d(TAG, "Configure() canceled, session has been destroyed in the meantime!")
|
|
||||||
} else {
|
|
||||||
// Camera should still be active, so not sure what went wrong. Rethrow
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getOrCreateDevice(cameraId: String): CameraDevice {
|
|
||||||
val currentDevice = device
|
|
||||||
if (currentDevice?.id == cameraId && currentDevice.isValid) {
|
|
||||||
return currentDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
this.session?.tryAbortCaptures()
|
|
||||||
this.device?.close()
|
|
||||||
this.device = null
|
|
||||||
this.session = null
|
|
||||||
|
|
||||||
Log.i(TAG, "Creating new device...")
|
|
||||||
val newDevice = cameraManager.openCamera(cameraId, { device, error ->
|
|
||||||
Log.i(TAG, "Camera $device closed!")
|
|
||||||
if (this.device == device) {
|
|
||||||
this.didDestroyFromOutside = true
|
|
||||||
this.session?.tryAbortCaptures()
|
|
||||||
this.session = null
|
|
||||||
this.device = null
|
|
||||||
this.isActive = false
|
|
||||||
}
|
|
||||||
if (error != null) {
|
|
||||||
callback.onError(error)
|
|
||||||
}
|
|
||||||
}, CameraQueues.videoQueue)
|
|
||||||
this.device = newDevice
|
|
||||||
return newDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getOrCreateSession(device: CameraDevice, outputs: List<SurfaceOutput>): CameraCaptureSession {
|
|
||||||
val currentSession = session
|
|
||||||
if (currentSession?.device == device) {
|
|
||||||
return currentSession
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputs.isEmpty()) throw NoOutputsError()
|
|
||||||
|
|
||||||
Log.i(TAG, "Creating new session...")
|
|
||||||
val newSession = device.createCaptureSession(cameraManager, outputs, { session ->
|
|
||||||
Log.i(TAG, "Session $session closed!")
|
|
||||||
if (this.session == session) {
|
|
||||||
this.didDestroyFromOutside = true
|
|
||||||
this.session?.tryAbortCaptures()
|
|
||||||
this.session = null
|
|
||||||
this.isActive = false
|
|
||||||
}
|
|
||||||
}, CameraQueues.videoQueue)
|
|
||||||
session = newSession
|
|
||||||
return newSession
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOrCreateCameraDeviceDetails(device: CameraDevice): CameraDeviceDetails {
|
|
||||||
val currentDetails = cameraDeviceDetails
|
|
||||||
if (currentDetails?.cameraId == device.id) {
|
|
||||||
return currentDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
val newDetails = CameraDeviceDetails(cameraManager, device.id)
|
|
||||||
cameraDeviceDetails = newDetails
|
|
||||||
return newDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onError(error: Throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SessionIsNotLockedError(message: String) : Error(message)
|
|
||||||
}
|
|
@ -1,117 +1,52 @@
|
|||||||
package com.mrousavy.camera.core
|
package com.mrousavy.camera.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Point
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
|
import android.widget.FrameLayout
|
||||||
import com.facebook.react.bridge.UiThreadUtil
|
import com.facebook.react.bridge.UiThreadUtil
|
||||||
import com.mrousavy.camera.extensions.resize
|
import com.mrousavy.camera.extensions.getMaximumPreviewSize
|
||||||
import com.mrousavy.camera.extensions.rotatedBy
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
|
||||||
import com.mrousavy.camera.types.ResizeMode
|
import com.mrousavy.camera.types.ResizeMode
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
|
class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) {
|
||||||
SurfaceView(context),
|
var size: Size = getMaximumPreviewSize()
|
||||||
SurfaceHolder.Callback {
|
|
||||||
var size: Size = CameraDeviceDetails.getMaximumPreviewSize()
|
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
field = value
|
||||||
Log.i(TAG, "Surface Size changed: $field -> $value")
|
UiThreadUtil.runOnUiThread {
|
||||||
field = value
|
Log.i(TAG, "Setting PreviewView Surface Size to $width x $height...")
|
||||||
updateLayout()
|
holder.setFixedSize(value.height, value.width)
|
||||||
|
requestLayout()
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var resizeMode: ResizeMode = ResizeMode.COVER
|
var resizeMode: ResizeMode = ResizeMode.COVER
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
field = value
|
||||||
Log.i(TAG, "Resize Mode changed: $field -> $value")
|
UiThreadUtil.runOnUiThread {
|
||||||
field = value
|
requestLayout()
|
||||||
updateLayout()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var orientation: Orientation = Orientation.PORTRAIT
|
|
||||||
set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
Log.i(TAG, "View Orientation changed: $field -> $value")
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT
|
|
||||||
set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
Log.i(TAG, "Input Orientation changed: $field -> $value")
|
|
||||||
field = value
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val viewSize: Size
|
|
||||||
get() {
|
|
||||||
val displayMetrics = context.resources.displayMetrics
|
|
||||||
val dpX = width / displayMetrics.density
|
|
||||||
val dpY = height / displayMetrics.density
|
|
||||||
return Size(dpX.toInt(), dpY.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.i(TAG, "Creating PreviewView...")
|
Log.i(TAG, "Creating PreviewView...")
|
||||||
holder.setKeepScreenOn(true)
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
holder.addCallback(this)
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
Gravity.CENTER
|
||||||
|
)
|
||||||
holder.addCallback(callback)
|
holder.addCallback(callback)
|
||||||
holder.setFixedSize(size.width, size.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) = Unit
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
|
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
||||||
size = Size(width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setSurfaceSize(width: Int, height: Int, cameraSensorOrientation: Orientation) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
inputOrientation = cameraSensorOrientation
|
|
||||||
holder.resize(width, height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point {
|
|
||||||
val sensorOrientation = cameraDeviceDetails.sensorOrientation
|
|
||||||
val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height())
|
|
||||||
val viewOrientation = Orientation.PORTRAIT
|
|
||||||
|
|
||||||
val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation)
|
|
||||||
Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)")
|
|
||||||
return rotated
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateLayout() {
|
|
||||||
UiThreadUtil.runOnUiThread {
|
|
||||||
requestLayout()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
|
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
|
||||||
var contentSize = contentSize
|
|
||||||
var androidOrientation = context.getResources().getConfiguration().orientation;
|
|
||||||
|
|
||||||
if (androidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
contentSize = Size(contentSize.height, contentSize.width)
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentAspectRatio = contentSize.width.toDouble() / contentSize.height
|
val contentAspectRatio = contentSize.width.toDouble() / contentSize.height
|
||||||
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
|
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
|
||||||
if (!(contentAspectRatio > 0 && containerAspectRatio > 0)) {
|
|
||||||
// One of the aspect ratios is 0 or NaN, maybe the view hasn't been laid out yet.
|
|
||||||
return contentSize
|
|
||||||
}
|
|
||||||
|
|
||||||
val widthOverHeight = when (resizeMode) {
|
val widthOverHeight = when (resizeMode) {
|
||||||
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
|
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
|
||||||
@ -133,11 +68,10 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
|
|||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
|
||||||
val measuredViewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
|
val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
|
||||||
val surfaceSize = size.rotatedBy(inputOrientation)
|
val fittedSize = getSize(size, viewSize, resizeMode)
|
||||||
val fittedSize = getSize(surfaceSize, measuredViewSize, resizeMode)
|
|
||||||
|
|
||||||
Log.i(TAG, "PreviewView is $measuredViewSize rendering $surfaceSize orientation ($orientation). Resizing to: $fittedSize ($resizeMode)")
|
Log.i(TAG, "PreviewView is $viewSize, rendering $size content. Resizing to: $fittedSize ($resizeMode)")
|
||||||
setMeasuredDimension(fittedSize.width, fittedSize.height)
|
setMeasuredDimension(fittedSize.width, fittedSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ class RecordingSession(
|
|||||||
private val hdr: Boolean = false,
|
private val hdr: Boolean = false,
|
||||||
private val cameraOrientation: Orientation,
|
private val cameraOrientation: Orientation,
|
||||||
private val options: RecordVideoOptions,
|
private val options: RecordVideoOptions,
|
||||||
private val filePath: String,
|
|
||||||
private val callback: (video: Video) -> Unit,
|
private val callback: (video: Video) -> Unit,
|
||||||
private val onError: (error: CameraError) -> Unit,
|
private val onError: (error: CameraError) -> Unit,
|
||||||
private val allCallbacks: CameraSession.Callback,
|
private val allCallbacks: CameraSession.Callback,
|
||||||
@ -38,7 +37,12 @@ class RecordingSession(
|
|||||||
|
|
||||||
data class Video(val path: String, val durationMs: Long, val size: Size)
|
data class Video(val path: String, val durationMs: Long, val size: Size)
|
||||||
|
|
||||||
private val outputPath: File = File(filePath)
|
private val outputPath = run {
|
||||||
|
val videoDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
|
||||||
|
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
|
||||||
|
val videoFileName = "VID_${sdf.format(Date())}"
|
||||||
|
File(videoDir!!, videoFileName)
|
||||||
|
}
|
||||||
|
|
||||||
private val bitRate = getBitRate()
|
private val bitRate = getBitRate()
|
||||||
private val recorder = ChunkedRecordingManager.fromParams(
|
private val recorder = ChunkedRecordingManager.fromParams(
|
||||||
|
@ -33,7 +33,6 @@ class VideoPipeline(
|
|||||||
val format: PixelFormat = PixelFormat.NATIVE,
|
val format: PixelFormat = PixelFormat.NATIVE,
|
||||||
private val isMirrored: Boolean = false,
|
private val isMirrored: Boolean = false,
|
||||||
private val enableFrameProcessor: Boolean = false,
|
private val enableFrameProcessor: Boolean = false,
|
||||||
enableGpuBuffers: Boolean = false,
|
|
||||||
private val callback: CameraSession.Callback
|
private val callback: CameraSession.Callback
|
||||||
) : SurfaceTexture.OnFrameAvailableListener,
|
) : SurfaceTexture.OnFrameAvailableListener,
|
||||||
Closeable {
|
Closeable {
|
||||||
@ -80,25 +79,17 @@ class VideoPipeline(
|
|||||||
val format = getImageReaderFormat()
|
val format = getImageReaderFormat()
|
||||||
Log.i(TAG, "Using ImageReader round-trip (format: #$format)")
|
Log.i(TAG, "Using ImageReader round-trip (format: #$format)")
|
||||||
|
|
||||||
// Create ImageReader
|
|
||||||
if (enableGpuBuffers && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val usageFlags = getRecommendedHardwareBufferFlags()
|
|
||||||
Log.i(TAG, "Creating ImageReader with GPU-optimized usage flags: $usageFlags")
|
|
||||||
imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES, usageFlags)
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Creating ImageReader with default usage flags...")
|
|
||||||
imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ImageWriter
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
Log.i(TAG, "Creating ImageWriter with format #$format...")
|
Log.i(TAG, "Using API 29 for GPU ImageReader...")
|
||||||
|
val usageFlags = getRecommendedHardwareBufferFlags()
|
||||||
|
Log.i(TAG, "Using ImageReader flags: $usageFlags")
|
||||||
|
imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES, usageFlags)
|
||||||
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES, format)
|
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES, format)
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Creating ImageWriter with default format...")
|
Log.i(TAG, "Using legacy API for CPU ImageReader...")
|
||||||
|
imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES)
|
||||||
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES)
|
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageReader!!.setOnImageAvailableListener({ reader ->
|
imageReader!!.setOnImageAvailableListener({ reader ->
|
||||||
// Log.i(TAG, "ImageReader::onImageAvailable!")s
|
// Log.i(TAG, "ImageReader::onImageAvailable!")s
|
||||||
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
||||||
@ -116,7 +107,7 @@ class VideoPipeline(
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "FrameProcessor/ImageReader pipeline threw an error!", e)
|
Log.e(TAG, "FrameProcessor/ImageReader pipeline threw an error!", e)
|
||||||
callback.onError(e)
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
frame.decrementRefCount()
|
frame.decrementRefCount()
|
||||||
}
|
}
|
||||||
@ -134,11 +125,8 @@ class VideoPipeline(
|
|||||||
isActive = false
|
isActive = false
|
||||||
imageWriter?.close()
|
imageWriter?.close()
|
||||||
imageReader?.close()
|
imageReader?.close()
|
||||||
removeRecordingSessionOutputSurface()
|
|
||||||
recordingSession = null
|
recordingSession = null
|
||||||
surfaceTexture.setOnFrameAvailableListener(null, null)
|
|
||||||
surfaceTexture.release()
|
surfaceTexture.release()
|
||||||
surface.release()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +173,7 @@ class VideoPipeline(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (recordingSession != null) {
|
if (recordingSession != null) {
|
||||||
// Configure OpenGL pipeline to stream Frames into the Recording Session's surface
|
// Configure OpenGL pipeline to stream Frames into the Recording Session's surface
|
||||||
Log.i(TAG, "Setting ${recordingSession.size} RecordingSession Output...")
|
Log.i(TAG, "Setting $width x $height RecordingSession Output...")
|
||||||
setRecordingSessionOutputSurface(recordingSession.surface)
|
setRecordingSessionOutputSurface(recordingSession.surface)
|
||||||
this.recordingSession = recordingSession
|
this.recordingSession = recordingSession
|
||||||
} else {
|
} else {
|
||||||
@ -240,11 +228,7 @@ class VideoPipeline(
|
|||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun supportsHardwareBufferFlags(flags: Long): Boolean {
|
private fun supportsHardwareBufferFlags(flags: Long): Boolean {
|
||||||
val hardwareBufferFormat = format.toHardwareBufferFormat()
|
val hardwareBufferFormat = format.toHardwareBufferFormat()
|
||||||
try {
|
return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags)
|
||||||
return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private external fun getInputTextureId(): Int
|
private external fun getInputTextureId(): Int
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
package com.mrousavy.camera.core.capture
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
|
||||||
import com.mrousavy.camera.core.FlashUnavailableError
|
|
||||||
import com.mrousavy.camera.core.InvalidVideoHdrError
|
|
||||||
import com.mrousavy.camera.core.LowLightBoostNotSupportedError
|
|
||||||
import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError
|
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
|
||||||
import com.mrousavy.camera.extensions.setZoom
|
|
||||||
import com.mrousavy.camera.types.CameraDeviceFormat
|
|
||||||
import com.mrousavy.camera.types.Torch
|
|
||||||
|
|
||||||
abstract class CameraCaptureRequest(
|
|
||||||
private val torch: Torch = Torch.OFF,
|
|
||||||
private val enableVideoHdr: Boolean = false,
|
|
||||||
val enableLowLightBoost: Boolean = false,
|
|
||||||
val exposureBias: Double? = null,
|
|
||||||
val zoom: Float = 1.0f,
|
|
||||||
val format: CameraDeviceFormat? = null
|
|
||||||
) {
|
|
||||||
enum class Template {
|
|
||||||
RECORD,
|
|
||||||
PHOTO,
|
|
||||||
PHOTO_ZSL,
|
|
||||||
PHOTO_SNAPSHOT,
|
|
||||||
PREVIEW;
|
|
||||||
|
|
||||||
fun toRequestTemplate(): Int =
|
|
||||||
when (this) {
|
|
||||||
RECORD -> CameraDevice.TEMPLATE_RECORD
|
|
||||||
PHOTO -> CameraDevice.TEMPLATE_STILL_CAPTURE
|
|
||||||
PHOTO_ZSL -> CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
|
|
||||||
PHOTO_SNAPSHOT -> CameraDevice.TEMPLATE_VIDEO_SNAPSHOT
|
|
||||||
PREVIEW -> CameraDevice.TEMPLATE_PREVIEW
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun createCaptureRequest(
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder
|
|
||||||
|
|
||||||
protected open fun createCaptureRequest(
|
|
||||||
template: Template,
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder {
|
|
||||||
val builder = device.createCaptureRequest(template.toRequestTemplate())
|
|
||||||
|
|
||||||
// Add all repeating output surfaces
|
|
||||||
outputs.forEach { output ->
|
|
||||||
builder.addTarget(output.surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set HDR
|
|
||||||
if (enableVideoHdr) {
|
|
||||||
if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr")
|
|
||||||
if (!format.supportsVideoHdr) throw InvalidVideoHdrError()
|
|
||||||
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
|
||||||
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE)
|
|
||||||
} else if (enableLowLightBoost) {
|
|
||||||
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
|
|
||||||
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
|
|
||||||
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Exposure Bias
|
|
||||||
if (exposureBias != null) {
|
|
||||||
val clamped = deviceDetails.exposureRange.clamp(exposureBias.toInt())
|
|
||||||
builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Zoom
|
|
||||||
builder.setZoom(zoom, deviceDetails)
|
|
||||||
|
|
||||||
// Set Torch
|
|
||||||
if (torch == Torch.ON) {
|
|
||||||
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
|
|
||||||
builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
package com.mrousavy.camera.core.capture
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
|
||||||
import com.mrousavy.camera.types.HardwareLevel
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
|
||||||
import com.mrousavy.camera.types.QualityPrioritization
|
|
||||||
import com.mrousavy.camera.types.Torch
|
|
||||||
|
|
||||||
class PhotoCaptureRequest(
|
|
||||||
repeatingRequest: RepeatingCaptureRequest,
|
|
||||||
private val qualityPrioritization: QualityPrioritization,
|
|
||||||
private val enableAutoStabilization: Boolean,
|
|
||||||
enablePhotoHdr: Boolean,
|
|
||||||
private val outputOrientation: Orientation
|
|
||||||
) : CameraCaptureRequest(
|
|
||||||
Torch.OFF,
|
|
||||||
enablePhotoHdr,
|
|
||||||
repeatingRequest.enableLowLightBoost,
|
|
||||||
repeatingRequest.exposureBias,
|
|
||||||
repeatingRequest.zoom,
|
|
||||||
repeatingRequest.format
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "PhotoCaptureRequest"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCaptureRequest(
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder {
|
|
||||||
val template = when (qualityPrioritization) {
|
|
||||||
QualityPrioritization.QUALITY -> Template.PHOTO
|
|
||||||
QualityPrioritization.BALANCED -> {
|
|
||||||
if (deviceDetails.supportsZsl) {
|
|
||||||
Template.PHOTO_ZSL
|
|
||||||
} else {
|
|
||||||
Template.PHOTO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QualityPrioritization.SPEED -> {
|
|
||||||
if (deviceDetails.supportsSnapshotCapture) {
|
|
||||||
Template.PHOTO_SNAPSHOT
|
|
||||||
} else if (deviceDetails.supportsZsl) {
|
|
||||||
Template.PHOTO_ZSL
|
|
||||||
} else {
|
|
||||||
Template.PHOTO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(TAG, "Using CaptureRequest Template $template...")
|
|
||||||
return this.createCaptureRequest(template, device, deviceDetails, outputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCaptureRequest(
|
|
||||||
template: Template,
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder {
|
|
||||||
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
|
|
||||||
|
|
||||||
// Set various speed vs quality optimization flags
|
|
||||||
when (qualityPrioritization) {
|
|
||||||
QualityPrioritization.SPEED -> {
|
|
||||||
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) {
|
|
||||||
builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_FAST)
|
|
||||||
if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_FAST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_FAST)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) &&
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
|
||||||
) {
|
|
||||||
builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_FAST)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_FAST)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_FAST)) {
|
|
||||||
builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_FAST)
|
|
||||||
}
|
|
||||||
builder.set(CaptureRequest.JPEG_QUALITY, 85)
|
|
||||||
}
|
|
||||||
QualityPrioritization.BALANCED -> {
|
|
||||||
builder.set(CaptureRequest.JPEG_QUALITY, 92)
|
|
||||||
}
|
|
||||||
QualityPrioritization.QUALITY -> {
|
|
||||||
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) {
|
|
||||||
builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_HIGH_QUALITY)
|
|
||||||
if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) &&
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
|
||||||
) {
|
|
||||||
builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)) {
|
|
||||||
builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)
|
|
||||||
}
|
|
||||||
builder.set(CaptureRequest.JPEG_QUALITY, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set JPEG Orientation
|
|
||||||
val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails)
|
|
||||||
builder.set(CaptureRequest.JPEG_ORIENTATION, targetOrientation.toDegrees())
|
|
||||||
|
|
||||||
// Set stabilization for this Frame
|
|
||||||
if (enableAutoStabilization) {
|
|
||||||
if (deviceDetails.opticalStabilizationModes.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)) {
|
|
||||||
builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)
|
|
||||||
} else if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package com.mrousavy.camera.core.capture
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Range
|
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
|
||||||
import com.mrousavy.camera.core.InvalidFpsError
|
|
||||||
import com.mrousavy.camera.core.InvalidVideoStabilizationMode
|
|
||||||
import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError
|
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
|
||||||
import com.mrousavy.camera.types.CameraDeviceFormat
|
|
||||||
import com.mrousavy.camera.types.HardwareLevel
|
|
||||||
import com.mrousavy.camera.types.Torch
|
|
||||||
import com.mrousavy.camera.types.VideoStabilizationMode
|
|
||||||
|
|
||||||
class RepeatingCaptureRequest(
|
|
||||||
private val enableVideoPipeline: Boolean,
|
|
||||||
torch: Torch = Torch.OFF,
|
|
||||||
private val fps: Int? = null,
|
|
||||||
private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
|
|
||||||
enableVideoHdr: Boolean = false,
|
|
||||||
enableLowLightBoost: Boolean = false,
|
|
||||||
exposureBias: Double? = null,
|
|
||||||
zoom: Float = 1.0f,
|
|
||||||
format: CameraDeviceFormat? = null
|
|
||||||
) : CameraCaptureRequest(torch, enableVideoHdr, enableLowLightBoost, exposureBias, zoom, format) {
|
|
||||||
override fun createCaptureRequest(
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder {
|
|
||||||
val template = if (enableVideoPipeline) Template.RECORD else Template.PREVIEW
|
|
||||||
return this.createCaptureRequest(template, device, deviceDetails, outputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBestDigitalStabilizationMode(deviceDetails: CameraDeviceDetails): Int {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) {
|
|
||||||
return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCaptureRequest(
|
|
||||||
template: Template,
|
|
||||||
device: CameraDevice,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
outputs: List<SurfaceOutput>
|
|
||||||
): CaptureRequest.Builder {
|
|
||||||
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
|
|
||||||
|
|
||||||
if (deviceDetails.modes.contains(CameraCharacteristics.CONTROL_MODE_AUTO)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set AF
|
|
||||||
if (enableVideoPipeline && deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
|
|
||||||
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
|
||||||
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_AUTO)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
|
|
||||||
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
|
|
||||||
builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set AE
|
|
||||||
if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_ON)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
|
||||||
} else if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_OFF)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set AWB
|
|
||||||
if (deviceDetails.awbModes.contains(CameraCharacteristics.CONTROL_AWB_MODE_AUTO)) {
|
|
||||||
builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set FPS
|
|
||||||
if (fps != null) {
|
|
||||||
if (format == null) throw PropRequiresFormatToBeNonNullError("fps")
|
|
||||||
if (format.maxFps < fps) throw InvalidFpsError(fps)
|
|
||||||
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Video Stabilization
|
|
||||||
if (videoStabilizationMode != VideoStabilizationMode.OFF) {
|
|
||||||
if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode")
|
|
||||||
if (!format.videoStabilizationModes.contains(videoStabilizationMode)) {
|
|
||||||
throw InvalidVideoStabilizationMode(videoStabilizationMode)
|
|
||||||
}
|
|
||||||
when (videoStabilizationMode) {
|
|
||||||
VideoStabilizationMode.STANDARD -> {
|
|
||||||
builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails))
|
|
||||||
}
|
|
||||||
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
|
|
||||||
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) {
|
|
||||||
builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
|
|
||||||
} else {
|
|
||||||
builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw InvalidVideoStabilizationMode(videoStabilizationMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,7 +10,13 @@ import android.view.Surface
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: OutputType, val enableHdr: Boolean = false) : Closeable {
|
open class SurfaceOutput(
|
||||||
|
val surface: Surface,
|
||||||
|
val size: Size,
|
||||||
|
val outputType: OutputType,
|
||||||
|
val enableHdr: Boolean = false,
|
||||||
|
private val closeSurfaceOnEnd: Boolean = false
|
||||||
|
) : Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "SurfaceOutput"
|
const val TAG = "SurfaceOutput"
|
||||||
|
|
||||||
@ -46,18 +52,12 @@ open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: O
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRepeating: Boolean
|
|
||||||
get() {
|
|
||||||
return when (outputType) {
|
|
||||||
OutputType.VIDEO, OutputType.PREVIEW, OutputType.VIDEO_AND_PREVIEW -> true
|
|
||||||
OutputType.PHOTO -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = "$outputType (${size.width} x ${size.height})"
|
override fun toString(): String = "$outputType (${size.width} x ${size.height})"
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
// close() does nothing by default
|
if (closeSurfaceOnEnd) {
|
||||||
|
surface.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class OutputType {
|
enum class OutputType {
|
||||||
|
@ -5,70 +5,47 @@ import android.hardware.camera2.CaptureFailure
|
|||||||
import android.hardware.camera2.CaptureRequest
|
import android.hardware.camera2.CaptureRequest
|
||||||
import android.hardware.camera2.TotalCaptureResult
|
import android.hardware.camera2.TotalCaptureResult
|
||||||
import android.media.MediaActionSound
|
import android.media.MediaActionSound
|
||||||
import android.util.Log
|
import com.mrousavy.camera.core.CameraQueues
|
||||||
import com.mrousavy.camera.core.CaptureAbortedError
|
import com.mrousavy.camera.core.CaptureAbortedError
|
||||||
import com.mrousavy.camera.core.CaptureTimedOutError
|
|
||||||
import com.mrousavy.camera.core.UnknownCaptureError
|
import com.mrousavy.camera.core.UnknownCaptureError
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
|
|
||||||
private const val TAG = "CameraCaptureSession"
|
|
||||||
|
|
||||||
suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult =
|
suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
val shutterSound = if (enableShutterSound) MediaActionSound() else null
|
val shutterSound = if (enableShutterSound) MediaActionSound() else null
|
||||||
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)
|
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
delay(5000) // after 5s, cancel capture
|
|
||||||
if (continuation.isActive) {
|
|
||||||
Log.e(TAG, "Capture timed out after 5 seconds!")
|
|
||||||
continuation.resumeWithException(CaptureTimedOutError())
|
|
||||||
tryAbortCaptures()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.capture(
|
this.capture(
|
||||||
captureRequest,
|
captureRequest,
|
||||||
object : CameraCaptureSession.CaptureCallback() {
|
object : CameraCaptureSession.CaptureCallback() {
|
||||||
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
|
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
|
||||||
super.onCaptureCompleted(session, request, result)
|
super.onCaptureCompleted(session, request, result)
|
||||||
|
|
||||||
if (request == captureRequest) {
|
continuation.resume(result)
|
||||||
continuation.resume(result)
|
shutterSound?.release()
|
||||||
shutterSound?.release()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
|
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
|
||||||
super.onCaptureStarted(session, request, timestamp, frameNumber)
|
super.onCaptureStarted(session, request, timestamp, frameNumber)
|
||||||
|
|
||||||
if (request == captureRequest) {
|
if (enableShutterSound) {
|
||||||
if (enableShutterSound) {
|
shutterSound?.play(MediaActionSound.SHUTTER_CLICK)
|
||||||
shutterSound?.play(MediaActionSound.SHUTTER_CLICK)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
|
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
|
||||||
super.onCaptureFailed(session, request, failure)
|
super.onCaptureFailed(session, request, failure)
|
||||||
|
val wasImageCaptured = failure.wasImageCaptured()
|
||||||
if (request == captureRequest) {
|
val error = when (failure.reason) {
|
||||||
val wasImageCaptured = failure.wasImageCaptured()
|
CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured)
|
||||||
val error = when (failure.reason) {
|
CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured)
|
||||||
CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured)
|
else -> UnknownCaptureError(wasImageCaptured)
|
||||||
CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured)
|
|
||||||
else -> UnknownCaptureError(wasImageCaptured)
|
|
||||||
}
|
|
||||||
continuation.resumeWithException(error)
|
|
||||||
}
|
}
|
||||||
|
continuation.resumeWithException(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null
|
CameraQueues.cameraQueue.handler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,151 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import android.hardware.camera2.CaptureResult
|
|
||||||
import android.hardware.camera2.params.MeteringRectangle
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Size
|
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
|
||||||
import com.mrousavy.camera.core.FocusCanceledError
|
|
||||||
import com.mrousavy.camera.types.Flash
|
|
||||||
import com.mrousavy.camera.types.HardwareLevel
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
|
|
||||||
data class PrecaptureOptions(
|
|
||||||
val modes: List<PrecaptureTrigger>,
|
|
||||||
val flash: Flash = Flash.OFF,
|
|
||||||
val pointsOfInterest: List<Point>,
|
|
||||||
val skipIfPassivelyFocused: Boolean,
|
|
||||||
val timeoutMs: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PrecaptureResult(val needsFlash: Boolean)
|
|
||||||
|
|
||||||
private const val TAG = "Precapture"
|
|
||||||
private val DEFAULT_METERING_SIZE = Size(100, 100)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a precapture sequence to trigger an AF, AE or AWB scan and lock to the optimal values.
|
|
||||||
* After this function completes, you can capture high quality photos as AF/AE/AWB are in focused state.
|
|
||||||
*
|
|
||||||
* To reset to auto-focus again, create a new `RepeatingRequest` with a fresh set of CONTROL_MODEs set.
|
|
||||||
*/
|
|
||||||
suspend fun CameraCaptureSession.precapture(
|
|
||||||
request: CaptureRequest.Builder,
|
|
||||||
deviceDetails: CameraDeviceDetails,
|
|
||||||
options: PrecaptureOptions
|
|
||||||
): PrecaptureResult {
|
|
||||||
Log.i(TAG, "Running precapture sequence... ($options)")
|
|
||||||
request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
|
||||||
|
|
||||||
var enableFlash = options.flash == Flash.ON
|
|
||||||
var afState = FocusState.Inactive
|
|
||||||
var aeState = ExposureState.Inactive
|
|
||||||
var awbState = WhiteBalanceState.Inactive
|
|
||||||
val precaptureModes = options.modes.toMutableList()
|
|
||||||
|
|
||||||
// 1. Cancel any ongoing precapture sequences
|
|
||||||
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
|
|
||||||
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
|
|
||||||
if (options.flash == Flash.AUTO || options.skipIfPassivelyFocused) {
|
|
||||||
// We want to read the current AE/AF/AWB values to determine if we need flash or can skip AF/AE/AWB precapture
|
|
||||||
val result = this.capture(request.build(), false)
|
|
||||||
|
|
||||||
afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE)
|
|
||||||
aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE)
|
|
||||||
awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE)
|
|
||||||
|
|
||||||
Log.i(TAG, "Precapture current states: AF: $afState, AE: $aeState, AWB: $awbState")
|
|
||||||
enableFlash = aeState == ExposureState.FlashRequired && options.flash == Flash.AUTO
|
|
||||||
} else {
|
|
||||||
// we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture.
|
|
||||||
this.capture(request.build(), null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!coroutineContext.isActive) throw FocusCanceledError()
|
|
||||||
|
|
||||||
val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1
|
|
||||||
val meteringRectangles = options.pointsOfInterest.map { point ->
|
|
||||||
MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight)
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
if (options.skipIfPassivelyFocused) {
|
|
||||||
// If user allows us to skip precapture for values that are already focused, remove them from the precapture modes.
|
|
||||||
if (afState.isPassivelyFocused) {
|
|
||||||
Log.i(TAG, "AF is already focused, skipping...")
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AF)
|
|
||||||
}
|
|
||||||
if (aeState.isPassivelyFocused) {
|
|
||||||
Log.i(TAG, "AE is already focused, skipping...")
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AE)
|
|
||||||
}
|
|
||||||
if (awbState.isPassivelyFocused) {
|
|
||||||
Log.i(TAG, "AWB is already focused, skipping...")
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AWB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Submit a precapture start sequence
|
|
||||||
if (enableFlash && deviceDetails.hasFlash) {
|
|
||||||
request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
|
||||||
}
|
|
||||||
if (precaptureModes.contains(PrecaptureTrigger.AF)) {
|
|
||||||
// AF Precapture
|
|
||||||
if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
|
|
||||||
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
|
|
||||||
if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// AF is not supported on this device.
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (precaptureModes.contains(PrecaptureTrigger.AE)) {
|
|
||||||
// AE Precapture
|
|
||||||
if (deviceDetails.aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
|
||||||
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
|
|
||||||
if (meteringRectangles.isNotEmpty() &&
|
|
||||||
deviceDetails.supportsExposureRegions &&
|
|
||||||
deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)
|
|
||||||
) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// AE is not supported on this device.
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (precaptureModes.contains(PrecaptureTrigger.AWB)) {
|
|
||||||
// AWB Precapture
|
|
||||||
if (deviceDetails.awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO)) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO)
|
|
||||||
if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) {
|
|
||||||
request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// AWB is not supported on this device.
|
|
||||||
precaptureModes.remove(PrecaptureTrigger.AWB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.capture(request.build(), null, null)
|
|
||||||
|
|
||||||
if (!coroutineContext.isActive) throw FocusCanceledError()
|
|
||||||
|
|
||||||
// 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks
|
|
||||||
request.set(CaptureRequest.CONTROL_AF_TRIGGER, null)
|
|
||||||
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null)
|
|
||||||
val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), options.timeoutMs, *precaptureModes.toTypedArray())
|
|
||||||
|
|
||||||
if (!coroutineContext.isActive) throw FocusCanceledError()
|
|
||||||
|
|
||||||
Log.i(TAG, "AF/AE/AWB successfully locked!")
|
|
||||||
|
|
||||||
val needsFlash = result.exposureState == ExposureState.FlashRequired
|
|
||||||
return PrecaptureResult(needsFlash)
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
import android.hardware.camera2.CaptureFailure
|
|
||||||
import android.hardware.camera2.CaptureRequest
|
|
||||||
import android.hardware.camera2.CaptureResult
|
|
||||||
import android.hardware.camera2.TotalCaptureResult
|
|
||||||
import android.util.Log
|
|
||||||
import com.mrousavy.camera.core.CaptureAbortedError
|
|
||||||
import com.mrousavy.camera.core.CaptureTimedOutError
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
|
|
||||||
private const val TAG = "CameraCaptureSession"
|
|
||||||
|
|
||||||
enum class PrecaptureTrigger {
|
|
||||||
AE,
|
|
||||||
AF,
|
|
||||||
AWB
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AutoState {
|
|
||||||
val isCompleted: Boolean
|
|
||||||
val isPassivelyFocused: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class FocusState : AutoState {
|
|
||||||
Unknown,
|
|
||||||
Inactive,
|
|
||||||
Scanning,
|
|
||||||
Focused,
|
|
||||||
Unfocused,
|
|
||||||
PassiveScanning,
|
|
||||||
PassiveFocused,
|
|
||||||
PassiveUnfocused;
|
|
||||||
|
|
||||||
override val isCompleted: Boolean
|
|
||||||
get() = this == Focused || this == Unfocused
|
|
||||||
override val isPassivelyFocused: Boolean
|
|
||||||
get() = this == PassiveFocused
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromAFState(afState: Int): FocusState =
|
|
||||||
when (afState) {
|
|
||||||
CaptureResult.CONTROL_AF_STATE_INACTIVE -> Inactive
|
|
||||||
CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> Scanning
|
|
||||||
CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> Focused
|
|
||||||
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> Unfocused
|
|
||||||
CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> PassiveScanning
|
|
||||||
CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> PassiveFocused
|
|
||||||
CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> PassiveUnfocused
|
|
||||||
else -> Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum class ExposureState : AutoState {
|
|
||||||
Unknown,
|
|
||||||
Locked,
|
|
||||||
Inactive,
|
|
||||||
Precapture,
|
|
||||||
Searching,
|
|
||||||
Converged,
|
|
||||||
FlashRequired;
|
|
||||||
|
|
||||||
override val isCompleted: Boolean
|
|
||||||
get() = this == Converged || this == FlashRequired
|
|
||||||
override val isPassivelyFocused: Boolean
|
|
||||||
get() = this == Converged
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromAEState(aeState: Int): ExposureState =
|
|
||||||
when (aeState) {
|
|
||||||
CaptureResult.CONTROL_AE_STATE_INACTIVE -> Inactive
|
|
||||||
CaptureResult.CONTROL_AE_STATE_SEARCHING -> Searching
|
|
||||||
CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> Precapture
|
|
||||||
CaptureResult.CONTROL_AE_STATE_CONVERGED -> Converged
|
|
||||||
CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> FlashRequired
|
|
||||||
CaptureResult.CONTROL_AE_STATE_LOCKED -> Locked
|
|
||||||
else -> Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WhiteBalanceState : AutoState {
|
|
||||||
Unknown,
|
|
||||||
Inactive,
|
|
||||||
Locked,
|
|
||||||
Searching,
|
|
||||||
Converged;
|
|
||||||
|
|
||||||
override val isCompleted: Boolean
|
|
||||||
get() = this == Converged
|
|
||||||
override val isPassivelyFocused: Boolean
|
|
||||||
get() = this == Converged
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromAWBState(awbState: Int): WhiteBalanceState =
|
|
||||||
when (awbState) {
|
|
||||||
CaptureResult.CONTROL_AWB_STATE_INACTIVE -> Inactive
|
|
||||||
CaptureResult.CONTROL_AWB_STATE_SEARCHING -> Searching
|
|
||||||
CaptureResult.CONTROL_AWB_STATE_CONVERGED -> Converged
|
|
||||||
CaptureResult.CONTROL_AWB_STATE_LOCKED -> Locked
|
|
||||||
else -> Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ResultState(val focusState: FocusState, val exposureState: ExposureState, val whiteBalanceState: WhiteBalanceState)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a new repeating request for the [CameraCaptureSession] that contains a precapture trigger, and wait until the given precaptures have locked.
|
|
||||||
*/
|
|
||||||
suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture(
|
|
||||||
request: CaptureRequest,
|
|
||||||
timeoutMs: Long,
|
|
||||||
vararg precaptureTriggers: PrecaptureTrigger
|
|
||||||
): ResultState =
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
// Map<PrecaptureTrigger, Boolean> of all completed precaptures
|
|
||||||
val completed = precaptureTriggers.associateWith { false }.toMutableMap()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
delay(timeoutMs) // after timeout, cancel capture
|
|
||||||
if (continuation.isActive) {
|
|
||||||
Log.e(TAG, "Precapture timed out after ${timeoutMs / 1000} seconds!")
|
|
||||||
continuation.resumeWithException(CaptureTimedOutError())
|
|
||||||
try {
|
|
||||||
setRepeatingRequest(request, null, null)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// session might have already been closed
|
|
||||||
Log.e(TAG, "Error resetting session repeating request..", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setRepeatingRequest(
|
|
||||||
request,
|
|
||||||
object : CameraCaptureSession.CaptureCallback() {
|
|
||||||
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
|
|
||||||
super.onCaptureCompleted(session, request, result)
|
|
||||||
|
|
||||||
if (continuation.isActive) {
|
|
||||||
val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE)
|
|
||||||
val aeState = ExposureState.fromAEState(
|
|
||||||
result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE
|
|
||||||
)
|
|
||||||
val awbState = WhiteBalanceState.fromAWBState(
|
|
||||||
result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE
|
|
||||||
)
|
|
||||||
Log.i(TAG, "Precapture state: AF: $afState, AE: $aeState, AWB: $awbState")
|
|
||||||
|
|
||||||
// AF Precapture
|
|
||||||
if (precaptureTriggers.contains(PrecaptureTrigger.AF)) {
|
|
||||||
completed[PrecaptureTrigger.AF] = afState.isCompleted
|
|
||||||
}
|
|
||||||
// AE Precapture
|
|
||||||
if (precaptureTriggers.contains(PrecaptureTrigger.AE)) {
|
|
||||||
completed[PrecaptureTrigger.AE] = aeState.isCompleted
|
|
||||||
}
|
|
||||||
// AWB Precapture
|
|
||||||
if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) {
|
|
||||||
completed[PrecaptureTrigger.AWB] = awbState.isCompleted
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completed.values.all { it == true }) {
|
|
||||||
// All precaptures did complete!
|
|
||||||
continuation.resume(ResultState(afState, aeState, awbState))
|
|
||||||
session.setRepeatingRequest(request, null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
|
|
||||||
super.onCaptureFailed(session, request, failure)
|
|
||||||
|
|
||||||
if (continuation.isActive) {
|
|
||||||
// Capture failed or session closed.
|
|
||||||
continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured()))
|
|
||||||
try {
|
|
||||||
session.setRepeatingRequest(request, null, null)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e(TAG, "Failed to continue repeating request!", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
|
|
||||||
fun CameraCaptureSession.tryAbortCaptures() {
|
|
||||||
try {
|
|
||||||
abortCaptures()
|
|
||||||
} catch (_: Throwable) {}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
|
|
||||||
fun CameraCaptureSession.tryStopRepeating() {
|
|
||||||
try {
|
|
||||||
stopRepeating()
|
|
||||||
} catch (_: Throwable) {}
|
|
||||||
}
|
|
@ -1,13 +1,39 @@
|
|||||||
package com.mrousavy.camera.extensions
|
package com.mrousavy.camera.extensions
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.media.CamcorderProfile
|
||||||
|
import android.os.Build
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
|
||||||
|
private fun getMaximumVideoSize(cameraId: String): Size? {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH)
|
||||||
|
if (profiles != null) {
|
||||||
|
val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height }
|
||||||
|
if (largestProfile != null) {
|
||||||
|
return Size(largestProfile.width, largestProfile.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraIdInt = cameraId.toIntOrNull()
|
||||||
|
if (cameraIdInt != null) {
|
||||||
|
val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH)
|
||||||
|
return Size(profile.videoFrameWidth, profile.videoFrameHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> {
|
fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> {
|
||||||
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
||||||
val sizes = config.getOutputSizes(format) ?: emptyArray()
|
val sizes = config.getOutputSizes(format) ?: emptyArray()
|
||||||
val maxVideoSize = CamcorderProfileUtils.getMaximumVideoSize(cameraId)
|
val maxVideoSize = getMaximumVideoSize(cameraId)
|
||||||
if (maxVideoSize != null) {
|
if (maxVideoSize != null) {
|
||||||
return sizes.filter { it.bigger <= maxVideoSize.bigger }
|
return sizes.filter { it.bigger <= maxVideoSize.bigger }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.mrousavy.camera.extensions
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.util.Size
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
|
||||||
|
fun getMaximumPreviewSize(): Size {
|
||||||
|
// See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
|
||||||
|
// According to the Android Developer documentation, PREVIEW streams can have a resolution
|
||||||
|
// of up to the phone's display's resolution, with a maximum of 1920x1080.
|
||||||
|
val display1080p = Size(1080, 1920)
|
||||||
|
val displaySize = Size(
|
||||||
|
Resources.getSystem().displayMetrics.widthPixels,
|
||||||
|
Resources.getSystem().displayMetrics.heightPixels
|
||||||
|
)
|
||||||
|
val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller
|
||||||
|
|
||||||
|
return if (isHighResScreen) display1080p else displaySize
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CameraCharacteristics.getPreviewTargetSize(targetSize: Size?): Size {
|
||||||
|
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
||||||
|
val maximumPreviewSize = getMaximumPreviewSize()
|
||||||
|
val outputSizes = config.getOutputSizes(SurfaceHolder::class.java)
|
||||||
|
.filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
|
||||||
|
|
||||||
|
return outputSizes.closestToOrMax(targetSize)
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.mrousavy.camera.extensions
|
||||||
|
|
||||||
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.hardware.camera2.CameraDevice
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.view.Surface
|
||||||
|
import com.mrousavy.camera.types.Flash
|
||||||
|
import com.mrousavy.camera.types.Orientation
|
||||||
|
import com.mrousavy.camera.types.QualityPrioritization
|
||||||
|
|
||||||
|
private fun supportsSnapshotCapture(cameraCharacteristics: CameraCharacteristics): Boolean {
|
||||||
|
// As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation:
|
||||||
|
val hardwareLevel = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!!
|
||||||
|
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) return false
|
||||||
|
|
||||||
|
val capabilities = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
|
||||||
|
val hasDepth = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT)
|
||||||
|
val isBackwardsCompatible = !capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE)
|
||||||
|
if (hasDepth && !isBackwardsCompatible) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CameraDevice.createPhotoCaptureRequest(
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
surface: Surface,
|
||||||
|
zoom: Float,
|
||||||
|
qualityPrioritization: QualityPrioritization,
|
||||||
|
flashMode: Flash,
|
||||||
|
enableRedEyeReduction: Boolean,
|
||||||
|
enableAutoStabilization: Boolean,
|
||||||
|
enableHdr: Boolean,
|
||||||
|
orientation: Orientation
|
||||||
|
): CaptureRequest {
|
||||||
|
val cameraCharacteristics = cameraManager.getCameraCharacteristics(this.id)
|
||||||
|
|
||||||
|
val template = if (qualityPrioritization == QualityPrioritization.SPEED && supportsSnapshotCapture(cameraCharacteristics)) {
|
||||||
|
CameraDevice.TEMPLATE_VIDEO_SNAPSHOT
|
||||||
|
} else {
|
||||||
|
CameraDevice.TEMPLATE_STILL_CAPTURE
|
||||||
|
}
|
||||||
|
val captureRequest = this.createCaptureRequest(template)
|
||||||
|
captureRequest.addTarget(surface)
|
||||||
|
|
||||||
|
// TODO: Maybe we can even expose that prop directly?
|
||||||
|
val jpegQuality = when (qualityPrioritization) {
|
||||||
|
QualityPrioritization.SPEED -> 85
|
||||||
|
QualityPrioritization.BALANCED -> 92
|
||||||
|
QualityPrioritization.QUALITY -> 100
|
||||||
|
}
|
||||||
|
captureRequest.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte())
|
||||||
|
|
||||||
|
captureRequest.set(CaptureRequest.JPEG_ORIENTATION, orientation.toDegrees())
|
||||||
|
|
||||||
|
// TODO: Use the same options as from the preview request. This is duplicate code!
|
||||||
|
|
||||||
|
when (flashMode) {
|
||||||
|
// Set the Flash Mode
|
||||||
|
Flash.OFF -> {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
||||||
|
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
|
||||||
|
}
|
||||||
|
Flash.ON -> {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
||||||
|
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
||||||
|
}
|
||||||
|
Flash.AUTO -> {
|
||||||
|
if (enableRedEyeReduction) {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE)
|
||||||
|
} else {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableAutoStabilization) {
|
||||||
|
// Enable optical or digital image stabilization
|
||||||
|
val digitalStabilization = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES)
|
||||||
|
val hasDigitalStabilization = digitalStabilization?.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON) ?: false
|
||||||
|
|
||||||
|
val opticalStabilization = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION)
|
||||||
|
val hasOpticalStabilization = opticalStabilization?.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) ?: false
|
||||||
|
if (hasOpticalStabilization) {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
|
||||||
|
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
|
||||||
|
} else if (hasDigitalStabilization) {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON)
|
||||||
|
} else {
|
||||||
|
// no stabilization is supported. ignore it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if that zoom value is even supported.
|
||||||
|
captureRequest.setZoom(zoom, cameraCharacteristics)
|
||||||
|
|
||||||
|
// Set HDR
|
||||||
|
// TODO: Check if that value is even supported
|
||||||
|
if (enableHdr) {
|
||||||
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
||||||
|
}
|
||||||
|
|
||||||
|
return captureRequest.build()
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
|
|
||||||
val CameraDevice.isValid: Boolean
|
|
||||||
get() {
|
|
||||||
try {
|
|
||||||
this.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
|
||||||
return true
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,20 @@
|
|||||||
package com.mrousavy.camera.extensions
|
package com.mrousavy.camera.extensions
|
||||||
|
|
||||||
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CaptureRequest
|
import android.hardware.camera2.CaptureRequest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
import android.util.Range
|
||||||
import com.mrousavy.camera.types.HardwareLevel
|
|
||||||
|
|
||||||
fun CaptureRequest.Builder.setZoom(zoom: Float, deviceDetails: CameraDeviceDetails) {
|
fun CaptureRequest.Builder.setZoom(zoom: Float, cameraCharacteristics: CameraCharacteristics) {
|
||||||
val zoomRange = deviceDetails.zoomRange
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val zoomClamped = zoomRange.clamp(zoom)
|
val zoomRange = cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) ?: Range(1f, 1f)
|
||||||
|
val zoomClamped = zoomRange.clamp(zoom)
|
||||||
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
this.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomClamped)
|
this.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomClamped)
|
||||||
} else {
|
} else {
|
||||||
val size = deviceDetails.activeSize
|
val maxZoom = cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
|
||||||
|
val zoomRange = Range(1f, maxZoom ?: 1f)
|
||||||
|
val size = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
||||||
|
val zoomClamped = zoomRange.clamp(zoom)
|
||||||
this.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoomClamped))
|
this.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoomClamped))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.graphics.PointF
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Size
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
|
||||||
|
|
||||||
fun Point.rotatedBy(fromSize: Size, toSize: Size, fromOrientation: Orientation, toOrientation: Orientation): Point {
|
|
||||||
val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360
|
|
||||||
val difference = Orientation.fromRotationDegrees(differenceDegrees)
|
|
||||||
val normalizedPoint = PointF(this.x / fromSize.width.toFloat(), this.y / fromSize.height.toFloat())
|
|
||||||
|
|
||||||
val rotatedNormalizedPoint = when (difference) {
|
|
||||||
Orientation.PORTRAIT -> normalizedPoint
|
|
||||||
Orientation.PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y)
|
|
||||||
Orientation.LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x)
|
|
||||||
Orientation.LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x)
|
|
||||||
}
|
|
||||||
|
|
||||||
val rotatedX = rotatedNormalizedPoint.x * toSize.width
|
|
||||||
val rotatedY = rotatedNormalizedPoint.y * toSize.height
|
|
||||||
Log.i("ROTATE", "$this -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY")
|
|
||||||
return Point(rotatedX.toInt(), rotatedY.toInt())
|
|
||||||
}
|
|
@ -4,9 +4,9 @@ import android.media.CamcorderProfile
|
|||||||
import android.media.MediaRecorder.VideoEncoder
|
import android.media.MediaRecorder.VideoEncoder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Size
|
||||||
import com.mrousavy.camera.core.RecordingSession
|
import com.mrousavy.camera.core.RecordingSession
|
||||||
import com.mrousavy.camera.types.VideoCodec
|
import com.mrousavy.camera.types.VideoCodec
|
||||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
data class RecommendedProfile(
|
data class RecommendedProfile(
|
||||||
@ -23,7 +23,7 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
|
|||||||
val targetResolution = size
|
val targetResolution = size
|
||||||
val encoder = codec.toVideoEncoder()
|
val encoder = codec.toVideoEncoder()
|
||||||
val bitDepth = if (hdr) 10 else 8
|
val bitDepth = if (hdr) 10 else 8
|
||||||
val quality = CamcorderProfileUtils.findClosestCamcorderProfileQuality(cameraId, targetResolution, true)
|
val quality = findClosestCamcorderProfileQuality(cameraId, targetResolution)
|
||||||
Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality")
|
Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality")
|
||||||
|
|
||||||
var recommendedProfile: RecommendedProfile? = null
|
var recommendedProfile: RecommendedProfile? = null
|
||||||
@ -75,3 +75,39 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
|
|||||||
}
|
}
|
||||||
return bitRate.toInt()
|
return bitRate.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int =
|
||||||
|
when (camcorderProfile) {
|
||||||
|
CamcorderProfile.QUALITY_QCIF -> 176 * 144
|
||||||
|
CamcorderProfile.QUALITY_QVGA -> 320 * 240
|
||||||
|
CamcorderProfile.QUALITY_CIF -> 352 * 288
|
||||||
|
CamcorderProfile.QUALITY_VGA -> 640 * 480
|
||||||
|
CamcorderProfile.QUALITY_480P -> 720 * 480
|
||||||
|
CamcorderProfile.QUALITY_720P -> 1280 * 720
|
||||||
|
CamcorderProfile.QUALITY_1080P -> 1920 * 1080
|
||||||
|
CamcorderProfile.QUALITY_2K -> 2048 * 1080
|
||||||
|
CamcorderProfile.QUALITY_QHD -> 2560 * 1440
|
||||||
|
CamcorderProfile.QUALITY_2160P -> 3840 * 2160
|
||||||
|
CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160
|
||||||
|
CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320
|
||||||
|
else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size): Int {
|
||||||
|
// Iterate through all available CamcorderProfiles and find the one that matches the closest
|
||||||
|
val targetResolution = resolution.width * resolution.height
|
||||||
|
val cameraIdInt = cameraId.toIntOrNull()
|
||||||
|
|
||||||
|
val profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile ->
|
||||||
|
if (cameraIdInt != null) {
|
||||||
|
return@filter CamcorderProfile.hasProfile(cameraIdInt, profile)
|
||||||
|
} else {
|
||||||
|
return@filter CamcorderProfile.hasProfile(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val closestProfile = profiles.minBy { profile ->
|
||||||
|
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
|
||||||
|
return@minBy abs(currentResolution - targetResolution)
|
||||||
|
}
|
||||||
|
return closestProfile
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ package com.mrousavy.camera.extensions
|
|||||||
|
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.util.SizeF
|
import android.util.SizeF
|
||||||
import com.mrousavy.camera.types.Orientation
|
import android.view.Surface
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -14,10 +14,13 @@ fun List<Size>.closestToOrMax(size: Size?): Size =
|
|||||||
this.maxBy { it.width * it.height }
|
this.maxBy { it.width * it.height }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Size.rotatedBy(orientation: Orientation): Size =
|
fun Size.rotated(surfaceRotation: Int): Size =
|
||||||
when (orientation) {
|
when (surfaceRotation) {
|
||||||
Orientation.PORTRAIT, Orientation.PORTRAIT_UPSIDE_DOWN -> this
|
Surface.ROTATION_0 -> Size(width, height)
|
||||||
Orientation.LANDSCAPE_LEFT, Orientation.LANDSCAPE_RIGHT -> Size(height, width)
|
Surface.ROTATION_90 -> Size(height, width)
|
||||||
|
Surface.ROTATION_180 -> Size(width, height)
|
||||||
|
Surface.ROTATION_270 -> Size(height, width)
|
||||||
|
else -> Size(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
val Size.bigger: Int
|
val Size.bigger: Int
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
package com.mrousavy.camera.extensions
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.SurfaceHolder
|
|
||||||
import androidx.annotation.UiThread
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
|
|
||||||
private const val TAG = "SurfaceHolder"
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
suspend fun SurfaceHolder.resize(targetWidth: Int, targetHeight: Int) {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
val currentSize = this.surfaceFrame
|
|
||||||
if (currentSize.width() == targetWidth && currentSize.height() == targetHeight) {
|
|
||||||
// Already in target size
|
|
||||||
continuation.resume(Unit)
|
|
||||||
return@suspendCancellableCoroutine
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Resizing SurfaceHolder to $targetWidth x $targetHeight...")
|
|
||||||
|
|
||||||
val callback = object : SurfaceHolder.Callback {
|
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) = Unit
|
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
||||||
if (width == targetWidth && height == targetHeight) {
|
|
||||||
holder.removeCallback(this)
|
|
||||||
Log.i(TAG, "Resized SurfaceHolder to $width x $height!")
|
|
||||||
continuation.resume(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
||||||
holder.removeCallback(this)
|
|
||||||
Log.e(TAG, "Failed to resize SurfaceHolder to $targetWidth x $targetHeight!")
|
|
||||||
continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.addCallback(callback)
|
|
||||||
this.setFixedSize(targetWidth, targetHeight)
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import android.hardware.HardwareBuffer;
|
|||||||
import android.media.Image;
|
import android.media.Image;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import com.facebook.proguard.annotations.DoNotStrip;
|
import com.facebook.proguard.annotations.DoNotStrip;
|
||||||
import com.mrousavy.camera.core.FrameInvalidError;
|
|
||||||
import com.mrousavy.camera.core.HardwareBuffersNotAvailableError;
|
import com.mrousavy.camera.core.HardwareBuffersNotAvailableError;
|
||||||
import com.mrousavy.camera.types.PixelFormat;
|
import com.mrousavy.camera.types.PixelFormat;
|
||||||
import com.mrousavy.camera.types.Orientation;
|
import com.mrousavy.camera.types.Orientation;
|
||||||
@ -24,17 +23,42 @@ public class Frame {
|
|||||||
this.isMirrored = isMirrored;
|
this.isMirrored = isMirrored;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertIsValid() throws FrameInvalidError {
|
public Image getImage() {
|
||||||
if (!getIsImageValid(image)) {
|
synchronized (this) {
|
||||||
throw new FrameInvalidError();
|
Image img = image;
|
||||||
|
if (!getIsImageValid(img)) {
|
||||||
|
throw new RuntimeException("Frame is already closed! " +
|
||||||
|
"Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" +
|
||||||
|
"- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" +
|
||||||
|
"- If you want to do async processing, use `runAsync(...)` instead.\n" +
|
||||||
|
"- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`");
|
||||||
|
}
|
||||||
|
return img;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized boolean getIsImageValid(Image image) {
|
@SuppressWarnings("unused")
|
||||||
if (refCount <= 0) return false;
|
@DoNotStrip
|
||||||
|
public int getWidth() {
|
||||||
|
return getImage().getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@DoNotStrip
|
||||||
|
public int getHeight() {
|
||||||
|
return getImage().getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@DoNotStrip
|
||||||
|
public boolean getIsValid() {
|
||||||
|
return getIsImageValid(getImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getIsImageValid(Image image) {
|
||||||
try {
|
try {
|
||||||
// will throw an exception if the image is already closed
|
// will throw an exception if the image is already closed
|
||||||
image.getFormat();
|
synchronized (this) { image.getFormat(); }
|
||||||
// no exception thrown, image must still be valid.
|
// no exception thrown, image must still be valid.
|
||||||
return true;
|
return true;
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
@ -43,104 +67,78 @@ public class Frame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Image getImage() {
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized int getWidth() throws FrameInvalidError {
|
public boolean getIsMirrored() {
|
||||||
assertIsValid();
|
|
||||||
return image.getWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@DoNotStrip
|
|
||||||
public synchronized int getHeight() throws FrameInvalidError {
|
|
||||||
assertIsValid();
|
|
||||||
return image.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@DoNotStrip
|
|
||||||
public synchronized boolean getIsValid() throws FrameInvalidError {
|
|
||||||
assertIsValid();
|
|
||||||
return getIsImageValid(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@DoNotStrip
|
|
||||||
public synchronized boolean getIsMirrored() throws FrameInvalidError {
|
|
||||||
assertIsValid();
|
|
||||||
return isMirrored;
|
return isMirrored;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized long getTimestamp() throws FrameInvalidError {
|
public long getTimestamp() {
|
||||||
assertIsValid();
|
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized Orientation getOrientation() throws FrameInvalidError {
|
public Orientation getOrientation() {
|
||||||
assertIsValid();
|
|
||||||
return orientation;
|
return orientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized PixelFormat getPixelFormat() throws FrameInvalidError {
|
public PixelFormat getPixelFormat() {
|
||||||
assertIsValid();
|
return PixelFormat.Companion.fromImageFormat(getImage().getFormat());
|
||||||
return PixelFormat.Companion.fromImageFormat(image.getFormat());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized int getPlanesCount() throws FrameInvalidError {
|
public int getPlanesCount() {
|
||||||
assertIsValid();
|
return getImage().getPlanes().length;
|
||||||
return image.getPlanes().length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized int getBytesPerRow() throws FrameInvalidError {
|
public int getBytesPerRow() {
|
||||||
assertIsValid();
|
return getImage().getPlanes()[0].getRowStride();
|
||||||
return image.getPlanes()[0].getRowStride();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
private Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError, FrameInvalidError {
|
public Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError {
|
||||||
return getHardwareBuffer();
|
return getHardwareBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError, FrameInvalidError {
|
public HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
throw new HardwareBuffersNotAvailableError();
|
throw new HardwareBuffersNotAvailableError();
|
||||||
}
|
}
|
||||||
assertIsValid();
|
return getImage().getHardwareBuffer();
|
||||||
return image.getHardwareBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
public synchronized void incrementRefCount() {
|
public void incrementRefCount() {
|
||||||
refCount++;
|
synchronized (this) {
|
||||||
}
|
refCount++;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@DoNotStrip
|
|
||||||
public synchronized void decrementRefCount() {
|
|
||||||
refCount--;
|
|
||||||
if (refCount <= 0) {
|
|
||||||
// If no reference is held on this Image, close it.
|
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void close() {
|
@SuppressWarnings("unused")
|
||||||
image.close();
|
@DoNotStrip
|
||||||
|
public void decrementRefCount() {
|
||||||
|
synchronized (this) {
|
||||||
|
refCount--;
|
||||||
|
if (refCount <= 0) {
|
||||||
|
// If no reference is held on this Image, close it.
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
synchronized (this) {
|
||||||
|
image.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,6 @@ public final class FrameProcessor {
|
|||||||
@Keep
|
@Keep
|
||||||
private final HybridData mHybridData;
|
private final HybridData mHybridData;
|
||||||
|
|
||||||
@DoNotStrip
|
|
||||||
@Keep
|
|
||||||
public FrameProcessor(HybridData hybridData) {
|
public FrameProcessor(HybridData hybridData) {
|
||||||
mHybridData = hybridData;
|
mHybridData = hybridData;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import com.mrousavy.camera.core.ViewNotFoundError
|
|||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
@Suppress("KotlinJniMissingFunction") // we use fbjni.
|
@Suppress("KotlinJniMissingFunction") // we use fbjni.
|
||||||
class VisionCameraProxy(private val reactContext: ReactApplicationContext) {
|
class VisionCameraProxy(context: ReactApplicationContext) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "VisionCameraProxy"
|
const val TAG = "VisionCameraProxy"
|
||||||
}
|
}
|
||||||
@ -24,8 +24,6 @@ class VisionCameraProxy(private val reactContext: ReactApplicationContext) {
|
|||||||
private var mHybridData: HybridData
|
private var mHybridData: HybridData
|
||||||
private var mContext: WeakReference<ReactApplicationContext>
|
private var mContext: WeakReference<ReactApplicationContext>
|
||||||
private var mScheduler: VisionCameraScheduler
|
private var mScheduler: VisionCameraScheduler
|
||||||
val context: ReactApplicationContext
|
|
||||||
get() = reactContext
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
|
val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
|
||||||
|
@ -3,12 +3,20 @@ package com.mrousavy.camera.types
|
|||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
|
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
|
||||||
|
|
||||||
data class CodeScannerOptions(val codeTypes: List<CodeType>) {
|
class CodeScannerOptions(map: ReadableMap) {
|
||||||
companion object {
|
val codeTypes: List<CodeType>
|
||||||
fun fromJSValue(value: ReadableMap): CodeScannerOptions {
|
|
||||||
val jsCodeTypes = value.getArray("codeTypes") ?: throw InvalidTypeScriptUnionError("codeScanner", value.toString())
|
init {
|
||||||
val codeTypes = jsCodeTypes.toArrayList().map { CodeType.fromUnionValue(it as String) }
|
val codeTypes = map.getArray("codeTypes")?.toArrayList() ?: throw InvalidTypeScriptUnionError("codeScanner", map.toString())
|
||||||
return CodeScannerOptions(codeTypes)
|
this.codeTypes = codeTypes.map {
|
||||||
|
return@map CodeType.fromUnionValue(it as String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is CodeScannerOptions) return false
|
||||||
|
return codeTypes.size == other.codeTypes.size && codeTypes.containsAll(other.codeTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int = codeTypes.hashCode()
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ enum class CodeType(override val unionValue: String) : JSUnionValue {
|
|||||||
EAN_8("ean-8"),
|
EAN_8("ean-8"),
|
||||||
ITF("itf"),
|
ITF("itf"),
|
||||||
UPC_E("upc-e"),
|
UPC_E("upc-e"),
|
||||||
UPC_A("upc-a"),
|
|
||||||
QR("qr"),
|
QR("qr"),
|
||||||
PDF_417("pdf-417"),
|
PDF_417("pdf-417"),
|
||||||
AZTEC("aztec"),
|
AZTEC("aztec"),
|
||||||
@ -30,7 +29,6 @@ enum class CodeType(override val unionValue: String) : JSUnionValue {
|
|||||||
EAN_8 -> Barcode.FORMAT_EAN_8
|
EAN_8 -> Barcode.FORMAT_EAN_8
|
||||||
ITF -> Barcode.FORMAT_ITF
|
ITF -> Barcode.FORMAT_ITF
|
||||||
UPC_E -> Barcode.FORMAT_UPC_E
|
UPC_E -> Barcode.FORMAT_UPC_E
|
||||||
UPC_A -> Barcode.FORMAT_UPC_A
|
|
||||||
QR -> Barcode.FORMAT_QR_CODE
|
QR -> Barcode.FORMAT_QR_CODE
|
||||||
PDF_417 -> Barcode.FORMAT_PDF417
|
PDF_417 -> Barcode.FORMAT_PDF417
|
||||||
AZTEC -> Barcode.FORMAT_AZTEC
|
AZTEC -> Barcode.FORMAT_AZTEC
|
||||||
@ -49,7 +47,6 @@ enum class CodeType(override val unionValue: String) : JSUnionValue {
|
|||||||
Barcode.FORMAT_EAN_8 -> EAN_8
|
Barcode.FORMAT_EAN_8 -> EAN_8
|
||||||
Barcode.FORMAT_ITF -> ITF
|
Barcode.FORMAT_ITF -> ITF
|
||||||
Barcode.FORMAT_UPC_E -> UPC_E
|
Barcode.FORMAT_UPC_E -> UPC_E
|
||||||
Barcode.FORMAT_UPC_A -> UPC_A
|
|
||||||
Barcode.FORMAT_QR_CODE -> QR
|
Barcode.FORMAT_QR_CODE -> QR
|
||||||
Barcode.FORMAT_PDF417 -> PDF_417
|
Barcode.FORMAT_PDF417 -> PDF_417
|
||||||
Barcode.FORMAT_AZTEC -> AZTEC
|
Barcode.FORMAT_AZTEC -> AZTEC
|
||||||
@ -67,7 +64,6 @@ enum class CodeType(override val unionValue: String) : JSUnionValue {
|
|||||||
"ean-8" -> EAN_8
|
"ean-8" -> EAN_8
|
||||||
"itf" -> ITF
|
"itf" -> ITF
|
||||||
"upc-e" -> UPC_E
|
"upc-e" -> UPC_E
|
||||||
"upc-a" -> UPC_A
|
|
||||||
"qr" -> QR
|
"qr" -> QR
|
||||||
"pdf-417" -> PDF_417
|
"pdf-417" -> PDF_417
|
||||||
"aztec" -> AZTEC
|
"aztec" -> AZTEC
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package com.mrousavy.camera.types
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments
|
|
||||||
import com.facebook.react.bridge.WritableMap
|
|
||||||
import com.facebook.react.uimanager.events.Event
|
|
||||||
|
|
||||||
class CameraInitializedEvent(surfaceId: Int, viewId: Int) : Event<CameraInitializedEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraInitialized"
|
|
||||||
override fun getEventData(): WritableMap = Arguments.createMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraStartedEvent(surfaceId: Int, viewId: Int) : Event<CameraStartedEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraStarted"
|
|
||||||
override fun getEventData(): WritableMap = Arguments.createMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraStoppedEvent(surfaceId: Int, viewId: Int) : Event<CameraStoppedEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraStopped"
|
|
||||||
override fun getEventData(): WritableMap = Arguments.createMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraErrorEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event<CameraErrorEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraError"
|
|
||||||
override fun getEventData() = data
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraViewReadyEvent(surfaceId: Int, viewId: Int) : Event<CameraViewReadyEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraViewReady"
|
|
||||||
override fun getEventData(): WritableMap = Arguments.createMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) :
|
|
||||||
Event<CameraCodeScannedEvent>(surfaceId, viewId) {
|
|
||||||
override fun getEventName() = "cameraCodeScanned"
|
|
||||||
override fun getEventData() = data
|
|
||||||
}
|
|
@ -9,19 +9,6 @@ enum class HardwareLevel(override val unionValue: String) : JSUnionValue {
|
|||||||
FULL("full"),
|
FULL("full"),
|
||||||
LEVEL_3("full");
|
LEVEL_3("full");
|
||||||
|
|
||||||
private val rank: Int
|
|
||||||
get() {
|
|
||||||
return when (this) {
|
|
||||||
LEGACY -> 0
|
|
||||||
LIMITED -> 1
|
|
||||||
EXTERNAL -> 1
|
|
||||||
FULL -> 2
|
|
||||||
LEVEL_3 -> 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAtLeast(level: HardwareLevel): Boolean = this.rank >= level.rank
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel =
|
fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel =
|
||||||
when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) {
|
when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package com.mrousavy.camera.types
|
package com.mrousavy.camera.types
|
||||||
|
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
|
||||||
enum class Orientation(override val unionValue: String) : JSUnionValue {
|
enum class Orientation(override val unionValue: String) : JSUnionValue {
|
||||||
PORTRAIT("portrait"),
|
PORTRAIT("portrait"),
|
||||||
@ -19,22 +19,23 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
|
|||||||
fun toDegrees(): Int =
|
fun toDegrees(): Int =
|
||||||
when (this) {
|
when (this) {
|
||||||
PORTRAIT -> 0
|
PORTRAIT -> 0
|
||||||
LANDSCAPE_LEFT -> 90
|
LANDSCAPE_RIGHT -> 90
|
||||||
PORTRAIT_UPSIDE_DOWN -> 180
|
PORTRAIT_UPSIDE_DOWN -> 180
|
||||||
LANDSCAPE_RIGHT -> 270
|
LANDSCAPE_LEFT -> 270
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation {
|
fun toSensorRelativeOrientation(cameraCharacteristics: CameraCharacteristics): Orientation {
|
||||||
|
val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
|
||||||
|
|
||||||
// Convert target orientation to rotation degrees (0, 90, 180, 270)
|
// Convert target orientation to rotation degrees (0, 90, 180, 270)
|
||||||
var rotationDegrees = this.toDegrees()
|
var rotationDegrees = this.toDegrees()
|
||||||
|
|
||||||
// Reverse device orientation for front-facing cameras
|
// Reverse device orientation for front-facing cameras
|
||||||
if (deviceDetails.lensFacing == LensFacing.FRONT) {
|
val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||||
rotationDegrees = -rotationDegrees
|
if (facingFront) rotationDegrees = -rotationDegrees
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate sensor rotation by target rotation
|
// Rotate sensor rotation by target rotation
|
||||||
val newRotationDegrees = (deviceDetails.sensorOrientation.toDegrees() + rotationDegrees + 360) % 360
|
val newRotationDegrees = (sensorOrientation + rotationDegrees + 360) % 360
|
||||||
|
|
||||||
return fromRotationDegrees(newRotationDegrees)
|
return fromRotationDegrees(newRotationDegrees)
|
||||||
}
|
}
|
||||||
@ -51,9 +52,9 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
|
|||||||
|
|
||||||
fun fromRotationDegrees(rotationDegrees: Int): Orientation =
|
fun fromRotationDegrees(rotationDegrees: Int): Orientation =
|
||||||
when (rotationDegrees) {
|
when (rotationDegrees) {
|
||||||
in 45..135 -> LANDSCAPE_LEFT
|
in 45..135 -> LANDSCAPE_RIGHT
|
||||||
in 135..225 -> PORTRAIT_UPSIDE_DOWN
|
in 135..225 -> PORTRAIT_UPSIDE_DOWN
|
||||||
in 225..315 -> LANDSCAPE_RIGHT
|
in 225..315 -> LANDSCAPE_LEFT
|
||||||
else -> PORTRAIT
|
else -> PORTRAIT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ class RecordVideoOptions(map: ReadableMap) {
|
|||||||
var videoCodec = VideoCodec.H264
|
var videoCodec = VideoCodec.H264
|
||||||
var videoBitRateOverride: Double? = null
|
var videoBitRateOverride: Double? = null
|
||||||
var videoBitRateMultiplier: Double? = null
|
var videoBitRateMultiplier: Double? = null
|
||||||
var orientation: Orientation? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (map.hasKey("fileType")) {
|
if (map.hasKey("fileType")) {
|
||||||
@ -26,8 +25,5 @@ class RecordVideoOptions(map: ReadableMap) {
|
|||||||
if (map.hasKey("videoBitRateMultiplier")) {
|
if (map.hasKey("videoBitRateMultiplier")) {
|
||||||
videoBitRateMultiplier = map.getDouble("videoBitRateMultiplier")
|
videoBitRateMultiplier = map.getDouble("videoBitRateMultiplier")
|
||||||
}
|
}
|
||||||
if (map.hasKey("orientation")) {
|
|
||||||
orientation = Orientation.fromUnionValue(map.getString("orientation"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package com.mrousavy.camera.types
|
package com.mrousavy.camera.types
|
||||||
|
|
||||||
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
|
|
||||||
|
|
||||||
enum class ResizeMode(override val unionValue: String) : JSUnionValue {
|
enum class ResizeMode(override val unionValue: String) : JSUnionValue {
|
||||||
COVER("cover"),
|
COVER("cover"),
|
||||||
CONTAIN("contain");
|
CONTAIN("contain");
|
||||||
@ -11,7 +9,7 @@ enum class ResizeMode(override val unionValue: String) : JSUnionValue {
|
|||||||
when (unionValue) {
|
when (unionValue) {
|
||||||
"cover" -> COVER
|
"cover" -> COVER
|
||||||
"contain" -> CONTAIN
|
"contain" -> CONTAIN
|
||||||
else -> throw InvalidTypeScriptUnionError("resizeMode", unionValue)
|
else -> COVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,21 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu
|
|||||||
CINEMATIC("cinematic"),
|
CINEMATIC("cinematic"),
|
||||||
CINEMATIC_EXTENDED("cinematic-extended");
|
CINEMATIC_EXTENDED("cinematic-extended");
|
||||||
|
|
||||||
|
fun toDigitalStabilizationMode(): Int =
|
||||||
|
when (this) {
|
||||||
|
OFF -> CONTROL_VIDEO_STABILIZATION_MODE_OFF
|
||||||
|
STANDARD -> CONTROL_VIDEO_STABILIZATION_MODE_ON
|
||||||
|
CINEMATIC -> 2 // TODO: CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
|
||||||
|
else -> CONTROL_VIDEO_STABILIZATION_MODE_OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOpticalStabilizationMode(): Int =
|
||||||
|
when (this) {
|
||||||
|
OFF -> LENS_OPTICAL_STABILIZATION_MODE_OFF
|
||||||
|
CINEMATIC_EXTENDED -> LENS_OPTICAL_STABILIZATION_MODE_ON
|
||||||
|
else -> LENS_OPTICAL_STABILIZATION_MODE_OFF
|
||||||
|
}
|
||||||
|
|
||||||
companion object : JSUnionValue.Companion<VideoStabilizationMode> {
|
companion object : JSUnionValue.Companion<VideoStabilizationMode> {
|
||||||
override fun fromUnionValue(unionValue: String?): VideoStabilizationMode =
|
override fun fromUnionValue(unionValue: String?): VideoStabilizationMode =
|
||||||
when (unionValue) {
|
when (unionValue) {
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
package com.mrousavy.camera.utils
|
|
||||||
|
|
||||||
import android.media.CamcorderProfile
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Size
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class CamcorderProfileUtils {
|
|
||||||
companion object {
|
|
||||||
private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int =
|
|
||||||
when (camcorderProfile) {
|
|
||||||
CamcorderProfile.QUALITY_QCIF -> 176 * 144
|
|
||||||
CamcorderProfile.QUALITY_QVGA -> 320 * 240
|
|
||||||
CamcorderProfile.QUALITY_CIF -> 352 * 288
|
|
||||||
CamcorderProfile.QUALITY_VGA -> 640 * 480
|
|
||||||
CamcorderProfile.QUALITY_480P -> 720 * 480
|
|
||||||
CamcorderProfile.QUALITY_720P -> 1280 * 720
|
|
||||||
CamcorderProfile.QUALITY_1080P -> 1920 * 1080
|
|
||||||
CamcorderProfile.QUALITY_2K -> 2048 * 1080
|
|
||||||
CamcorderProfile.QUALITY_QHD -> 2560 * 1440
|
|
||||||
CamcorderProfile.QUALITY_2160P -> 3840 * 2160
|
|
||||||
CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160
|
|
||||||
CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320
|
|
||||||
else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size, allowLargerSize: Boolean): Int {
|
|
||||||
// Iterate through all available CamcorderProfiles and find the one that matches the closest
|
|
||||||
val targetResolution = resolution.width * resolution.height
|
|
||||||
val cameraIdInt = cameraId.toIntOrNull()
|
|
||||||
|
|
||||||
var profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile ->
|
|
||||||
if (cameraIdInt != null) {
|
|
||||||
return@filter CamcorderProfile.hasProfile(cameraIdInt, profile)
|
|
||||||
} else {
|
|
||||||
return@filter CamcorderProfile.hasProfile(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!allowLargerSize) {
|
|
||||||
profiles = profiles.filter { profile ->
|
|
||||||
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
|
|
||||||
return@filter currentResolution <= targetResolution
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val closestProfile = profiles.minBy { profile ->
|
|
||||||
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
|
|
||||||
return@minBy abs(currentResolution - targetResolution)
|
|
||||||
}
|
|
||||||
return closestProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMaximumVideoSize(cameraId: String): Size? {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH)
|
|
||||||
if (profiles != null) {
|
|
||||||
val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height }
|
|
||||||
if (largestProfile != null) {
|
|
||||||
return Size(largestProfile.width, largestProfile.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraIdInt = cameraId.toIntOrNull()
|
|
||||||
if (cameraIdInt != null) {
|
|
||||||
val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH)
|
|
||||||
return Size(profile.videoFrameWidth, profile.videoFrameHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMaximumFps(cameraId: String, size: Size): Int? {
|
|
||||||
try {
|
|
||||||
val quality = findClosestCamcorderProfileQuality(cameraId, size, false)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val profiles = CamcorderProfile.getAll(cameraId, quality)
|
|
||||||
if (profiles != null) {
|
|
||||||
return profiles.videoProfiles.maxOf { profile -> profile.frameRate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraIdInt = cameraId.toIntOrNull()
|
|
||||||
if (cameraIdInt != null) {
|
|
||||||
val profile = CamcorderProfile.get(cameraIdInt, quality)
|
|
||||||
return profile.videoFrameRate
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -30,7 +30,7 @@ To try the playground out for yourself, run the following commands:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/mrousavy/react-native-vision-camera
|
git clone https://github.com/mrousavy/react-native-vision-camera
|
||||||
cd react-native-vision-camera/package
|
cd react-native-vision-camera
|
||||||
yarn bootstrap
|
yarn bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -484,7 +484,7 @@ PODS:
|
|||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.10)
|
- SDWebImage/Core (~> 5.10)
|
||||||
- SocketRocket (0.6.1)
|
- SocketRocket (0.6.1)
|
||||||
- VisionCamera (3.9.0-beta.6):
|
- VisionCamera (3.8.2):
|
||||||
- React
|
- React
|
||||||
- React-callinvoker
|
- React-callinvoker
|
||||||
- React-Core
|
- React-Core
|
||||||
@ -724,9 +724,9 @@ SPEC CHECKSUMS:
|
|||||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
||||||
VisionCamera: 33c90675adf75528199f840f81dfbe74a2fe6c3f
|
VisionCamera: edbcd00e27a438b2228f67823e2b8d15a189065f
|
||||||
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5
|
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5
|
||||||
|
|
||||||
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
|
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
|
||||||
|
|
||||||
COCOAPODS: 1.14.3
|
COCOAPODS: 1.11.3
|
||||||
|
@ -11,7 +11,7 @@ import AVFoundation
|
|||||||
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
|
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
|
||||||
|
|
||||||
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
|
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
|
||||||
func startRecording(options: NSDictionary, filePath: String, callback jsCallback: @escaping RCTResponseSenderBlock) {
|
func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) {
|
||||||
// Type-safety
|
// Type-safety
|
||||||
let callback = Callback(jsCallback)
|
let callback = Callback(jsCallback)
|
||||||
|
|
||||||
@ -21,7 +21,6 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
|||||||
// Start Recording with success and error callbacks
|
// Start Recording with success and error callbacks
|
||||||
cameraSession.startRecording(
|
cameraSession.startRecording(
|
||||||
options: options,
|
options: options,
|
||||||
filePath: filePath,
|
|
||||||
onVideoRecorded: { video in
|
onVideoRecorded: { video in
|
||||||
callback.resolve(video.toJSValue())
|
callback.resolve(video.toJSValue())
|
||||||
},
|
},
|
||||||
@ -50,12 +49,4 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
|||||||
func resumeRecording(promise: Promise) {
|
func resumeRecording(promise: Promise) {
|
||||||
cameraSession.resumeRecording(promise: promise)
|
cameraSession.resumeRecording(promise: promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lockExposure(promise: Promise) {
|
|
||||||
cameraSession.lockCurrentExposure(promise: promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockExposure(promise: Promise) {
|
|
||||||
cameraSession.unlockCurrentExposure(promise: promise)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -62,8 +62,6 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
@objc var onStarted: RCTDirectEventBlock?
|
@objc var onStarted: RCTDirectEventBlock?
|
||||||
@objc var onStopped: RCTDirectEventBlock?
|
@objc var onStopped: RCTDirectEventBlock?
|
||||||
@objc var onViewReady: RCTDirectEventBlock?
|
@objc var onViewReady: RCTDirectEventBlock?
|
||||||
@objc var onInitReady: RCTDirectEventBlock?
|
|
||||||
@objc var onVideoChunkReady: RCTDirectEventBlock?
|
|
||||||
@objc var onCodeScanned: RCTDirectEventBlock?
|
@objc var onCodeScanned: RCTDirectEventBlock?
|
||||||
// zoom
|
// zoom
|
||||||
@objc var enableZoomGesture = false {
|
@objc var enableZoomGesture = false {
|
||||||
@ -303,15 +301,6 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
}
|
}
|
||||||
onInitialized([:])
|
onInitialized([:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func onCameraConfigurationChanged(_ configuration: CameraConfiguration?, _ difference: CameraConfiguration.Difference?) {
|
|
||||||
guard let configuration, let difference else { return }
|
|
||||||
|
|
||||||
if difference.orientationChanged, let connection = previewView.videoPreviewLayer.connection {
|
|
||||||
let videoPreviewLayer = previewView.videoPreviewLayer
|
|
||||||
connection.setOrientation(configuration.orientation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func onCameraStarted() {
|
func onCameraStarted() {
|
||||||
ReactLogger.log(level: .info, message: "Camera started!")
|
ReactLogger.log(level: .info, message: "Camera started!")
|
||||||
@ -346,31 +335,6 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk) {
|
|
||||||
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
|
|
||||||
|
|
||||||
guard let onVideoChunkReady, let onInitReady else {
|
|
||||||
ReactLogger.log(level: .warning, message: "Either onInitReady or onVideoChunkReady are not valid!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch chunk.type {
|
|
||||||
case .initialization:
|
|
||||||
onInitReady([
|
|
||||||
"filepath": chunk.url.path,
|
|
||||||
])
|
|
||||||
case let .data(index: index, duration: duration):
|
|
||||||
var data: [String: Any] = [
|
|
||||||
"filepath": chunk.url.path,
|
|
||||||
"index": index,
|
|
||||||
]
|
|
||||||
if let duration {
|
|
||||||
data["duration"] = duration.seconds
|
|
||||||
}
|
|
||||||
onVideoChunkReady(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) {
|
func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) {
|
||||||
guard let onCodeScanned = onCodeScanned else {
|
guard let onCodeScanned = onCodeScanned else {
|
||||||
|
@ -55,8 +55,6 @@ RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock);
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onInitReady, RCTDirectEventBlock);
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoChunkReady, RCTDirectEventBlock);
|
|
||||||
// Code Scanner
|
// Code Scanner
|
||||||
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
|
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
|
||||||
@ -64,8 +62,7 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
|
|||||||
// Camera View Functions
|
// Camera View Functions
|
||||||
RCT_EXTERN_METHOD(startRecording
|
RCT_EXTERN_METHOD(startRecording
|
||||||
: (nonnull NSNumber*)node options
|
: (nonnull NSNumber*)node options
|
||||||
: (NSDictionary*)options filePath
|
: (NSDictionary*)options onRecordCallback
|
||||||
: (NSString*)filePath onRecordCallback
|
|
||||||
: (RCTResponseSenderBlock)onRecordCallback);
|
: (RCTResponseSenderBlock)onRecordCallback);
|
||||||
RCT_EXTERN_METHOD(pauseRecording
|
RCT_EXTERN_METHOD(pauseRecording
|
||||||
: (nonnull NSNumber*)node resolve
|
: (nonnull NSNumber*)node resolve
|
||||||
@ -86,13 +83,5 @@ RCT_EXTERN_METHOD(focus
|
|||||||
: (NSDictionary*)point resolve
|
: (NSDictionary*)point resolve
|
||||||
: (RCTPromiseResolveBlock)resolve reject
|
: (RCTPromiseResolveBlock)resolve reject
|
||||||
: (RCTPromiseRejectBlock)reject);
|
: (RCTPromiseRejectBlock)reject);
|
||||||
RCT_EXTERN_METHOD(lockCurrentExposure
|
|
||||||
: (nonnull NSNumber*)node resolve
|
|
||||||
: (RCTPromiseResolveBlock)resolve reject
|
|
||||||
: (RCTPromiseRejectBlock)reject);
|
|
||||||
RCT_EXTERN_METHOD(unlockCurrentExposure
|
|
||||||
: (nonnull NSNumber*)node resolve
|
|
||||||
: (RCTPromiseResolveBlock)resolve reject
|
|
||||||
: (RCTPromiseRejectBlock)reject);
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager {
|
|||||||
// This means that any errors that occur in this function have to be delegated through
|
// This means that any errors that occur in this function have to be delegated through
|
||||||
// the callback, but I'd prefer for them to throw for the original function instead.
|
// the callback, but I'd prefer for them to throw for the original function instead.
|
||||||
@objc
|
@objc
|
||||||
final func startRecording(_ node: NSNumber, options: NSDictionary, filePath: NSString, onRecordCallback: @escaping RCTResponseSenderBlock) {
|
final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) {
|
||||||
let component = getCameraView(withTag: node)
|
let component = getCameraView(withTag: node)
|
||||||
component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback)
|
component.startRecording(options: options, callback: onRecordCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@ -110,18 +110,6 @@ final class CameraViewManager: RCTViewManager {
|
|||||||
resolve(result.descriptor)
|
resolve(result.descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
|
||||||
final func lockCurrentExposure(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
||||||
let component = getCameraView(withTag: node)
|
|
||||||
component.lockExposure(promise: Promise(resolver: resolve, rejecter: reject))
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
final func unlockCurrentExposure(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
||||||
let component = getCameraView(withTag: node)
|
|
||||||
component.unlockExposure(promise: Promise(resolver: resolve, rejecter: reject))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ enum DeviceError: String {
|
|||||||
case .lowLightBoostNotSupported:
|
case .lowLightBoostNotSupported:
|
||||||
return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
|
return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
|
||||||
case .focusNotSupported:
|
case .focusNotSupported:
|
||||||
return "The currently selected camera device does not support focusing!"
|
return "The currently selected camera device does not support focussing!"
|
||||||
case .microphoneUnavailable:
|
case .microphoneUnavailable:
|
||||||
return "The microphone was unavailable."
|
return "The microphone was unavailable."
|
||||||
case .notAvailableOnSimulator:
|
case .notAvailableOnSimulator:
|
||||||
@ -176,7 +176,6 @@ enum CaptureError {
|
|||||||
case noRecordingInProgress
|
case noRecordingInProgress
|
||||||
case fileError
|
case fileError
|
||||||
case createTempFileError(message: String? = nil)
|
case createTempFileError(message: String? = nil)
|
||||||
case createRecordingDirectoryError(message: String? = nil)
|
|
||||||
case createRecorderError(message: String? = nil)
|
case createRecorderError(message: String? = nil)
|
||||||
case videoNotEnabled
|
case videoNotEnabled
|
||||||
case photoNotEnabled
|
case photoNotEnabled
|
||||||
@ -194,8 +193,6 @@ enum CaptureError {
|
|||||||
return "file-io-error"
|
return "file-io-error"
|
||||||
case .createTempFileError:
|
case .createTempFileError:
|
||||||
return "create-temp-file-error"
|
return "create-temp-file-error"
|
||||||
case .createRecordingDirectoryError:
|
|
||||||
return "create-recording-directory-error"
|
|
||||||
case .createRecorderError:
|
case .createRecorderError:
|
||||||
return "create-recorder-error"
|
return "create-recorder-error"
|
||||||
case .videoNotEnabled:
|
case .videoNotEnabled:
|
||||||
@ -221,8 +218,6 @@ enum CaptureError {
|
|||||||
return "An unexpected File IO error occured!"
|
return "An unexpected File IO error occured!"
|
||||||
case let .createTempFileError(message: message):
|
case let .createTempFileError(message: message):
|
||||||
return "Failed to create a temporary file! \(message ?? "(no additional message)")"
|
return "Failed to create a temporary file! \(message ?? "(no additional message)")"
|
||||||
case let .createRecordingDirectoryError(message: message):
|
|
||||||
return "Failed to create a recording directory! \(message ?? "(no additional message)")"
|
|
||||||
case let .createRecorderError(message: message):
|
case let .createRecorderError(message: message):
|
||||||
return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")"
|
return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")"
|
||||||
case .videoNotEnabled:
|
case .videoNotEnabled:
|
||||||
|
@ -15,7 +15,6 @@ extension CameraSession {
|
|||||||
Starts a video + audio recording with a custom Asset Writer.
|
Starts a video + audio recording with a custom Asset Writer.
|
||||||
*/
|
*/
|
||||||
func startRecording(options: RecordVideoOptions,
|
func startRecording(options: RecordVideoOptions,
|
||||||
filePath: String,
|
|
||||||
onVideoRecorded: @escaping (_ video: Video) -> Void,
|
onVideoRecorded: @escaping (_ video: Video) -> Void,
|
||||||
onError: @escaping (_ error: CameraError) -> Void) {
|
onError: @escaping (_ error: CameraError) -> Void) {
|
||||||
// Run on Camera Queue
|
// Run on Camera Queue
|
||||||
@ -35,14 +34,6 @@ extension CameraSession {
|
|||||||
|
|
||||||
let enableAudio = self.configuration?.audio != .disabled
|
let enableAudio = self.configuration?.audio != .disabled
|
||||||
|
|
||||||
// Callback for when new chunks are ready
|
|
||||||
let onChunkReady: (ChunkedRecorder.Chunk) -> Void = { chunk in
|
|
||||||
guard let delegate = self.delegate else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delegate.onVideoChunkReady(chunk: chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback for when the recording ends
|
// Callback for when the recording ends
|
||||||
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
|
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
|
||||||
defer {
|
defer {
|
||||||
@ -71,7 +62,7 @@ extension CameraSession {
|
|||||||
} else {
|
} else {
|
||||||
if status == .completed {
|
if status == .completed {
|
||||||
// Recording was successfully saved
|
// Recording was successfully saved
|
||||||
let video = Video(path: recordingSession.outputDiretory.absoluteString,
|
let video = Video(path: recordingSession.url.absoluteString,
|
||||||
duration: recordingSession.duration,
|
duration: recordingSession.duration,
|
||||||
size: recordingSession.size ?? CGSize.zero)
|
size: recordingSession.size ?? CGSize.zero)
|
||||||
onVideoRecorded(video)
|
onVideoRecorded(video)
|
||||||
@ -82,22 +73,22 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !FileManager.default.fileExists(atPath: filePath) {
|
// Create temporary file
|
||||||
do {
|
let errorPointer = ErrorPointer(nilLiteral: ())
|
||||||
try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true)
|
let fileExtension = options.fileType.descriptor ?? "mov"
|
||||||
} catch {
|
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
|
||||||
onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription)))
|
let message = errorPointer?.pointee?.description
|
||||||
return
|
onError(.capture(.createTempFileError(message: message)))
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)")
|
ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)")
|
||||||
|
let tempURL = URL(string: "file://\(tempFilePath)")!
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Create RecordingSession for the temp file
|
// Create RecordingSession for the temp file
|
||||||
let recordingSession = try RecordingSession(outputDiretory: filePath,
|
let recordingSession = try RecordingSession(url: tempURL,
|
||||||
fileType: options.fileType,
|
fileType: options.fileType,
|
||||||
onChunkReady: onChunkReady,
|
|
||||||
completion: onFinish)
|
completion: onFinish)
|
||||||
|
|
||||||
// Init Audio + Activate Audio Session (optional)
|
// Init Audio + Activate Audio Session (optional)
|
||||||
@ -191,68 +182,4 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lockCurrentExposure(promise: Promise) {
|
|
||||||
CameraQueues.cameraQueue.async {
|
|
||||||
withPromise(promise) {
|
|
||||||
guard let captureDevice = AVCaptureDevice.default(for: .video) else {
|
|
||||||
print("No capture device available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard captureDevice.isExposureModeSupported(.custom) else {
|
|
||||||
ReactLogger.log(level: .info, message: "Custom exposure mode not supported")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
// Lock the device for configuration
|
|
||||||
try captureDevice.lockForConfiguration()
|
|
||||||
|
|
||||||
// Get the current exposure duration and ISO
|
|
||||||
let currentExposureDuration = captureDevice.exposureDuration
|
|
||||||
let currentISO = captureDevice.iso
|
|
||||||
|
|
||||||
// Check if the device supports custom exposure settings
|
|
||||||
if captureDevice.isExposureModeSupported(.custom) {
|
|
||||||
// Lock the current exposure and ISO by setting custom exposure mode
|
|
||||||
captureDevice.setExposureModeCustom(duration: currentExposureDuration, iso: currentISO, completionHandler: nil)
|
|
||||||
ReactLogger.log(level: .info, message: "Exposure and ISO locked at current values")
|
|
||||||
} else {
|
|
||||||
ReactLogger.log(level: .info, message:"Custom exposure mode not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the device after configuration
|
|
||||||
captureDevice.unlockForConfiguration()
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
ReactLogger.log(level: .warning, message:"Error locking exposure: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockCurrentExposure(promise: Promise) {
|
|
||||||
CameraQueues.cameraQueue.async {
|
|
||||||
withPromise(promise) {
|
|
||||||
guard let captureDevice = AVCaptureDevice.default(for: .video) else {
|
|
||||||
print("No capture device available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
if captureDevice.isExposureModeSupported(.autoExpose) {
|
|
||||||
try captureDevice.lockForConfiguration()
|
|
||||||
captureDevice.exposureMode = .continuousAutoExposure
|
|
||||||
captureDevice.unlockForConfiguration()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
ReactLogger.log(level: .warning, message:"Error unlocking exposure: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,6 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
try lambda(config)
|
try lambda(config)
|
||||||
} catch {
|
} catch {
|
||||||
self.onConfigureError(error)
|
self.onConfigureError(error)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let difference = CameraConfiguration.Difference(between: self.configuration, and: config)
|
let difference = CameraConfiguration.Difference(between: self.configuration, and: config)
|
||||||
|
|
||||||
@ -118,64 +117,51 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
do {
|
do {
|
||||||
// If needed, configure the AVCaptureSession (inputs, outputs)
|
// If needed, configure the AVCaptureSession (inputs, outputs)
|
||||||
if difference.isSessionConfigurationDirty {
|
if difference.isSessionConfigurationDirty {
|
||||||
self.captureSession.beginConfiguration()
|
try self.withSessionLock {
|
||||||
|
// 1. Update input device
|
||||||
// 1. Update input device
|
if difference.inputChanged {
|
||||||
if difference.inputChanged {
|
try self.configureDevice(configuration: config)
|
||||||
try self.configureDevice(configuration: config)
|
}
|
||||||
|
// 2. Update outputs
|
||||||
|
if difference.outputsChanged {
|
||||||
|
try self.configureOutputs(configuration: config)
|
||||||
|
}
|
||||||
|
// 3. Update Video Stabilization
|
||||||
|
if difference.videoStabilizationChanged {
|
||||||
|
self.configureVideoStabilization(configuration: config)
|
||||||
|
}
|
||||||
|
// 4. Update output orientation
|
||||||
|
if difference.orientationChanged {
|
||||||
|
self.configureOrientation(configuration: config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 2. Update outputs
|
|
||||||
if difference.outputsChanged {
|
|
||||||
try self.configureOutputs(configuration: config)
|
|
||||||
}
|
|
||||||
// 3. Update Video Stabilization
|
|
||||||
if difference.videoStabilizationChanged {
|
|
||||||
self.configureVideoStabilization(configuration: config)
|
|
||||||
}
|
|
||||||
// 4. Update output orientation
|
|
||||||
if difference.orientationChanged {
|
|
||||||
self.configureOrientation(configuration: config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let device = self.videoDeviceInput?.device else {
|
|
||||||
throw CameraError.device(.noDevice)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..)
|
// If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..)
|
||||||
if difference.isDeviceConfigurationDirty {
|
if difference.isDeviceConfigurationDirty {
|
||||||
try device.lockForConfiguration()
|
try self.withDeviceLock { device in
|
||||||
defer {
|
// 4. Configure format
|
||||||
device.unlockForConfiguration()
|
if difference.formatChanged {
|
||||||
|
try self.configureFormat(configuration: config, device: device)
|
||||||
|
}
|
||||||
|
// 5. After step 2. and 4., we also need to configure the PixelFormat.
|
||||||
|
// This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats.
|
||||||
|
if difference.outputsChanged || difference.formatChanged {
|
||||||
|
try self.configurePixelFormat(configuration: config)
|
||||||
|
}
|
||||||
|
// 6. Configure side-props (fps, lowLightBoost)
|
||||||
|
if difference.sidePropsChanged {
|
||||||
|
try self.configureSideProps(configuration: config, device: device)
|
||||||
|
}
|
||||||
|
// 7. Configure zoom
|
||||||
|
if difference.zoomChanged {
|
||||||
|
self.configureZoom(configuration: config, device: device)
|
||||||
|
}
|
||||||
|
// 8. Configure exposure bias
|
||||||
|
if difference.exposureChanged {
|
||||||
|
self.configureExposure(configuration: config, device: device)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Configure format
|
|
||||||
if difference.formatChanged {
|
|
||||||
try self.configureFormat(configuration: config, device: device)
|
|
||||||
}
|
|
||||||
// 5. After step 2. and 4., we also need to configure the PixelFormat.
|
|
||||||
// This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats.
|
|
||||||
if difference.outputsChanged || difference.formatChanged {
|
|
||||||
try self.configurePixelFormat(configuration: config)
|
|
||||||
}
|
|
||||||
// 6. Configure side-props (fps, lowLightBoost)
|
|
||||||
if difference.sidePropsChanged {
|
|
||||||
try self.configureSideProps(configuration: config, device: device)
|
|
||||||
}
|
|
||||||
// 7. Configure zoom
|
|
||||||
if difference.zoomChanged {
|
|
||||||
self.configureZoom(configuration: config, device: device)
|
|
||||||
}
|
|
||||||
// 8. Configure exposure bias
|
|
||||||
if difference.exposureChanged {
|
|
||||||
self.configureExposure(configuration: config, device: device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if difference.isSessionConfigurationDirty {
|
|
||||||
// We commit the session config updates AFTER the device config,
|
|
||||||
// that way we can also batch those changes into one update instead of doing two updates.
|
|
||||||
self.captureSession.commitConfiguration()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Start or stop the session if needed
|
// 9. Start or stop the session if needed
|
||||||
@ -183,11 +169,9 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
|
|
||||||
// 10. Enable or disable the Torch if needed (requires session to be running)
|
// 10. Enable or disable the Torch if needed (requires session to be running)
|
||||||
if difference.torchChanged {
|
if difference.torchChanged {
|
||||||
try device.lockForConfiguration()
|
try self.withDeviceLock { device in
|
||||||
defer {
|
try self.configureTorch(configuration: config, device: device)
|
||||||
device.unlockForConfiguration()
|
|
||||||
}
|
}
|
||||||
try self.configureTorch(configuration: config, device: device)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify about Camera initialization
|
// Notify about Camera initialization
|
||||||
@ -195,7 +179,6 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
self.delegate?.onSessionInitialized()
|
self.delegate?.onSessionInitialized()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.delegate?.onCameraConfigurationChanged(config, difference)
|
|
||||||
// After configuring, set this to the new configuration.
|
// After configuring, set this to the new configuration.
|
||||||
self.configuration = config
|
self.configuration = config
|
||||||
} catch {
|
} catch {
|
||||||
@ -223,6 +206,41 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Runs the given [lambda] under an AVCaptureSession configuration lock (`beginConfiguration()`)
|
||||||
|
*/
|
||||||
|
private func withSessionLock(_ lambda: () throws -> Void) throws {
|
||||||
|
// Lock Capture Session for configuration
|
||||||
|
ReactLogger.log(level: .info, message: "Beginning CameraSession configuration...")
|
||||||
|
captureSession.beginConfiguration()
|
||||||
|
defer {
|
||||||
|
// Unlock Capture Session again and submit configuration to Hardware
|
||||||
|
self.captureSession.commitConfiguration()
|
||||||
|
ReactLogger.log(level: .info, message: "Committed CameraSession configuration!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call lambda
|
||||||
|
try lambda()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Runs the given [lambda] under an AVCaptureDevice configuration lock (`lockForConfiguration()`)
|
||||||
|
*/
|
||||||
|
private func withDeviceLock(_ lambda: (_ device: AVCaptureDevice) throws -> Void) throws {
|
||||||
|
guard let device = videoDeviceInput?.device else {
|
||||||
|
throw CameraError.session(.cameraNotReady)
|
||||||
|
}
|
||||||
|
ReactLogger.log(level: .info, message: "Beginning CaptureDevice configuration...")
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
defer {
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
ReactLogger.log(level: .info, message: "Committed CaptureDevice configuration!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call lambda with Device
|
||||||
|
try lambda(device)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Starts or stops the CaptureSession if needed (`isActive`)
|
Starts or stops the CaptureSession if needed (`isActive`)
|
||||||
*/
|
*/
|
||||||
|
@ -21,8 +21,6 @@ protocol CameraSessionDelegate: AnyObject {
|
|||||||
Called when the [CameraSession] successfully initializes
|
Called when the [CameraSession] successfully initializes
|
||||||
*/
|
*/
|
||||||
func onSessionInitialized()
|
func onSessionInitialized()
|
||||||
|
|
||||||
func onCameraConfigurationChanged(_ configuration: CameraConfiguration?, _ difference: CameraConfiguration.Difference?)
|
|
||||||
/**
|
/**
|
||||||
Called when the [CameraSession] starts streaming frames. (isActive=true)
|
Called when the [CameraSession] starts streaming frames. (isActive=true)
|
||||||
*/
|
*/
|
||||||
@ -35,10 +33,6 @@ protocol CameraSessionDelegate: AnyObject {
|
|||||||
Called for every frame (if video or frameProcessor is enabled)
|
Called for every frame (if video or frameProcessor is enabled)
|
||||||
*/
|
*/
|
||||||
func onFrame(sampleBuffer: CMSampleBuffer)
|
func onFrame(sampleBuffer: CMSampleBuffer)
|
||||||
/**
|
|
||||||
Called whenever a new video chunk is available
|
|
||||||
*/
|
|
||||||
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk)
|
|
||||||
/**
|
/**
|
||||||
Called whenever a QR/Barcode has been scanned. Only if the CodeScanner Output is enabled
|
Called whenever a QR/Barcode has been scanned. Only if the CodeScanner Output is enabled
|
||||||
*/
|
*/
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
//
|
|
||||||
// ChunkedRecorder.swift
|
|
||||||
// VisionCamera
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 12/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
|
|
||||||
class ChunkedRecorder: NSObject {
|
|
||||||
|
|
||||||
enum ChunkType {
|
|
||||||
case initialization
|
|
||||||
case data(index: UInt64, duration: CMTime?)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Chunk {
|
|
||||||
let url: URL
|
|
||||||
let type: ChunkType
|
|
||||||
}
|
|
||||||
|
|
||||||
let outputURL: URL
|
|
||||||
let onChunkReady: ((Chunk) -> Void)
|
|
||||||
|
|
||||||
private var chunkIndex: UInt64 = 0
|
|
||||||
|
|
||||||
init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
|
|
||||||
self.outputURL = outputURL
|
|
||||||
self.onChunkReady = onChunkReady
|
|
||||||
guard FileManager.default.fileExists(atPath: outputURL.path) else {
|
|
||||||
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ChunkedRecorder: AVAssetWriterDelegate {
|
|
||||||
|
|
||||||
func assetWriter(_ writer: AVAssetWriter,
|
|
||||||
didOutputSegmentData segmentData: Data,
|
|
||||||
segmentType: AVAssetSegmentType,
|
|
||||||
segmentReport: AVAssetSegmentReport?) {
|
|
||||||
|
|
||||||
switch segmentType {
|
|
||||||
case .initialization:
|
|
||||||
saveInitSegment(segmentData)
|
|
||||||
case .separable:
|
|
||||||
saveSegment(segmentData, report: segmentReport)
|
|
||||||
@unknown default:
|
|
||||||
fatalError("Unknown AVAssetSegmentType!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveInitSegment(_ data: Data) {
|
|
||||||
let url = outputURL.appendingPathComponent("init.mp4")
|
|
||||||
save(data: data, url: url)
|
|
||||||
onChunkReady(url: url, type: .initialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) {
|
|
||||||
let name = "\(chunkIndex).mp4"
|
|
||||||
let url = outputURL.appendingPathComponent(name)
|
|
||||||
save(data: data, url: url)
|
|
||||||
let duration = report?
|
|
||||||
.trackReports
|
|
||||||
.filter { $0.mediaType == .video }
|
|
||||||
.first?
|
|
||||||
.duration
|
|
||||||
onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration))
|
|
||||||
chunkIndex += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save(data: Data, url: URL) {
|
|
||||||
do {
|
|
||||||
try data.write(to: url)
|
|
||||||
} catch {
|
|
||||||
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onChunkReady(url: URL, type: ChunkType) {
|
|
||||||
onChunkReady(Chunk(url: url, type: type))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -29,7 +29,6 @@ class RecordingSession {
|
|||||||
private let assetWriter: AVAssetWriter
|
private let assetWriter: AVAssetWriter
|
||||||
private var audioWriter: AVAssetWriterInput?
|
private var audioWriter: AVAssetWriterInput?
|
||||||
private var videoWriter: AVAssetWriterInput?
|
private var videoWriter: AVAssetWriterInput?
|
||||||
private let recorder: ChunkedRecorder
|
|
||||||
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
|
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
|
||||||
|
|
||||||
private var startTimestamp: CMTime?
|
private var startTimestamp: CMTime?
|
||||||
@ -49,8 +48,8 @@ class RecordingSession {
|
|||||||
/**
|
/**
|
||||||
Gets the file URL of the recorded video.
|
Gets the file URL of the recorded video.
|
||||||
*/
|
*/
|
||||||
var outputDiretory: URL {
|
var url: URL {
|
||||||
return recorder.outputURL
|
return assetWriter.outputURL
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,32 +70,14 @@ class RecordingSession {
|
|||||||
return (lastWrittenTimestamp - startTimestamp).seconds
|
return (lastWrittenTimestamp - startTimestamp).seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
init(outputDiretory: String,
|
init(url: URL,
|
||||||
fileType: AVFileType,
|
fileType: AVFileType,
|
||||||
onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void),
|
|
||||||
completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws {
|
completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws {
|
||||||
completionHandler = completion
|
completionHandler = completion
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let outputURL = URL(fileURLWithPath: outputDiretory)
|
assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType)
|
||||||
recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady)
|
|
||||||
assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!)
|
|
||||||
assetWriter.shouldOptimizeForNetworkUse = false
|
assetWriter.shouldOptimizeForNetworkUse = false
|
||||||
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
|
|
||||||
assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6, preferredTimescale: 1)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Apple HLS fMP4 does not have an Edit List Box ('elst') in an initialization segment to remove
|
|
||||||
audio priming duration which advanced audio formats like AAC have, since the sample tables
|
|
||||||
are empty. As a result, if the output PTS of the first non-fully trimmed audio sample buffer is
|
|
||||||
kCMTimeZero, the audio samples’ presentation time in segment files may be pushed forward by the
|
|
||||||
audio priming duration. This may cause audio and video to be out of sync. You should add a time
|
|
||||||
offset to all samples to avoid this situation.
|
|
||||||
*/
|
|
||||||
let startTimeOffset = CMTime(value: 10, timescale: 1)
|
|
||||||
assetWriter.initialSegmentStartTime = startTimeOffset
|
|
||||||
|
|
||||||
assetWriter.delegate = recorder
|
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
throw CameraError.capture(.createRecorderError(message: error.description))
|
throw CameraError.capture(.createRecorderError(message: error.description))
|
||||||
}
|
}
|
||||||
|
@ -32,36 +32,28 @@ extension AVCaptureOutput {
|
|||||||
func setOrientation(_ orientation: Orientation) {
|
func setOrientation(_ orientation: Orientation) {
|
||||||
// Set orientation for each connection
|
// Set orientation for each connection
|
||||||
for connection in connections {
|
for connection in connections {
|
||||||
connection.setOrientation(orientation)
|
#if swift(>=5.9)
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
// Camera Sensors are always in landscape rotation (90deg).
|
||||||
|
// We are setting the target rotation here, so we need to rotate by landscape once.
|
||||||
|
let cameraOrientation = orientation.rotateBy(orientation: .landscapeLeft)
|
||||||
|
let degrees = cameraOrientation.toDegrees()
|
||||||
|
|
||||||
|
// TODO: Don't rotate the video output because it adds overhead. Instead just use EXIF flags for the .mp4 file if recording.
|
||||||
|
// Does that work when we flip the camera?
|
||||||
|
if connection.isVideoRotationAngleSupported(degrees) {
|
||||||
|
connection.videoRotationAngle = degrees
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if connection.isVideoOrientationSupported {
|
||||||
|
connection.videoOrientation = orientation.toAVCaptureVideoOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if connection.isVideoOrientationSupported {
|
||||||
|
connection.videoOrientation = orientation.toAVCaptureVideoOrientation()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extension AVCaptureConnection {
|
|
||||||
func setOrientation(_ orientation: Orientation) {
|
|
||||||
#if swift(>=5.9)
|
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
// Camera Sensors are always in landscape rotation (90deg).
|
|
||||||
// We are setting the target rotation here, so we need to rotate by landscape once.
|
|
||||||
let cameraOrientation = orientation.rotateBy(orientation: .landscapeLeft)
|
|
||||||
let degrees = cameraOrientation.toDegrees()
|
|
||||||
|
|
||||||
// TODO: Don't rotate the video output because it adds overhead. Instead just use EXIF flags for the .mp4 file if recording.
|
|
||||||
// Does that work when we flip the camera?
|
|
||||||
if isVideoRotationAngleSupported(degrees) {
|
|
||||||
videoRotationAngle = degrees
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if isVideoOrientationSupported {
|
|
||||||
videoOrientation = orientation.toAVCaptureVideoOrientation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if isVideoOrientationSupported {
|
|
||||||
videoOrientation = orientation.toAVCaptureVideoOrientation()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -40,9 +40,6 @@ extension AVMetadataObject.ObjectType {
|
|||||||
case "upc-e":
|
case "upc-e":
|
||||||
self = .upce
|
self = .upce
|
||||||
return
|
return
|
||||||
case "upc-a":
|
|
||||||
self = .ean13
|
|
||||||
return
|
|
||||||
case "qr":
|
case "qr":
|
||||||
self = .qr
|
self = .qr
|
||||||
return
|
return
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// AppDelegate.swift
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 11/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@main
|
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
||||||
// Override point for customization after application launch.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: UISceneSession Lifecycle
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
||||||
// Called when a new scene session is being created.
|
|
||||||
// Use this method to select a configuration to create the new scene with.
|
|
||||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
||||||
}
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
|
||||||
// Called when the user discards a scene session.
|
|
||||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
|
||||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
||||||
<dependencies>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="EHf-IW-A2E">
|
|
||||||
<objects>
|
|
||||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="53" y="375"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
</document>
|
|
@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="tne-QT-ifu">
|
|
||||||
<objects>
|
|
||||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="TestRecorder" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VWP-nN-U6K">
|
|
||||||
<rect key="frame" x="157.33333333333334" y="722.66666666666663" width="78.333333333333343" height="34.333333333333371"/>
|
|
||||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
|
||||||
<state key="normal" title="Record"/>
|
|
||||||
<buttonConfiguration key="configuration" style="filled" title="Record"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleRecord:" destination="BYZ-38-t0r" eventType="touchUpInside" id="63a-uH-hTe"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="VWP-nN-U6K" secondAttribute="bottom" constant="61" id="0iW-h7-WDE"/>
|
|
||||||
<constraint firstItem="VWP-nN-U6K" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="yZb-ba-qfO"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<connections>
|
|
||||||
<outlet property="recordButton" destination="VWP-nN-U6K" id="gSk-uh-nDX"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="115" y="-27"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
<resources>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>UIApplicationSceneManifest</key>
|
|
||||||
<dict>
|
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
|
||||||
<false/>
|
|
||||||
<key>UISceneConfigurations</key>
|
|
||||||
<dict>
|
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>UISceneConfigurationName</key>
|
|
||||||
<string>Default Configuration</string>
|
|
||||||
<key>UISceneDelegateClassName</key>
|
|
||||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
|
||||||
<key>UISceneStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// ReactStubs.h
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 12/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
@interface UIView (React)
|
|
||||||
|
|
||||||
- (void)didSetProps:(NSArray<NSString *> *)changedProps;
|
|
||||||
|
|
||||||
@end
|
|
@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// ReactStubs.m
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 12/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ReactStubs.h"
|
|
||||||
|
|
||||||
@implementation UIView (React)
|
|
||||||
|
|
||||||
- (void)didSetProps:(__unused NSArray<NSString *> *)changedProps
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
@ -1,102 +0,0 @@
|
|||||||
//
|
|
||||||
// ReactStubs.swift
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 11/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
|
|
||||||
enum RCTLogLevel: String {
|
|
||||||
case trace
|
|
||||||
case info
|
|
||||||
case warning
|
|
||||||
case error
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RCTLogSource {
|
|
||||||
case native
|
|
||||||
}
|
|
||||||
|
|
||||||
func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) {
|
|
||||||
print(level.rawValue, "-", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias RCTDirectEventBlock = (Any?) -> Void
|
|
||||||
typealias RCTPromiseResolveBlock = (Any?) -> Void
|
|
||||||
typealias RCTPromiseRejectBlock = (String, String, NSError?) -> Void
|
|
||||||
typealias RCTResponseSenderBlock = (Any) -> Void
|
|
||||||
|
|
||||||
func NSNull() -> [String: String] {
|
|
||||||
return [:]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] {
|
|
||||||
var causeDictionary: [String: Any]?
|
|
||||||
if let cause = cause {
|
|
||||||
causeDictionary = [
|
|
||||||
"cause": "\(cause.domain): \(cause.code) \(cause.description)",
|
|
||||||
"userInfo": cause.userInfo
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
"error": "\(cameraError.code): \(cameraError.message)",
|
|
||||||
"extra": [
|
|
||||||
"code": cameraError.code,
|
|
||||||
"message": cameraError.message,
|
|
||||||
"cause": causeDictionary ?? NSNull(),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeReactError(_ cameraError: CameraError) -> [String: Any] {
|
|
||||||
return makeReactError(cameraError, cause: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RCTFPSGraph: UIView {
|
|
||||||
convenience init(frame: CGRect, color: UIColor) {
|
|
||||||
self.init(frame: frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
func onTick(_ tick: CFTimeInterval) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? {
|
|
||||||
let directory = NSTemporaryDirectory().appending("ReactNative")
|
|
||||||
let fm = FileManager.default
|
|
||||||
if fm.fileExists(atPath: directory) {
|
|
||||||
try! fm.removeItem(atPath: directory)
|
|
||||||
}
|
|
||||||
if !fm.fileExists(atPath: directory) {
|
|
||||||
try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
return directory
|
|
||||||
.appending("/").appending(UUID().uuidString)
|
|
||||||
.appending(".").appending(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RCTViewManager: NSObject {
|
|
||||||
|
|
||||||
var methodQueue: DispatchQueue! { nil }
|
|
||||||
class func requiresMainQueueSetup() -> Bool { false }
|
|
||||||
func view() -> UIView! { nil }
|
|
||||||
|
|
||||||
struct Bridge {
|
|
||||||
let uiManager = UIManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UIManager {
|
|
||||||
func view(forReactTag: NSNumber) -> UIView! {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bridge: Bridge = Bridge()
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// SceneDelegate.swift
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 11/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
||||||
|
|
||||||
var window: UIWindow?
|
|
||||||
|
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
||||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
|
||||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
|
||||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
|
||||||
guard let _ = (scene as? UIWindowScene) else { return }
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneDidDisconnect(_ scene: UIScene) {
|
|
||||||
// Called as the scene is being released by the system.
|
|
||||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
|
||||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
|
||||||
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
|
||||||
// Called when the scene has moved from an inactive state to an active state.
|
|
||||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
|
||||||
// Called when the scene will move from an active state to an inactive state.
|
|
||||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
|
||||||
// Called as the scene transitions from the background to the foreground.
|
|
||||||
// Use this method to undo the changes made on entering the background.
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
||||||
// Called as the scene transitions from the foreground to the background.
|
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
|
||||||
// to restore the scene back to its current state.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
//
|
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
#import "ReactStubs.h"
|
|
@ -1,131 +0,0 @@
|
|||||||
//
|
|
||||||
// ViewController.swift
|
|
||||||
// TestRecorder
|
|
||||||
//
|
|
||||||
// Created by Rafael Bastos on 11/07/2024.
|
|
||||||
// Copyright © 2024 mrousavy. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
class ViewController: UIViewController {
|
|
||||||
|
|
||||||
@IBOutlet weak var recordButton: UIButton!
|
|
||||||
|
|
||||||
let cameraView = CameraView()
|
|
||||||
let filePath: String = {
|
|
||||||
NSTemporaryDirectory() + "TestRecorder"
|
|
||||||
}()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
try? FileManager.default.removeItem(atPath: filePath)
|
|
||||||
|
|
||||||
cameraView.translatesAutoresizingMaskIntoConstraints = false;
|
|
||||||
view.insertSubview(cameraView, at: 0)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
cameraView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
cameraView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
cameraView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
recordButton.isHidden = true
|
|
||||||
cameraView.onInitialized = { _ in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.recordButton.isHidden = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cameraView.onInitReady = { json in
|
|
||||||
print("onInitReady:", json ?? "nil")
|
|
||||||
}
|
|
||||||
cameraView.onVideoChunkReady = { json in
|
|
||||||
print("onVideoChunkReady:", json ?? "nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
Task { @MainActor in
|
|
||||||
await requestAuthorizations()
|
|
||||||
|
|
||||||
cameraView.photo = true
|
|
||||||
cameraView.video = true
|
|
||||||
cameraView.audio = false
|
|
||||||
cameraView.isActive = true
|
|
||||||
cameraView.cameraId = getCameraDeviceId() as NSString?
|
|
||||||
cameraView.didSetProps([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAuthorized(for mediaType: AVMediaType) async -> Bool {
|
|
||||||
let status = AVCaptureDevice.authorizationStatus(for: mediaType)
|
|
||||||
var isAuthorized = status == .authorized
|
|
||||||
if status == .notDetermined {
|
|
||||||
isAuthorized = await AVCaptureDevice.requestAccess(for: mediaType)
|
|
||||||
}
|
|
||||||
return isAuthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func requestAuthorizations() async {
|
|
||||||
guard await isAuthorized(for: .video) else { return }
|
|
||||||
guard await isAuthorized(for: .audio) else { return }
|
|
||||||
// Set up the capture session.
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getCameraDeviceId() -> String? {
|
|
||||||
let deviceTypes: [AVCaptureDevice.DeviceType] = [
|
|
||||||
.builtInWideAngleCamera
|
|
||||||
]
|
|
||||||
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: .video, position: .back)
|
|
||||||
|
|
||||||
let device = discoverySession.devices.first
|
|
||||||
|
|
||||||
return device?.uniqueID
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction
|
|
||||||
func toggleRecord(_ button: UIButton) {
|
|
||||||
if button.title(for: .normal) == "Stop" {
|
|
||||||
|
|
||||||
cameraView.stopRecording(promise: Promise(
|
|
||||||
resolver: { result in
|
|
||||||
print("result")
|
|
||||||
}, rejecter: { code, message, cause in
|
|
||||||
print("error")
|
|
||||||
}))
|
|
||||||
|
|
||||||
button.setTitle("Record", for: .normal)
|
|
||||||
button.configuration = .filled()
|
|
||||||
|
|
||||||
} else {
|
|
||||||
cameraView.startRecording(
|
|
||||||
options: [
|
|
||||||
"fileType": "mp4",
|
|
||||||
"videoCodec": "h265",
|
|
||||||
],
|
|
||||||
filePath: filePath) { callback in
|
|
||||||
print("callback", callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setTitle("Stop", for: .normal)
|
|
||||||
button.configuration = .bordered()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {
|
|
||||||
switch UIDevice.current.orientation {
|
|
||||||
case .landscapeLeft:
|
|
||||||
cameraView.orientation = "landscape-right"
|
|
||||||
case .landscapeRight:
|
|
||||||
cameraView.orientation = "landscape-left"
|
|
||||||
default:
|
|
||||||
cameraView.orientation = "portrait"
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraView.didSetProps([])
|
|
||||||
super.viewWillTransition(to: size, with: coordinator)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,7 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct RecordVideoOptions {
|
struct RecordVideoOptions {
|
||||||
var fileType: AVFileType = .mp4
|
var fileType: AVFileType = .mov
|
||||||
var flash: Torch = .off
|
var flash: Torch = .off
|
||||||
var codec: AVVideoCodecType?
|
var codec: AVVideoCodecType?
|
||||||
/**
|
/**
|
||||||
|
@ -7,79 +7,6 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518125E0102000DB86D6 /* CameraViewManager.swift */; };
|
|
||||||
B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */; };
|
|
||||||
B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */; };
|
|
||||||
B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E852C410FB700CC198C /* ReactStubs.m */; };
|
|
||||||
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
|
||||||
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
|
||||||
B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */; };
|
|
||||||
B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */; };
|
|
||||||
B3EF9F112C3FBD8300832EE7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F102C3FBD8300832EE7 /* ViewController.swift */; };
|
|
||||||
B3EF9F142C3FBD8300832EE7 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F132C3FBD8300832EE7 /* Base */; };
|
|
||||||
B3EF9F162C3FBD8400832EE7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */; };
|
|
||||||
B3EF9F192C3FBD8400832EE7 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F182C3FBD8400832EE7 /* Base */; };
|
|
||||||
B3EF9F1E2C3FBDCF00832EE7 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518425E0102000DB86D6 /* CameraView.swift */; };
|
|
||||||
B3EF9F1F2C3FBDDC00832EE7 /* ReactLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516F25E0102000DB86D6 /* ReactLogger.swift */; };
|
|
||||||
B3EF9F212C3FBDFC00832EE7 /* ReactStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */; };
|
|
||||||
B3EF9F222C3FBE8200832EE7 /* CameraConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E62AD698DF00E93869 /* CameraConfiguration.swift */; };
|
|
||||||
B3EF9F232C3FBE8B00832EE7 /* VideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882332AD969E000317161 /* VideoStabilizationMode.swift */; };
|
|
||||||
B3EF9F242C3FBEBC00832EE7 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518325E0102000DB86D6 /* CameraError.swift */; };
|
|
||||||
B3EF9F252C3FBED900832EE7 /* Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DE2AD6FB230087F063 /* Orientation.swift */; };
|
|
||||||
B3EF9F262C3FBEEA00832EE7 /* CameraDeviceFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882312AD966FC00317161 /* CameraDeviceFormat.swift */; };
|
|
||||||
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87B11BE2A8E63B700732EBF /* PixelFormat.swift */; };
|
|
||||||
B3EF9F282C3FBF1900832EE7 /* JSUnionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882372AD96B4400317161 /* JSUnionValue.swift */; };
|
|
||||||
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E02AD7046E0087F063 /* Torch.swift */; };
|
|
||||||
B3EF9F2A2C3FBF3400832EE7 /* CodeScannerOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AD2ACC9731009D612F /* CodeScannerOptions.swift */; };
|
|
||||||
B3EF9F2B2C3FBF4100832EE7 /* AVMetadataObject.ObjectType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */; };
|
|
||||||
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517325E0102000DB86D6 /* EnumParserError.swift */; };
|
|
||||||
B3EF9F2D2C3FBF9600832EE7 /* CameraSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */; };
|
|
||||||
B3EF9F2E2C3FBFA600832EE7 /* CameraSession+CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EC2AD6A5E600E93869 /* CameraSession+CodeScanner.swift */; };
|
|
||||||
B3EF9F2F2C3FBFB200832EE7 /* CameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E42AD68D9300E93869 /* CameraSession.swift */; };
|
|
||||||
B3EF9F302C3FBFBB00832EE7 /* RecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */; };
|
|
||||||
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC7263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift */; };
|
|
||||||
B3EF9F322C3FBFF100832EE7 /* CameraQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84760DE2608F57D004C3180 /* CameraQueues.swift */; };
|
|
||||||
B3EF9F332C3FC00900832EE7 /* CameraSession+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC72AD8005400169C0D /* CameraSession+Configuration.swift */; };
|
|
||||||
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
|
|
||||||
B3EF9F372C3FC0CA00832EE7 /* CameraView+Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518225E0102000DB86D6 /* CameraView+Zoom.swift */; };
|
|
||||||
B3EF9F382C3FC0D900832EE7 /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83D5EE629377117000AFD2F /* PreviewView.swift */; };
|
|
||||||
B3EF9F3A2C3FC2EB00832EE7 /* AutoFocusSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882352AD96AFF00317161 /* AutoFocusSystem.swift */; };
|
|
||||||
B3EF9F3C2C3FC30D00832EE7 /* AVCaptureDevice.Position+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517C25E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift */; };
|
|
||||||
B3EF9F4A2C3FC31E00832EE7 /* AVFrameRateRange+includes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */; };
|
|
||||||
B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */; };
|
|
||||||
B3EF9F4C2C3FC31E00832EE7 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
|
|
||||||
B3EF9F4D2C3FC31E00832EE7 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */; };
|
|
||||||
B3EF9F4E2C3FC31E00832EE7 /* AVCaptureOutput+mirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */; };
|
|
||||||
B3EF9F4F2C3FC31E00832EE7 /* Collection+safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516225E0102000DB86D6 /* Collection+safe.swift */; };
|
|
||||||
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */; };
|
|
||||||
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88977BD2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift */; };
|
|
||||||
B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */; };
|
|
||||||
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */; };
|
|
||||||
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */; };
|
|
||||||
B3EF9F552C3FC31E00832EE7 /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */; };
|
|
||||||
B3EF9F562C3FC31E00832EE7 /* AVCaptureSession+synchronizeBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */; };
|
|
||||||
B3EF9F572C3FC31E00832EE7 /* AVCaptureDevice+isMultiCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */; };
|
|
||||||
B3EF9F582C3FC31E00832EE7 /* AVCaptureDevice+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */; };
|
|
||||||
B3EF9F592C3FC31E00832EE7 /* AVCaptureDevice.Format+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */; };
|
|
||||||
B3EF9F5A2C3FC31E00832EE7 /* CMVideoDimensions+toCGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */; };
|
|
||||||
B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517A25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift */; };
|
|
||||||
B3EF9F5C2C3FC33E00832EE7 /* RecordVideoOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC92AD8034E00169C0D /* RecordVideoOptions.swift */; };
|
|
||||||
B3EF9F5D2C3FC34600832EE7 /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AECB2AD803B200169C0D /* Video.swift */; };
|
|
||||||
B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517925E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift */; };
|
|
||||||
B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517B25E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift */; };
|
|
||||||
B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */; };
|
|
||||||
B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */; };
|
|
||||||
B3EF9F622C3FC43000832EE7 /* AVVideoCodecType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517525E0102000DB86D6 /* AVVideoCodecType+descriptor.swift */; };
|
|
||||||
B3EF9F632C3FC43000832EE7 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517725E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift */; };
|
|
||||||
B3EF9F642C3FC43000832EE7 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */; };
|
|
||||||
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DA2AD6F0A00087F063 /* CameraSession+Audio.swift */; };
|
|
||||||
B3EF9F662C3FC44B00832EE7 /* CameraSession+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E82AD6A5D600E93869 /* CameraSession+Video.swift */; };
|
|
||||||
B3EF9F672C3FC44B00832EE7 /* CameraSession+Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EA2AD6A5DE00E93869 /* CameraSession+Photo.swift */; };
|
|
||||||
B3EF9F682C3FC44B00832EE7 /* CameraSession+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DC2AD6F62C0087F063 /* CameraSession+Focus.swift */; };
|
|
||||||
B3EF9F692C3FC44B00832EE7 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; };
|
|
||||||
B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517025E0102000DB86D6 /* Promise.swift */; };
|
|
||||||
B3EF9F6B2C3FD35600832EE7 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */; };
|
|
||||||
B3EF9F6C2C3FD36800832EE7 /* Callback.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD3BA1266E22D2006C80A2 /* Callback.swift */; };
|
|
||||||
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
|
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
|
||||||
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
|
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
|
||||||
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
|
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
|
||||||
@ -167,19 +94,6 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
B3AF8E832C410FB600CC198C /* TestRecorder-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestRecorder-Bridging-Header.h"; sourceTree = "<group>"; };
|
|
||||||
B3AF8E842C410FB700CC198C /* ReactStubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactStubs.h; sourceTree = "<group>"; };
|
|
||||||
B3AF8E852C410FB700CC198C /* ReactStubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactStubs.m; sourceTree = "<group>"; };
|
|
||||||
B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkedRecorder.swift; sourceTree = "<group>"; };
|
|
||||||
B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestRecorder.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
B3EF9F102C3FBD8300832EE7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
|
||||||
B3EF9F132C3FBD8300832EE7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
|
||||||
B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
||||||
B3EF9F182C3FBD8400832EE7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
|
||||||
B3EF9F1A2C3FBD8400832EE7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactStubs.swift; sourceTree = "<group>"; };
|
|
||||||
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeMode.swift; sourceTree = "<group>"; };
|
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeMode.swift; sourceTree = "<group>"; };
|
||||||
B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorPluginHostObject.mm; sourceTree = "<group>"; };
|
B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorPluginHostObject.mm; sourceTree = "<group>"; };
|
||||||
B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginHostObject.h; sourceTree = "<group>"; };
|
B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginHostObject.h; sourceTree = "<group>"; };
|
||||||
@ -277,13 +191,6 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
B3EF9F072C3FBD8300832EE7 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@ -314,30 +221,10 @@
|
|||||||
B887516125E0102000DB86D6 /* Extensions */,
|
B887516125E0102000DB86D6 /* Extensions */,
|
||||||
B887517225E0102000DB86D6 /* Parsers */,
|
B887517225E0102000DB86D6 /* Parsers */,
|
||||||
B887516D25E0102000DB86D6 /* React Utils */,
|
B887516D25E0102000DB86D6 /* React Utils */,
|
||||||
B3EF9F0B2C3FBD8300832EE7 /* TestRecorder */,
|
|
||||||
134814211AA4EA7D00B7C361 /* Products */,
|
134814211AA4EA7D00B7C361 /* Products */,
|
||||||
B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */,
|
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
B3EF9F0B2C3FBD8300832EE7 /* TestRecorder */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */,
|
|
||||||
B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */,
|
|
||||||
B3EF9F102C3FBD8300832EE7 /* ViewController.swift */,
|
|
||||||
B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */,
|
|
||||||
B3AF8E842C410FB700CC198C /* ReactStubs.h */,
|
|
||||||
B3AF8E852C410FB700CC198C /* ReactStubs.m */,
|
|
||||||
B3AF8E832C410FB600CC198C /* TestRecorder-Bridging-Header.h */,
|
|
||||||
B3EF9F122C3FBD8300832EE7 /* Main.storyboard */,
|
|
||||||
B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */,
|
|
||||||
B3EF9F172C3FBD8400832EE7 /* LaunchScreen.storyboard */,
|
|
||||||
B3EF9F1A2C3FBD8400832EE7 /* Info.plist */,
|
|
||||||
);
|
|
||||||
path = TestRecorder;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B80175EA2ABDEBBB00E7DE90 /* Types */ = {
|
B80175EA2ABDEBBB00E7DE90 /* Types */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -370,7 +257,6 @@
|
|||||||
B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */,
|
B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */,
|
||||||
B83D5EE629377117000AFD2F /* PreviewView.swift */,
|
B83D5EE629377117000AFD2F /* PreviewView.swift */,
|
||||||
B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */,
|
B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */,
|
||||||
B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */,
|
|
||||||
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */,
|
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */,
|
||||||
B84760DE2608F57D004C3180 /* CameraQueues.swift */,
|
B84760DE2608F57D004C3180 /* CameraQueues.swift */,
|
||||||
B887518325E0102000DB86D6 /* CameraError.swift */,
|
B887518325E0102000DB86D6 /* CameraError.swift */,
|
||||||
@ -480,42 +366,18 @@
|
|||||||
productReference = 134814201AA4EA6300B7C361 /* libVisionCamera.a */;
|
productReference = 134814201AA4EA6300B7C361 /* libVisionCamera.a */;
|
||||||
productType = "com.apple.product-type.library.static";
|
productType = "com.apple.product-type.library.static";
|
||||||
};
|
};
|
||||||
B3EF9F092C3FBD8300832EE7 /* TestRecorder */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = B3EF9F1D2C3FBD8400832EE7 /* Build configuration list for PBXNativeTarget "TestRecorder" */;
|
|
||||||
buildPhases = (
|
|
||||||
B3EF9F062C3FBD8300832EE7 /* Sources */,
|
|
||||||
B3EF9F072C3FBD8300832EE7 /* Frameworks */,
|
|
||||||
B3EF9F082C3FBD8300832EE7 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = TestRecorder;
|
|
||||||
productName = TestRecorder;
|
|
||||||
productReference = B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
58B511D31A9E6C8500147676 /* Project object */ = {
|
58B511D31A9E6C8500147676 /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1540;
|
|
||||||
LastUpgradeCheck = 1240;
|
LastUpgradeCheck = 1240;
|
||||||
ORGANIZATIONNAME = mrousavy;
|
ORGANIZATIONNAME = mrousavy;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
58B511DA1A9E6C8500147676 = {
|
58B511DA1A9E6C8500147676 = {
|
||||||
CreatedOnToolsVersion = 6.1.1;
|
CreatedOnToolsVersion = 6.1.1;
|
||||||
};
|
};
|
||||||
B3EF9F092C3FBD8300832EE7 = {
|
|
||||||
CreatedOnToolsVersion = 15.4;
|
|
||||||
DevelopmentTeam = HP3AMBWJGS;
|
|
||||||
LastSwiftMigration = 1540;
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "VisionCamera" */;
|
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "VisionCamera" */;
|
||||||
@ -525,7 +387,6 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
English,
|
English,
|
||||||
en,
|
en,
|
||||||
Base,
|
|
||||||
);
|
);
|
||||||
mainGroup = 58B511D21A9E6C8500147676;
|
mainGroup = 58B511D21A9E6C8500147676;
|
||||||
productRefGroup = 58B511D21A9E6C8500147676;
|
productRefGroup = 58B511D21A9E6C8500147676;
|
||||||
@ -533,24 +394,10 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
58B511DA1A9E6C8500147676 /* VisionCamera */,
|
58B511DA1A9E6C8500147676 /* VisionCamera */,
|
||||||
B3EF9F092C3FBD8300832EE7 /* TestRecorder */,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
B3EF9F082C3FBD8300832EE7 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
B3EF9F162C3FBD8400832EE7 /* Assets.xcassets in Resources */,
|
|
||||||
B3EF9F192C3FBD8400832EE7 /* Base in Resources */,
|
|
||||||
B3EF9F142C3FBD8300832EE7 /* Base in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */ = {
|
B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
@ -643,7 +490,6 @@
|
|||||||
B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */,
|
B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */,
|
||||||
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
|
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
|
||||||
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
||||||
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */,
|
|
||||||
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
|
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
|
||||||
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
|
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
|
||||||
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
||||||
@ -670,103 +516,8 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
B3EF9F062C3FBD8300832EE7 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
B3EF9F372C3FC0CA00832EE7 /* CameraView+Zoom.swift in Sources */,
|
|
||||||
B3EF9F232C3FBE8B00832EE7 /* VideoStabilizationMode.swift in Sources */,
|
|
||||||
B3EF9F4A2C3FC31E00832EE7 /* AVFrameRateRange+includes.swift in Sources */,
|
|
||||||
B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */,
|
|
||||||
B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
|
||||||
B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */,
|
|
||||||
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */,
|
|
||||||
B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */,
|
|
||||||
B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */,
|
|
||||||
B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */,
|
|
||||||
B3EF9F622C3FC43000832EE7 /* AVVideoCodecType+descriptor.swift in Sources */,
|
|
||||||
B3EF9F632C3FC43000832EE7 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */,
|
|
||||||
B3EF9F642C3FC43000832EE7 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */,
|
|
||||||
B3EF9F4C2C3FC31E00832EE7 /* AVAudioSession+updateCategory.swift in Sources */,
|
|
||||||
B3EF9F4D2C3FC31E00832EE7 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */,
|
|
||||||
B3EF9F4E2C3FC31E00832EE7 /* AVCaptureOutput+mirror.swift in Sources */,
|
|
||||||
B3EF9F4F2C3FC31E00832EE7 /* Collection+safe.swift in Sources */,
|
|
||||||
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */,
|
|
||||||
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */,
|
|
||||||
B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
|
||||||
B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */,
|
|
||||||
B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */,
|
|
||||||
B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */,
|
|
||||||
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */,
|
|
||||||
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */,
|
|
||||||
B3EF9F552C3FC31E00832EE7 /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */,
|
|
||||||
B3EF9F562C3FC31E00832EE7 /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
|
|
||||||
B3EF9F572C3FC31E00832EE7 /* AVCaptureDevice+isMultiCam.swift in Sources */,
|
|
||||||
B3EF9F582C3FC31E00832EE7 /* AVCaptureDevice+toDictionary.swift in Sources */,
|
|
||||||
B3EF9F592C3FC31E00832EE7 /* AVCaptureDevice.Format+toDictionary.swift in Sources */,
|
|
||||||
B3EF9F5A2C3FC31E00832EE7 /* CMVideoDimensions+toCGSize.swift in Sources */,
|
|
||||||
B3EF9F212C3FBDFC00832EE7 /* ReactStubs.swift in Sources */,
|
|
||||||
B3EF9F5C2C3FC33E00832EE7 /* RecordVideoOptions.swift in Sources */,
|
|
||||||
B3EF9F6B2C3FD35600832EE7 /* CameraView+RecordVideo.swift in Sources */,
|
|
||||||
B3EF9F222C3FBE8200832EE7 /* CameraConfiguration.swift in Sources */,
|
|
||||||
B3EF9F282C3FBF1900832EE7 /* JSUnionValue.swift in Sources */,
|
|
||||||
B3EF9F332C3FC00900832EE7 /* CameraSession+Configuration.swift in Sources */,
|
|
||||||
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */,
|
|
||||||
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */,
|
|
||||||
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */,
|
|
||||||
B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */,
|
|
||||||
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */,
|
|
||||||
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */,
|
|
||||||
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */,
|
|
||||||
B3EF9F382C3FC0D900832EE7 /* PreviewView.swift in Sources */,
|
|
||||||
B3EF9F3A2C3FC2EB00832EE7 /* AutoFocusSystem.swift in Sources */,
|
|
||||||
B3EF9F112C3FBD8300832EE7 /* ViewController.swift in Sources */,
|
|
||||||
B3EF9F5D2C3FC34600832EE7 /* Video.swift in Sources */,
|
|
||||||
B3EF9F2B2C3FBF4100832EE7 /* AVMetadataObject.ObjectType+descriptor.swift in Sources */,
|
|
||||||
B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */,
|
|
||||||
B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */,
|
|
||||||
B3EF9F2D2C3FBF9600832EE7 /* CameraSessionDelegate.swift in Sources */,
|
|
||||||
B3EF9F262C3FBEEA00832EE7 /* CameraDeviceFormat.swift in Sources */,
|
|
||||||
B3EF9F242C3FBEBC00832EE7 /* CameraError.swift in Sources */,
|
|
||||||
B3EF9F2E2C3FBFA600832EE7 /* CameraSession+CodeScanner.swift in Sources */,
|
|
||||||
B3EF9F252C3FBED900832EE7 /* Orientation.swift in Sources */,
|
|
||||||
B3EF9F662C3FC44B00832EE7 /* CameraSession+Video.swift in Sources */,
|
|
||||||
B3EF9F672C3FC44B00832EE7 /* CameraSession+Photo.swift in Sources */,
|
|
||||||
B3EF9F682C3FC44B00832EE7 /* CameraSession+Focus.swift in Sources */,
|
|
||||||
B3EF9F6C2C3FD36800832EE7 /* Callback.swift in Sources */,
|
|
||||||
B3EF9F692C3FC44B00832EE7 /* PhotoCaptureDelegate.swift in Sources */,
|
|
||||||
B3EF9F302C3FBFBB00832EE7 /* RecordingSession.swift in Sources */,
|
|
||||||
B3EF9F322C3FBFF100832EE7 /* CameraQueues.swift in Sources */,
|
|
||||||
B3EF9F2F2C3FBFB200832EE7 /* CameraSession.swift in Sources */,
|
|
||||||
B3EF9F2A2C3FBF3400832EE7 /* CodeScannerOptions.swift in Sources */,
|
|
||||||
B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */,
|
|
||||||
B3EF9F1E2C3FBDCF00832EE7 /* CameraView.swift in Sources */,
|
|
||||||
B3EF9F3C2C3FC30D00832EE7 /* AVCaptureDevice.Position+descriptor.swift in Sources */,
|
|
||||||
B3EF9F1F2C3FBDDC00832EE7 /* ReactLogger.swift in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
|
||||||
B3EF9F122C3FBD8300832EE7 /* Main.storyboard */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
B3EF9F132C3FBD8300832EE7 /* Base */,
|
|
||||||
);
|
|
||||||
name = Main.storyboard;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B3EF9F172C3FBD8400832EE7 /* LaunchScreen.storyboard */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
B3EF9F182C3FBD8400832EE7 /* Base */,
|
|
||||||
);
|
|
||||||
name = LaunchScreen.storyboard;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXVariantGroup section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
58B511ED1A9E6C8500147676 /* Debug */ = {
|
58B511ED1A9E6C8500147676 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@ -909,94 +660,6 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
B3EF9F1B2C3FBD8400832EE7 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
DEVELOPMENT_TEAM = HP3AMBWJGS;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_FILE = TestRecorder/Info.plist;
|
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "Record form camera";
|
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Record from microphone";
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = camera.TestRecorder;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "TestRecorder/TestRecorder-Bridging-Header.h";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
B3EF9F1C2C3FBD8400832EE7 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
DEVELOPMENT_TEAM = HP3AMBWJGS;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_FILE = TestRecorder/Info.plist;
|
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "Record form camera";
|
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Record from microphone";
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = camera.TestRecorder;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "TestRecorder/TestRecorder-Bridging-Header.h";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -1018,15 +681,6 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
B3EF9F1D2C3FBD8400832EE7 /* Build configuration list for PBXNativeTarget "TestRecorder" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
B3EF9F1B2C3FBD8400832EE7 /* Debug */,
|
|
||||||
B3EF9F1C2C3FBD8400832EE7 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = 58B511D31A9E6C8500147676 /* Project object */;
|
rootObject = 58B511D31A9E6C8500147676 /* Project object */;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-native-vision-camera",
|
"name": "react-native-vision-camera",
|
||||||
"version": "3.9.2",
|
"version": "3.8.2",
|
||||||
"description": "A powerful, high-performance React Native Camera library.",
|
"description": "A powerful, high-performance React Native Camera library.",
|
||||||
"main": "lib/commonjs/index",
|
"main": "lib/commonjs/index",
|
||||||
"module": "lib/module/index",
|
"module": "lib/module/index",
|
||||||
@ -49,33 +49,26 @@
|
|||||||
"postpack": "rm ./README.md"
|
"postpack": "rm ./README.md"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
|
||||||
"native",
|
|
||||||
"camera",
|
|
||||||
"react-native",
|
"react-native",
|
||||||
"react-native-camera",
|
|
||||||
"vision",
|
|
||||||
"javascript",
|
|
||||||
"typescript",
|
|
||||||
"android",
|
|
||||||
"ios",
|
"ios",
|
||||||
"library",
|
"android",
|
||||||
"instagram",
|
"camera",
|
||||||
"snapchat",
|
"vision",
|
||||||
"ai",
|
"native",
|
||||||
"scanner",
|
|
||||||
"qrcode",
|
|
||||||
"barcode",
|
|
||||||
"qr-code",
|
|
||||||
"jsi",
|
|
||||||
"worklet",
|
|
||||||
"module",
|
"module",
|
||||||
|
"react",
|
||||||
|
"ai",
|
||||||
|
"ar",
|
||||||
|
"qr",
|
||||||
|
"qr-code",
|
||||||
|
"barcode",
|
||||||
|
"scanner",
|
||||||
"frame",
|
"frame",
|
||||||
"processing",
|
"processing",
|
||||||
"realtime"
|
"realtime"
|
||||||
],
|
],
|
||||||
"repository": "https://github.com/mrousavy/react-native-vision-camera",
|
"repository": "https://github.com/mrousavy/react-native-vision-camera",
|
||||||
"author": "Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)",
|
"author": "Marc Rousavy <marcrousavy@hotmail.com> (https://github.com/mrousavy)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mrousavy/react-native-vision-camera/issues"
|
"url": "https://github.com/mrousavy/react-native-vision-camera/issues"
|
||||||
@ -166,6 +159,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,5 @@ if which clang-format >/dev/null; then
|
|||||||
clang-format -style=file:./cpp/.clang-format -i "$file"
|
clang-format -style=file:./cpp/.clang-format -i "$file"
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo "error: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)"
|
echo "warning: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
@ -3,6 +3,5 @@
|
|||||||
if which ktlint >/dev/null; then
|
if which ktlint >/dev/null; then
|
||||||
cd android && ktlint --color --relative --editorconfig=./.editorconfig -F ./**/*.kt*
|
cd android && ktlint --color --relative --editorconfig=./.editorconfig -F ./**/*.kt*
|
||||||
else
|
else
|
||||||
echo "error: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)"
|
echo "warning: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
@ -3,6 +3,5 @@
|
|||||||
if which swiftformat >/dev/null; then
|
if which swiftformat >/dev/null; then
|
||||||
cd ios && swiftformat --quiet .
|
cd ios && swiftformat --quiet .
|
||||||
else
|
else
|
||||||
echo "error: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)"
|
echo "warning: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
@ -3,6 +3,5 @@
|
|||||||
if which swiftlint >/dev/null; then
|
if which swiftlint >/dev/null; then
|
||||||
cd ios && swiftlint --quiet --fix && swiftlint --quiet
|
cd ios && swiftlint --quiet --fix && swiftlint --quiet
|
||||||
else
|
else
|
||||||
echo "error: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)"
|
echo "warning: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
@ -18,41 +18,37 @@ export type CameraPermissionStatus = 'granted' | 'not-determined' | 'denied' | '
|
|||||||
export type CameraPermissionRequestResult = 'granted' | 'denied'
|
export type CameraPermissionRequestResult = 'granted' | 'denied'
|
||||||
|
|
||||||
interface OnCodeScannedEvent {
|
interface OnCodeScannedEvent {
|
||||||
codes: Code[]
|
codes: Code[]
|
||||||
frame: CodeScannerFrame
|
frame: CodeScannerFrame
|
||||||
}
|
}
|
||||||
interface OnErrorEvent {
|
interface OnErrorEvent {
|
||||||
code: string
|
code: string
|
||||||
message: string
|
message: string
|
||||||
cause?: ErrorWithCause
|
cause?: ErrorWithCause
|
||||||
}
|
|
||||||
interface OnInitReadyEvent {
|
|
||||||
filepath: string
|
|
||||||
}
|
}
|
||||||
interface OnVideoChunkReadyEvent {
|
interface OnVideoChunkReadyEvent {
|
||||||
filepath: string
|
filepath: string
|
||||||
index: number
|
index: int
|
||||||
}
|
}
|
||||||
type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onError' | 'frameProcessor' | 'codeScanner'> & {
|
type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onError' | 'frameProcessor' | 'codeScanner'> & {
|
||||||
cameraId: string
|
cameraId: string
|
||||||
enableFrameProcessor: boolean
|
enableFrameProcessor: boolean
|
||||||
codeScannerOptions?: Omit<CodeScanner, 'onCodeScanned'>
|
codeScannerOptions?: Omit<CodeScanner, 'onCodeScanned'>
|
||||||
onInitialized?: (event: NativeSyntheticEvent<void>) => void
|
onInitialized?: (event: NativeSyntheticEvent<void>) => void
|
||||||
onError?: (event: NativeSyntheticEvent<OnErrorEvent>) => void
|
onError?: (event: NativeSyntheticEvent<OnErrorEvent>) => void
|
||||||
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
|
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
|
||||||
onStarted?: (event: NativeSyntheticEvent<void>) => void
|
onStarted?: (event: NativeSyntheticEvent<void>) => void
|
||||||
onStopped?: (event: NativeSyntheticEvent<void>) => void
|
onStopped?: (event: NativeSyntheticEvent<void>) => void
|
||||||
onInitReady?: (event: NativeSyntheticEvent<OnInitReadyEvent>) => void
|
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
|
||||||
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
|
onViewReady: () => void
|
||||||
onViewReady: () => void
|
|
||||||
}
|
}
|
||||||
type NativeRecordVideoOptions = Omit<RecordVideoOptions, 'onRecordingError' | 'onRecordingFinished' | 'videoBitRate'> & {
|
type NativeRecordVideoOptions = Omit<RecordVideoOptions, 'onRecordingError' | 'onRecordingFinished' | 'videoBitRate'> & {
|
||||||
videoBitRateOverride?: number
|
videoBitRateOverride?: number
|
||||||
videoBitRateMultiplier?: number
|
videoBitRateMultiplier?: number
|
||||||
}
|
}
|
||||||
type RefType = React.Component<NativeCameraViewProps> & Readonly<NativeMethods>
|
type RefType = React.Component<NativeCameraViewProps> & Readonly<NativeMethods>
|
||||||
interface CameraState {
|
interface CameraState {
|
||||||
isRecordingWithFlash: boolean
|
isRecordingWithFlash: boolean
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -86,444 +82,427 @@ interface CameraState {
|
|||||||
* @component
|
* @component
|
||||||
*/
|
*/
|
||||||
export class Camera extends React.PureComponent<CameraProps, CameraState> {
|
export class Camera extends React.PureComponent<CameraProps, CameraState> {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
static displayName = 'Camera'
|
static displayName = 'Camera'
|
||||||
/** @internal */
|
/** @internal */
|
||||||
displayName = Camera.displayName
|
displayName = Camera.displayName
|
||||||
private lastFrameProcessor: FrameProcessor | undefined
|
private lastFrameProcessor: FrameProcessor | undefined
|
||||||
private isNativeViewMounted = false
|
private isNativeViewMounted = false
|
||||||
|
|
||||||
private readonly ref: React.RefObject<RefType>
|
private readonly ref: React.RefObject<RefType>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
constructor(props: CameraProps) {
|
constructor(props: CameraProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.onViewReady = this.onViewReady.bind(this)
|
this.onViewReady = this.onViewReady.bind(this)
|
||||||
this.onInitialized = this.onInitialized.bind(this)
|
this.onInitialized = this.onInitialized.bind(this)
|
||||||
this.onStarted = this.onStarted.bind(this)
|
this.onStarted = this.onStarted.bind(this)
|
||||||
this.onStopped = this.onStopped.bind(this)
|
this.onStopped = this.onStopped.bind(this)
|
||||||
this.onError = this.onError.bind(this)
|
this.onError = this.onError.bind(this)
|
||||||
this.onCodeScanned = this.onCodeScanned.bind(this)
|
this.onCodeScanned = this.onCodeScanned.bind(this)
|
||||||
this.ref = React.createRef<RefType>()
|
this.ref = React.createRef<RefType>()
|
||||||
this.lastFrameProcessor = undefined
|
this.lastFrameProcessor = undefined
|
||||||
this.state = {
|
this.state = {
|
||||||
isRecordingWithFlash: false,
|
isRecordingWithFlash: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get handle(): number {
|
private get handle(): number {
|
||||||
const nodeHandle = findNodeHandle(this.ref.current)
|
const nodeHandle = findNodeHandle(this.ref.current)
|
||||||
if (nodeHandle == null || nodeHandle === -1) {
|
if (nodeHandle == null || nodeHandle === -1) {
|
||||||
throw new CameraRuntimeError(
|
throw new CameraRuntimeError(
|
||||||
'system/view-not-found',
|
'system/view-not-found',
|
||||||
"Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?",
|
"Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodeHandle
|
return nodeHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region View-specific functions (UIViewManager)
|
//#region View-specific functions (UIViewManager)
|
||||||
/**
|
/**
|
||||||
* Take a single photo and write it's content to a temporary file.
|
* Take a single photo and write it's content to a temporary file.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
* @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* const photo = await camera.current.takePhoto({
|
* const photo = await camera.current.takePhoto({
|
||||||
* qualityPrioritization: 'quality',
|
* qualityPrioritization: 'quality',
|
||||||
* flash: 'on',
|
* flash: 'on',
|
||||||
* enableAutoRedEyeReduction: true
|
* enableAutoRedEyeReduction: true
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async takePhoto(options?: TakePhotoOptions): Promise<PhotoFile> {
|
public async takePhoto(options?: TakePhotoOptions): Promise<PhotoFile> {
|
||||||
try {
|
try {
|
||||||
return await CameraModule.takePhoto(this.handle, options ?? {})
|
return await CameraModule.takePhoto(this.handle, options ?? {})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number {
|
private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number {
|
||||||
if (typeof bitRate === 'number' || bitRate == null) return 1
|
if (typeof bitRate === 'number' || bitRate == null) return 1
|
||||||
switch (bitRate) {
|
switch (bitRate) {
|
||||||
case 'extra-low':
|
case 'extra-low':
|
||||||
return 0.6
|
return 0.6
|
||||||
case 'low':
|
case 'low':
|
||||||
return 0.8
|
return 0.8
|
||||||
case 'normal':
|
case 'normal':
|
||||||
return 1
|
return 1
|
||||||
case 'high':
|
case 'high':
|
||||||
return 1.2
|
return 1.2
|
||||||
case 'extra-high':
|
case 'extra-high':
|
||||||
return 1.4
|
return 1.4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new video recording.
|
* Start a new video recording.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
* @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* camera.current.startRecording({
|
* camera.current.startRecording({
|
||||||
* onRecordingFinished: (video) => console.log(video),
|
* onRecordingFinished: (video) => console.log(video),
|
||||||
* onRecordingError: (error) => console.error(error),
|
* onRecordingError: (error) => console.error(error),
|
||||||
* })
|
* })
|
||||||
* setTimeout(() => {
|
* setTimeout(() => {
|
||||||
* camera.current.stopRecording()
|
* camera.current.stopRecording()
|
||||||
* }, 5000)
|
* }, 5000)
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public startRecording(options: RecordVideoOptions, filePath: string): void {
|
public startRecording(options: RecordVideoOptions): void {
|
||||||
const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options
|
const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options
|
||||||
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
|
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
|
||||||
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!')
|
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!')
|
||||||
|
|
||||||
if (options.flash === 'on') {
|
if (options.flash === 'on') {
|
||||||
// Enable torch for video recording
|
// Enable torch for video recording
|
||||||
this.setState({
|
this.setState({
|
||||||
isRecordingWithFlash: true,
|
isRecordingWithFlash: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeOptions: NativeRecordVideoOptions = passThruOptions
|
const nativeOptions: NativeRecordVideoOptions = passThruOptions
|
||||||
if (typeof videoBitRate === 'number') {
|
if (typeof videoBitRate === 'number') {
|
||||||
// If the user passed an absolute number as a bit-rate, we just use this as a full override.
|
// If the user passed an absolute number as a bit-rate, we just use this as a full override.
|
||||||
nativeOptions.videoBitRateOverride = videoBitRate
|
nativeOptions.videoBitRateOverride = videoBitRate
|
||||||
} else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') {
|
} else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') {
|
||||||
// If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it
|
// If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it
|
||||||
nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate)
|
nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
|
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
|
||||||
if (this.state.isRecordingWithFlash) {
|
if (this.state.isRecordingWithFlash) {
|
||||||
// disable torch again if it was enabled
|
// disable torch again if it was enabled
|
||||||
this.setState({
|
this.setState({
|
||||||
isRecordingWithFlash: false,
|
isRecordingWithFlash: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error != null) return onRecordingError(error)
|
if (error != null) return onRecordingError(error)
|
||||||
if (video != null) return onRecordingFinished(video)
|
if (video != null) return onRecordingFinished(video)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// TODO: Use TurboModules to make this awaitable.
|
// TODO: Use TurboModules to make this awaitable.
|
||||||
CameraModule.startRecording(this.handle, nativeOptions, filePath, onRecordCallback)
|
CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the current video recording.
|
* Pauses the current video recording.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
* @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Start
|
* // Start
|
||||||
* await camera.current.startRecording()
|
* await camera.current.startRecording()
|
||||||
* await timeout(1000)
|
* await timeout(1000)
|
||||||
* // Pause
|
* // Pause
|
||||||
* await camera.current.pauseRecording()
|
* await camera.current.pauseRecording()
|
||||||
* await timeout(500)
|
* await timeout(500)
|
||||||
* // Resume
|
* // Resume
|
||||||
* await camera.current.resumeRecording()
|
* await camera.current.resumeRecording()
|
||||||
* await timeout(2000)
|
* await timeout(2000)
|
||||||
* // Stop
|
* // Stop
|
||||||
* const video = await camera.current.stopRecording()
|
* const video = await camera.current.stopRecording()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async pauseRecording(): Promise<void> {
|
public async pauseRecording(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return await CameraModule.pauseRecording(this.handle)
|
return await CameraModule.pauseRecording(this.handle)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resumes a currently paused video recording.
|
* Resumes a currently paused video recording.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
* @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Start
|
* // Start
|
||||||
* await camera.current.startRecording()
|
* await camera.current.startRecording()
|
||||||
* await timeout(1000)
|
* await timeout(1000)
|
||||||
* // Pause
|
* // Pause
|
||||||
* await camera.current.pauseRecording()
|
* await camera.current.pauseRecording()
|
||||||
* await timeout(500)
|
* await timeout(500)
|
||||||
* // Resume
|
* // Resume
|
||||||
* await camera.current.resumeRecording()
|
* await camera.current.resumeRecording()
|
||||||
* await timeout(2000)
|
* await timeout(2000)
|
||||||
* // Stop
|
* // Stop
|
||||||
* const video = await camera.current.stopRecording()
|
* const video = await camera.current.stopRecording()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async resumeRecording(): Promise<void> {
|
public async resumeRecording(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return await CameraModule.resumeRecording(this.handle)
|
return await CameraModule.resumeRecording(this.handle)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the current video recording.
|
* Stop the current video recording.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
* @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* await camera.current.startRecording()
|
* await camera.current.startRecording()
|
||||||
* setTimeout(async () => {
|
* setTimeout(async () => {
|
||||||
* const video = await camera.current.stopRecording()
|
* const video = await camera.current.stopRecording()
|
||||||
* }, 5000)
|
* }, 5000)
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async stopRecording(): Promise<void> {
|
public async stopRecording(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return await CameraModule.stopRecording(this.handle)
|
return await CameraModule.stopRecording(this.handle)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focus the camera to a specific point in the coordinate system.
|
* Focus the camera to a specific point in the coordinate system.
|
||||||
* @param {Point} point The point to focus to. This should be relative
|
* @param {Point} point The point to focus to. This should be relative
|
||||||
* to the Camera view's coordinate system and is expressed in points.
|
* to the Camera view's coordinate system and is expressed in points.
|
||||||
* * `(0, 0)` means **top left**.
|
* * `(0, 0)` means **top left**.
|
||||||
* * `(CameraView.width, CameraView.height)` means **bottom right**.
|
* * `(CameraView.width, CameraView.height)` means **bottom right**.
|
||||||
*
|
*
|
||||||
* Make sure the value doesn't exceed the CameraView's dimensions.
|
* Make sure the value doesn't exceed the CameraView's dimensions.
|
||||||
*
|
*
|
||||||
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* await camera.current.focus({
|
* await camera.current.focus({
|
||||||
* x: tapEvent.x,
|
* x: tapEvent.x,
|
||||||
* y: tapEvent.y
|
* y: tapEvent.y
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async focus(point: Point): Promise<void> {
|
public async focus(point: Point): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return await CameraModule.focus(this.handle, point)
|
return await CameraModule.focus(this.handle, point)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw tryParseNativeCameraError(e)
|
throw tryParseNativeCameraError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
public async lockCurrentExposure(): Promise<void> {
|
//#region Static Functions (NativeModule)
|
||||||
try {
|
/**
|
||||||
return await CameraModule.lockCurrentExposure(this.handle)
|
* Get a list of all available camera devices on the current phone.
|
||||||
} catch (e) {
|
*
|
||||||
throw tryParseNativeCameraError(e)
|
* If you use Hooks, use the `useCameraDevices(..)` hook instead.
|
||||||
}
|
*
|
||||||
}
|
* * For Camera Devices attached to the phone, it is safe to assume that this will never change.
|
||||||
|
* * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const devices = Camera.getAvailableCameraDevices()
|
||||||
|
* const backCameras = devices.filter((d) => d.position === "back")
|
||||||
|
* const frontCameras = devices.filter((d) => d.position === "front")
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static getAvailableCameraDevices(): CameraDevice[] {
|
||||||
|
return CameraDevices.getAvailableCameraDevices()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds a listener that gets called everytime the Camera Devices change, for example
|
||||||
|
* when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out.
|
||||||
|
*
|
||||||
|
* If you use Hooks, use the `useCameraDevices()` hook instead.
|
||||||
|
*/
|
||||||
|
public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription {
|
||||||
|
return CameraDevices.addCameraDevicesChangedListener(listener)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
|
||||||
|
* the user has permitted the app to use the camera.
|
||||||
|
*
|
||||||
|
* To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
|
||||||
|
*/
|
||||||
|
public static getCameraPermissionStatus(): CameraPermissionStatus {
|
||||||
|
return CameraModule.getCameraPermissionStatus()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the current Microphone-Recording Permission Status. Check this before mounting the Camera to ensure
|
||||||
|
* the user has permitted the app to use the microphone.
|
||||||
|
*
|
||||||
|
* To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
|
||||||
|
*/
|
||||||
|
public static getMicrophonePermissionStatus(): CameraPermissionStatus {
|
||||||
|
return CameraModule.getMicrophonePermissionStatus()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows a "request permission" alert to the user, and resolves with the new camera permission status.
|
||||||
|
*
|
||||||
|
* If the user has previously blocked the app from using the camera, the alert will not be shown
|
||||||
|
* and `"denied"` will be returned.
|
||||||
|
*
|
||||||
|
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
||||||
|
*/
|
||||||
|
public static async requestCameraPermission(): Promise<CameraPermissionRequestResult> {
|
||||||
|
try {
|
||||||
|
return await CameraModule.requestCameraPermission()
|
||||||
|
} catch (e) {
|
||||||
|
throw tryParseNativeCameraError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
|
||||||
|
*
|
||||||
|
* If the user has previously blocked the app from using the microphone, the alert will not be shown
|
||||||
|
* and `"denied"` will be returned.
|
||||||
|
*
|
||||||
|
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
||||||
|
*/
|
||||||
|
public static async requestMicrophonePermission(): Promise<CameraPermissionRequestResult> {
|
||||||
|
try {
|
||||||
|
return await CameraModule.requestMicrophonePermission()
|
||||||
|
} catch (e) {
|
||||||
|
throw tryParseNativeCameraError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
public async unlockCurrentExposure(): Promise<void> {
|
//#region Events (Wrapped to maintain reference equality)
|
||||||
try {
|
private onError(event: NativeSyntheticEvent<OnErrorEvent>): void {
|
||||||
return await CameraModule.unlockCurrentExposure(this.handle)
|
const error = event.nativeEvent
|
||||||
} catch (e) {
|
const cause = isErrorWithCause(error.cause) ? error.cause : undefined
|
||||||
throw tryParseNativeCameraError(e)
|
// @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
|
||||||
}
|
const cameraError = new CameraRuntimeError(error.code, error.message, cause)
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Static Functions (NativeModule)
|
if (this.props.onError != null) {
|
||||||
/**
|
this.props.onError(cameraError)
|
||||||
* Get a list of all available camera devices on the current phone.
|
} else {
|
||||||
*
|
// User didn't pass an `onError` handler, so just log it to console
|
||||||
* If you use Hooks, use the `useCameraDevices(..)` hook instead.
|
console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError)
|
||||||
*
|
}
|
||||||
* * For Camera Devices attached to the phone, it is safe to assume that this will never change.
|
}
|
||||||
* * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const devices = Camera.getAvailableCameraDevices()
|
|
||||||
* const backCameras = devices.filter((d) => d.position === "back")
|
|
||||||
* const frontCameras = devices.filter((d) => d.position === "front")
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public static getAvailableCameraDevices(): CameraDevice[] {
|
|
||||||
return CameraDevices.getAvailableCameraDevices()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Adds a listener that gets called everytime the Camera Devices change, for example
|
|
||||||
* when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out.
|
|
||||||
*
|
|
||||||
* If you use Hooks, use the `useCameraDevices()` hook instead.
|
|
||||||
*/
|
|
||||||
public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription {
|
|
||||||
return CameraDevices.addCameraDevicesChangedListener(listener)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
|
|
||||||
* the user has permitted the app to use the camera.
|
|
||||||
*
|
|
||||||
* To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
|
|
||||||
*/
|
|
||||||
public static getCameraPermissionStatus(): CameraPermissionStatus {
|
|
||||||
return CameraModule.getCameraPermissionStatus()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Gets the current Microphone-Recording Permission Status. Check this before mounting the Camera to ensure
|
|
||||||
* the user has permitted the app to use the microphone.
|
|
||||||
*
|
|
||||||
* To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
|
|
||||||
*/
|
|
||||||
public static getMicrophonePermissionStatus(): CameraPermissionStatus {
|
|
||||||
return CameraModule.getMicrophonePermissionStatus()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Shows a "request permission" alert to the user, and resolves with the new camera permission status.
|
|
||||||
*
|
|
||||||
* If the user has previously blocked the app from using the camera, the alert will not be shown
|
|
||||||
* and `"denied"` will be returned.
|
|
||||||
*
|
|
||||||
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
|
||||||
*/
|
|
||||||
public static async requestCameraPermission(): Promise<CameraPermissionRequestResult> {
|
|
||||||
try {
|
|
||||||
return await CameraModule.requestCameraPermission()
|
|
||||||
} catch (e) {
|
|
||||||
throw tryParseNativeCameraError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
|
|
||||||
*
|
|
||||||
* If the user has previously blocked the app from using the microphone, the alert will not be shown
|
|
||||||
* and `"denied"` will be returned.
|
|
||||||
*
|
|
||||||
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
|
|
||||||
*/
|
|
||||||
public static async requestMicrophonePermission(): Promise<CameraPermissionRequestResult> {
|
|
||||||
try {
|
|
||||||
return await CameraModule.requestMicrophonePermission()
|
|
||||||
} catch (e) {
|
|
||||||
throw tryParseNativeCameraError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Events (Wrapped to maintain reference equality)
|
private onInitialized(): void {
|
||||||
private onError(event: NativeSyntheticEvent<OnErrorEvent>): void {
|
this.props.onInitialized?.()
|
||||||
const error = event.nativeEvent
|
}
|
||||||
const cause = isErrorWithCause(error.cause) ? error.cause : undefined
|
|
||||||
// @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
|
|
||||||
const cameraError = new CameraRuntimeError(error.code, error.message, cause)
|
|
||||||
|
|
||||||
if (this.props.onError != null) {
|
private onStarted(): void {
|
||||||
this.props.onError(cameraError)
|
this.props.onStarted?.()
|
||||||
} else {
|
}
|
||||||
// User didn't pass an `onError` handler, so just log it to console
|
|
||||||
console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onInitialized(): void {
|
private onStopped(): void {
|
||||||
this.props.onInitialized?.()
|
this.props.onStopped?.()
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
private onStarted(): void {
|
private onCodeScanned(event: NativeSyntheticEvent<OnCodeScannedEvent>): void {
|
||||||
this.props.onStarted?.()
|
const codeScanner = this.props.codeScanner
|
||||||
}
|
if (codeScanner == null) return
|
||||||
|
|
||||||
private onStopped(): void {
|
codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame)
|
||||||
this.props.onStopped?.()
|
}
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
private onCodeScanned(event: NativeSyntheticEvent<OnCodeScannedEvent>): void {
|
//#region Lifecycle
|
||||||
const codeScanner = this.props.codeScanner
|
private setFrameProcessor(frameProcessor: FrameProcessor): void {
|
||||||
if (codeScanner == null) return
|
VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor)
|
||||||
|
}
|
||||||
|
|
||||||
codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame)
|
private unsetFrameProcessor(): void {
|
||||||
}
|
VisionCameraProxy.removeFrameProcessor(this.handle)
|
||||||
|
}
|
||||||
|
|
||||||
//#region Lifecycle
|
private onViewReady(): void {
|
||||||
private setFrameProcessor(frameProcessor: FrameProcessor): void {
|
this.isNativeViewMounted = true
|
||||||
VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor)
|
if (this.props.frameProcessor != null) {
|
||||||
}
|
// user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now.
|
||||||
|
this.setFrameProcessor(this.props.frameProcessor)
|
||||||
|
this.lastFrameProcessor = this.props.frameProcessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsetFrameProcessor(): void {
|
/** @internal */
|
||||||
VisionCameraProxy.removeFrameProcessor(this.handle)
|
componentDidUpdate(): void {
|
||||||
}
|
if (!this.isNativeViewMounted) return
|
||||||
|
const frameProcessor = this.props.frameProcessor
|
||||||
|
if (frameProcessor !== this.lastFrameProcessor) {
|
||||||
|
// frameProcessor argument identity changed. Update native to reflect the change.
|
||||||
|
if (frameProcessor != null) this.setFrameProcessor(frameProcessor)
|
||||||
|
else this.unsetFrameProcessor()
|
||||||
|
|
||||||
private onViewReady(): void {
|
this.lastFrameProcessor = frameProcessor
|
||||||
this.isNativeViewMounted = true
|
}
|
||||||
if (this.props.frameProcessor != null) {
|
}
|
||||||
// user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now.
|
//#endregion
|
||||||
this.setFrameProcessor(this.props.frameProcessor)
|
|
||||||
this.lastFrameProcessor = this.props.frameProcessor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
componentDidUpdate(): void {
|
public render(): React.ReactNode {
|
||||||
if (!this.isNativeViewMounted) return
|
// We remove the big `device` object from the props because we only need to pass `cameraId` to native.
|
||||||
const frameProcessor = this.props.frameProcessor
|
const { device, frameProcessor, codeScanner, ...props } = this.props
|
||||||
if (frameProcessor !== this.lastFrameProcessor) {
|
|
||||||
// frameProcessor argument identity changed. Update native to reflect the change.
|
|
||||||
if (frameProcessor != null) this.setFrameProcessor(frameProcessor)
|
|
||||||
else this.unsetFrameProcessor()
|
|
||||||
|
|
||||||
this.lastFrameProcessor = frameProcessor
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
}
|
if (device == null) {
|
||||||
}
|
throw new Error(
|
||||||
//#endregion
|
'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
const shouldEnableBufferCompression = props.video === true && frameProcessor == null
|
||||||
public render(): React.ReactNode {
|
const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native')
|
||||||
// We remove the big `device` object from the props because we only need to pass `cameraId` to native.
|
const torch = this.state.isRecordingWithFlash ? 'on' : props.torch
|
||||||
const { device, frameProcessor, codeScanner, ...props } = this.props
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
return (
|
||||||
if (device == null) {
|
<NativeCameraView
|
||||||
throw new CameraRuntimeError(
|
{...props}
|
||||||
'device/no-device',
|
cameraId={device.id}
|
||||||
'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices',
|
ref={this.ref}
|
||||||
)
|
torch={torch}
|
||||||
}
|
onViewReady={this.onViewReady}
|
||||||
|
onInitialized={this.onInitialized}
|
||||||
const shouldEnableBufferCompression = props.video === true && frameProcessor == null
|
onCodeScanned={this.onCodeScanned}
|
||||||
const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native')
|
onStarted={this.onStarted}
|
||||||
const torch = this.state.isRecordingWithFlash ? 'on' : props.torch
|
onStopped={this.onStopped}
|
||||||
|
onError={this.onError}
|
||||||
return (
|
codeScannerOptions={codeScanner}
|
||||||
<NativeCameraView
|
enableFrameProcessor={frameProcessor != null}
|
||||||
{...props}
|
enableBufferCompression={props.enableBufferCompression ?? shouldEnableBufferCompression}
|
||||||
cameraId={device.id}
|
pixelFormat={pixelFormat}
|
||||||
ref={this.ref}
|
/>
|
||||||
torch={torch}
|
)
|
||||||
onViewReady={this.onViewReady}
|
}
|
||||||
onInitialized={this.onInitialized}
|
|
||||||
onCodeScanned={this.onCodeScanned}
|
|
||||||
onStarted={this.onStarted}
|
|
||||||
onStopped={this.onStopped}
|
|
||||||
onError={this.onError}
|
|
||||||
codeScannerOptions={codeScanner}
|
|
||||||
enableFrameProcessor={frameProcessor != null}
|
|
||||||
enableBufferCompression={props.enableBufferCompression ?? shouldEnableBufferCompression}
|
|
||||||
pixelFormat={pixelFormat}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
|
// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
|
||||||
const NativeCameraView = requireNativeComponent<NativeCameraViewProps>(
|
const NativeCameraView = requireNativeComponent<NativeCameraViewProps>(
|
||||||
'CameraView',
|
'CameraView',
|
||||||
// @ts-expect-error because the type declarations are kinda wrong, no?
|
// @ts-expect-error because the type declarations are kinda wrong, no?
|
||||||
Camera,
|
Camera,
|
||||||
)
|
)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user