Compare commits
	
		
			102 Commits
		
	
	
		
			ivan/try-t
			...
			4798aad464
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4798aad464 | |||
|  | 2c8d503e66 | ||
| 5b52acda26 | |||
| 17f675657e | |||
| c64516693c | |||
| e9f08ef488 | |||
| bf122db919 | |||
| 3319e48f7d | |||
| 58714f9dac | |||
| 8991779851 | |||
| f8efa172ba | |||
| 66f840eecb | |||
| fcf5fe70f3 | |||
|  | 3a20c44a31 | ||
| 0329e7976d | |||
| 7c162fecb1 | |||
| b28a152471 | |||
| 65021b895a | |||
|  | b006b1e744 | ||
|  | 694d9cfa8c | ||
|  | 91767e71c8 | ||
|  | 9f2c7906e5 | ||
|  | 621bfe333c | ||
|  | 20f8fa2937 | ||
|  | b03f9ea423 | ||
|  | 98d90a6442 | ||
|  | 0a43d7a160 | ||
|  | a2ce4df663 | ||
|  | 89ecb35616 | ||
|  | d9a1287b68 | ||
|  | 23459b2635 | ||
|  | 83168044a6 | ||
|  | 60925fc816 | ||
|  | d731fe491d | ||
|  | ebe04b246c | ||
|  | 97941a919f | ||
|  | 8e1f03907b | ||
|  | 3f1a7c9e32 | ||
|  | e8dd1e0b2e | ||
|  | bca9472ab8 | ||
|  | 79ebae56f3 | ||
|  | 86637ac112 | ||
|  | 8225ac1aff | ||
|  | 83b852acd0 | ||
|  | 5ab7b291db | ||
|  | eb57b3877c | ||
|  | 369cb4a043 | ||
|  | fabf019f66 | ||
|  | 4c159aff61 | ||
|  | 55992bb954 | ||
|  | 7ac6f4d008 | ||
|  | 9af6e61dc8 | ||
|  | 2c52fb01e4 | ||
|  | f6b7163660 | ||
|  | 2c976d8ccd | ||
|  | 147aff8683 | ||
|  | bda43d3984 | ||
|  | b105de0194 | ||
|  | 57c6431353 | ||
|  | aad7d2b8b3 | ||
|  | e6c12e2ed2 | ||
|  | 31754eb74c | ||
|  | 129e21f14e | ||
|  | 4168d8f752 | ||
|  | bcd12649e2 | ||
|  | c5646ca1e2 | ||
|  | 83c0184796 | ||
|  | 5df5ca9adf | ||
|  | 21042048ae | ||
|  | a7701c8c9c | ||
|  | 37398cc909 | ||
|  | 61b2f7dd4a | ||
|  | 14daaaaf9d | ||
|  | 1a0bd8f7c2 | ||
|  | 478688529b | ||
|  | 3699ccde94 | ||
|  | ad33dd91b1 | ||
|  | 0130085376 | ||
|  | ec7ce36d25 | ||
|  | b7e24c444e | ||
|  | a2a2e94865 | ||
|  | 1011c3f039 | ||
|  | 24f43efa35 | ||
|  | 0ed3aed48a | ||
|  | b3a88278de | ||
|  | 919e6c9fe8 | ||
|  | fb1d82ad9a | ||
|  | fce6616964 | ||
|  | a4e241a431 | ||
|  | 3192f5e939 | ||
|  | b20d0fc5f7 | ||
|  | 395ee7af89 | ||
|  | 3b892c209e | ||
|  | dbb7f80dc9 | ||
|  | 5acc64e031 | ||
|  | cd5fdd4924 | ||
|  | d8c95c901f | ||
|  | 97168c647c | ||
|  | 14554fa31a | ||
|  | e95264f782 | ||
|  | 8c66d36d8f | ||
|  | 1a8e518180 | 
							
								
								
									
										8
									
								
								.github/workflows/build-android.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/build-android.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,11 +30,11 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Setup JDK 11 | ||||
|       - name: Setup JDK 17 | ||||
|         uses: actions/setup-java@v4 | ||||
|         with: | ||||
|           distribution: 'zulu' | ||||
|           java-version: 11 | ||||
|           java-version: 17 | ||||
|           java-package: jdk | ||||
|  | ||||
|       - name: Get yarn cache directory path | ||||
| @@ -74,11 +74,11 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Setup JDK 11 | ||||
|       - name: Setup JDK 17 | ||||
|         uses: actions/setup-java@v4 | ||||
|         with: | ||||
|           distribution: 'zulu' | ||||
|           java-version: 11 | ||||
|           java-version: 17 | ||||
|           java-package: jdk | ||||
|  | ||||
|       - 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 | ||||
|  | ||||
|       - name: Restore Pods cache | ||||
|         uses: actions/cache@v3 | ||||
|         uses: actions/cache@v4 | ||||
|         with: | ||||
|           path: package/example/ios/Pods | ||||
|           key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|           working-directory: package/example/ios | ||||
|  | ||||
|       - name: Restore Pods cache | ||||
|         uses: actions/cache@v3 | ||||
|         uses: actions/cache@v4 | ||||
|         with: | ||||
|           path: package/example/ios/Pods | ||||
|           key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/compress-images.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/compress-images.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| 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,6 +47,15 @@ cd ios && pod install | ||||
|  | ||||
| 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 | ||||
|  | ||||
| ```tsx | ||||
|   | ||||
| @@ -130,4 +130,23 @@ The Code Scanner will call your [`onCodeScanned`](/docs/api/interfaces/CodeScann | ||||
|  | ||||
| <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) | ||||
|   | ||||
| @@ -34,12 +34,13 @@ 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) | ||||
|   * 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. | ||||
| * 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: | ||||
|  | ||||
| - [`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. 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. | ||||
| - [`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. | ||||
| - [`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. | ||||
| - [`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). | ||||
| - [`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>`. | ||||
| @@ -61,6 +62,7 @@ You can either find a matching format manually by looping through your `CameraDe | ||||
| ```ts | ||||
| const device = ... | ||||
| const format = useCameraFormat(device, [ | ||||
|   { videoAspectRatio: 16 / 9 }, | ||||
|   { videoResolution: { width: 3048, height: 2160 } }, | ||||
|   { fps: 60 } | ||||
| ]) | ||||
| @@ -72,6 +74,7 @@ const format = useCameraFormat(device, [ | ||||
| ```ts | ||||
| const device = ... | ||||
| const format = getCameraFormat(device, [ | ||||
|   { videoAspectRatio: 16 / 9 }, | ||||
|   { videoResolution: { width: 3048, height: 2160 } }, | ||||
|   { fps: 60 } | ||||
| ]) | ||||
|   | ||||
| @@ -89,7 +89,8 @@ Additionally, you can also directly access the Frame's pixel data using [`toArra | ||||
| const frameProcessor = useFrameProcessor((frame) => { | ||||
|   'worklet' | ||||
|   if (frame.pixelFormat === 'rgb') { | ||||
|     const data = frame.toArrayBuffer() | ||||
|     const buffer = frame.toArrayBuffer() | ||||
|     const data = new Uint8Array(buffer) | ||||
|     console.log(`Pixel at 0,0: RGB(${data[0]}, ${data[1]}, ${data[2]})`) | ||||
|   } | ||||
| }, []) | ||||
|   | ||||
| @@ -33,6 +33,7 @@ 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-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-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). | ||||
| * [yasintorun/**vision-camera-base64**](https://github.com/yasintorun/vision-camera-base64): A plugin to convert a Frame to a base64 string. | ||||
|  | ||||
| @@ -40,7 +41,7 @@ cd ios && pod install | ||||
|  | ||||
|  | ||||
| <!-- 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. | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -50,7 +51,7 @@ cd ios && pod install | ||||
| <br /> | ||||
| <p align="center"> | ||||
| <b> | ||||
| <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! | ||||
| <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! | ||||
| </b> | ||||
| </p> | ||||
| <br /> | ||||
|   | ||||
| @@ -70,6 +70,13 @@ Enable Buffer Compression ([`enableBufferCompression`](/docs/api/interfaces/Came | ||||
|  | ||||
| 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 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:** | ||||
|  | ||||
| <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> | ||||
|   <a href="https://apps.apple.com/at/app/shadowlens/id6471849004"> | ||||
|   <a href="https://apps.apple.com/app/shadowlens/id6471849004"> | ||||
|     <img height={40} src={useBaseUrl("img/appstore.svg")} /> | ||||
|   </a> | ||||
|   <a href="https://play.google.com/store/apps/details?id=com.mrousavy.shadowlens"> | ||||
|     <img height={58} src={useBaseUrl("img/playstore.png")} /> | ||||
|     <img height={40} src={useBaseUrl("img/googleplay.svg")} /> | ||||
|   </a> | ||||
| </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. | ||||
| 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. | ||||
| 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: | ||||
| 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: | ||||
|    ```tsx | ||||
|    <Camera device={device} isActive={true} style={{ width: 500, height: 500 }} /> | ||||
|    ``` | ||||
|   | ||||
| @@ -25,7 +25,7 @@ module.exports = { | ||||
|     }, | ||||
|     announcementBar: { | ||||
|       id: 'shadowlens', | ||||
|       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>', | ||||
|       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>', | ||||
|       backgroundColor: '#e39600', | ||||
|       textColor: '#ffffff', | ||||
|       isCloseable: false, | ||||
|   | ||||
							
								
								
									
										2
									
								
								docs/static/img/googleplay.svg
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								docs/static/img/googleplay.svg
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/static/img/playstore.png
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/static/img/playstore.png
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.8 KiB | 
| @@ -46,6 +46,10 @@ def safeExtGet(prop, fallback) { | ||||
|   rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback | ||||
| } | ||||
|  | ||||
| def safeExtGetBool(prop, fallback) { | ||||
|   Boolean.parseBoolean("${safeExtGet(prop, fallback)}") | ||||
| } | ||||
|  | ||||
| def reactNativeArchitectures() { | ||||
|   def value = project.getProperties().get("reactNativeArchitectures") | ||||
|   return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] | ||||
| @@ -68,10 +72,10 @@ static def findNodeModules(baseDir) { | ||||
|  | ||||
| def nodeModules = findNodeModules(projectDir) | ||||
|  | ||||
| def hasWorklets = !safeExtGet("VisionCamera_disableFrameProcessors", false) && findProject(":react-native-worklets-core") != null | ||||
| def hasWorklets = !safeExtGetBool('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"}!") | ||||
|  | ||||
| def enableCodeScanner = safeExtGet("VisionCamera_enableCodeScanner", false) | ||||
| def enableCodeScanner = safeExtGetBool('VisionCamera_enableCodeScanner', false) | ||||
|  | ||||
| repositories { | ||||
|   google() | ||||
|   | ||||
| @@ -14,7 +14,7 @@ MutableJByteBuffer::MutableJByteBuffer(jni::alias_ref<jni::JByteBuffer> byteBuff | ||||
|   _byteBuffer = jni::make_global(byteBuffer); | ||||
| } | ||||
|  | ||||
| MutableJByteBuffer::~MutableJByteBuffer() noexcept { | ||||
| MutableJByteBuffer::~MutableJByteBuffer() { | ||||
|   // 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 | ||||
|   // the Java method, we connect to a JNI environment first. | ||||
|   | ||||
| @@ -29,10 +29,11 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWi | ||||
| } | ||||
|  | ||||
| OpenGLRenderer::~OpenGLRenderer() { | ||||
|   __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGLRenderer..."); | ||||
|   destroy(); | ||||
|   if (_outputSurface != nullptr) { | ||||
|     ANativeWindow_release(_outputSurface); | ||||
|   } | ||||
|   destroy(); | ||||
| } | ||||
|  | ||||
| void OpenGLRenderer::destroy() { | ||||
|   | ||||
| @@ -34,7 +34,7 @@ JSharedArray::JSharedArray(const jni::alias_ref<jhybridobject>& javaThis, const | ||||
| #else | ||||
|   jsi::Runtime& runtime = *proxy->cthis()->getJSRuntime(); | ||||
| #endif | ||||
|   __android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %i...", byteBuffer->getDirectSize()); | ||||
|   __android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %zu...", byteBuffer->getDirectSize()); | ||||
|   _byteBuffer = jni::make_global(byteBuffer); | ||||
|   _size = _byteBuffer->getDirectSize(); | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ TSelf JVisionCameraScheduler::initHybrid(jni::alias_ref<jhybridobject> jThis) { | ||||
| } | ||||
|  | ||||
| void JVisionCameraScheduler::dispatchAsync(const std::function<void()>& job) { | ||||
|   std::unique_lock<std::mutex> lock(_mutex); | ||||
|   // 1. add job to queue | ||||
|   _jobs.push(job); | ||||
|   scheduleTrigger(); | ||||
|   | ||||
| @@ -5,6 +5,8 @@ import com.facebook.react.bridge.Arguments | ||||
| import com.facebook.react.bridge.ReactContext | ||||
| import com.facebook.react.bridge.WritableMap | ||||
| 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.mrousavy.camera.core.CameraError | ||||
| import com.mrousavy.camera.core.CodeScannerFrame | ||||
| @@ -12,26 +14,30 @@ import com.mrousavy.camera.core.UnknownCameraError | ||||
| import com.mrousavy.camera.core.code | ||||
| import com.mrousavy.camera.types.CodeType | ||||
| import java.io.File | ||||
| import com.mrousavy.camera.types.* | ||||
|  | ||||
| fun CameraView.invokeOnInitialized() { | ||||
|   Log.i(CameraView.TAG, "invokeOnInitialized()") | ||||
|  | ||||
|   val reactContext = context as ReactContext | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null) | ||||
|   val surfaceId = UIManagerHelper.getSurfaceId(this) | ||||
|   val event = CameraInitializedEvent(surfaceId, id) | ||||
|   this.sendEvent(event) | ||||
| } | ||||
|  | ||||
| fun CameraView.invokeOnStarted() { | ||||
|   Log.i(CameraView.TAG, "invokeOnStarted()") | ||||
|  | ||||
|   val reactContext = context as ReactContext | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStarted", null) | ||||
|   val surfaceId = UIManagerHelper.getSurfaceId(this) | ||||
|   val event = CameraStartedEvent(surfaceId, id) | ||||
|   this.sendEvent(event) | ||||
| } | ||||
|  | ||||
| fun CameraView.invokeOnStopped() { | ||||
|   Log.i(CameraView.TAG, "invokeOnStopped()") | ||||
|  | ||||
|   val reactContext = context as ReactContext | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStopped", null) | ||||
|   val surfaceId = UIManagerHelper.getSurfaceId(this) | ||||
|   val event = CameraStoppedEvent(surfaceId, id) | ||||
|   this.sendEvent(event) | ||||
| } | ||||
|  | ||||
| fun CameraView.invokeOnChunkReady(filepath: File, index: Int) { | ||||
| @@ -47,24 +53,27 @@ fun CameraView.invokeOnError(error: Throwable) { | ||||
|   Log.e(CameraView.TAG, "invokeOnError(...):") | ||||
|   error.printStackTrace() | ||||
|  | ||||
|   val cameraError = when (error) { | ||||
|     is CameraError -> error | ||||
|     else -> UnknownCameraError(error) | ||||
|   } | ||||
|   val event = Arguments.createMap() | ||||
|   event.putString("code", cameraError.code) | ||||
|   event.putString("message", cameraError.message) | ||||
|   val cameraError = | ||||
|     when (error) { | ||||
|       is CameraError -> error | ||||
|       else -> UnknownCameraError(error) | ||||
|     } | ||||
|   val data = Arguments.createMap() | ||||
|   data.putString("code", cameraError.code) | ||||
|   data.putString("message", cameraError.message) | ||||
|   cameraError.cause?.let { cause -> | ||||
|     event.putMap("cause", errorToMap(cause)) | ||||
|     data.putMap("cause", errorToMap(cause)) | ||||
|   } | ||||
|   val reactContext = context as ReactContext | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraError", event) | ||||
|  | ||||
|   val surfaceId = UIManagerHelper.getSurfaceId(this) | ||||
|   val event = CameraErrorEvent(surfaceId, id, data) | ||||
|   this.sendEvent(event) | ||||
| } | ||||
|  | ||||
| fun CameraView.invokeOnViewReady() { | ||||
|   val event = Arguments.createMap() | ||||
|   val reactContext = context as ReactContext | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraViewReady", event) | ||||
|   val surfaceId = UIManagerHelper.getSurfaceId(this) | ||||
|   val event = CameraViewReadyEvent(surfaceId, id) | ||||
|   this.sendEvent(event) | ||||
| } | ||||
|  | ||||
| fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeScannerFrame) { | ||||
| @@ -97,14 +106,23 @@ fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeSc | ||||
|     codes.pushMap(code) | ||||
|   } | ||||
|  | ||||
|   val event = Arguments.createMap() | ||||
|   event.putArray("codes", codes) | ||||
|   val data = Arguments.createMap() | ||||
|   data.putArray("codes", codes) | ||||
|   val codeScannerFrame = Arguments.createMap() | ||||
|   codeScannerFrame.putInt("width", scannerFrame.width) | ||||
|   codeScannerFrame.putInt("height", scannerFrame.height) | ||||
|   event.putMap("frame", codeScannerFrame) | ||||
|   data.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 | ||||
|   reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraCodeScanned", event) | ||||
|   val dispatcher = | ||||
|     UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) | ||||
|   dispatcher?.dispatchEvent(event) | ||||
| } | ||||
|  | ||||
| private fun errorToMap(error: Throwable): WritableMap { | ||||
|   | ||||
| @@ -13,66 +13,36 @@ import com.facebook.react.bridge.ReadableMap | ||||
| import com.facebook.react.bridge.WritableMap | ||||
| import com.mrousavy.camera.core.CameraSession | ||||
| import com.mrousavy.camera.core.InsufficientStorageError | ||||
| import com.mrousavy.camera.utils.FileUtils | ||||
| import com.mrousavy.camera.types.Flash | ||||
| import com.mrousavy.camera.types.QualityPrioritization | ||||
| import com.mrousavy.camera.utils.* | ||||
| import java.io.File | ||||
| import java.io.FileOutputStream | ||||
| import java.io.IOException | ||||
| import kotlinx.coroutines.* | ||||
|  | ||||
| private const val TAG = "CameraView.takePhoto" | ||||
| private const val TAG = "CameraView.takeSnapshot" | ||||
|  | ||||
| @SuppressLint("UnsafeOptInUsageError") | ||||
| suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { | ||||
|   val options = optionsMap.toHashMap() | ||||
|   Log.i(TAG, "Taking photo... Options: $options") | ||||
|   Log.i(TAG, "Taking snapshot... Options: $options") | ||||
|   val bitmap = previewView.getBitmap() ?: throw Error() | ||||
|  | ||||
|   val qualityPrioritization = options["qualityPrioritization"] as? String ?: "balanced" | ||||
|   val flash = options["flash"] as? String ?: "off" | ||||
|   val enableAutoRedEyeReduction = options["enableAutoRedEyeReduction"] == true | ||||
|   val enableAutoStabilization = options["enableAutoStabilization"] == true | ||||
|   val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true | ||||
|   val file = FileUtils.createTempFile(context, "png"); | ||||
|  | ||||
|   val flashMode = Flash.fromUnionValue(flash) | ||||
|   val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization) | ||||
|   // Write snapshot to .jpg file | ||||
|   FileUtils.writeBitmapTofile(bitmap, file, 100) | ||||
|  | ||||
|   val photo = cameraSession.takePhoto( | ||||
|     qualityPrioritizationMode, | ||||
|     flashMode, | ||||
|     enableShutterSound, | ||||
|     enableAutoRedEyeReduction, | ||||
|     enableAutoStabilization, | ||||
|     orientation | ||||
|   ) | ||||
|   Log.i(TAG, "Successfully saved snapshot to file!") | ||||
|  | ||||
|   photo.use { | ||||
|     Log.i(TAG, "Successfully captured ${photo.image.width} x ${photo.image.height} photo!") | ||||
|  | ||||
|     val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId!!) | ||||
|  | ||||
|     val path = try { | ||||
|       savePhotoToFile(context, cameraCharacteristics, photo) | ||||
|     } catch (e: IOException) { | ||||
|       if (e.message?.contains("no space left", true) == true) { | ||||
|         throw InsufficientStorageError() | ||||
|       } else { | ||||
|         throw e | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Log.i(TAG, "Successfully saved photo to file! $path") | ||||
|  | ||||
|     val map = Arguments.createMap() | ||||
|     map.putString("path", path) | ||||
|     map.putInt("width", photo.image.width) | ||||
|     map.putInt("height", photo.image.height) | ||||
|     map.putString("orientation", photo.orientation.unionValue) | ||||
|     map.putBoolean("isRawPhoto", photo.format == ImageFormat.RAW_SENSOR) | ||||
|     map.putBoolean("isMirrored", photo.isMirrored) | ||||
|  | ||||
|     return map | ||||
|   } | ||||
|   // Parse output data | ||||
|   val map = Arguments.createMap() | ||||
|   map.putString("path", file.absolutePath) | ||||
|   map.putInt("width", bitmap.width) | ||||
|   map.putInt("height", bitmap.height) | ||||
|   map.putBoolean("isMirrored", false) | ||||
|   return map | ||||
| } | ||||
|  | ||||
| private fun writePhotoToFile(photo: CameraSession.CapturedPhoto, file: File) { | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.hardware.camera2.CameraManager | ||||
| import android.util.Log | ||||
| import android.view.Gravity | ||||
| import android.view.ScaleGestureDetector | ||||
| import android.widget.FrameLayout | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.mrousavy.camera.core.CameraConfiguration | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| @@ -48,23 +48,23 @@ class CameraView(context: Context) : | ||||
|   // props that require reconfiguring | ||||
|   var cameraId: String? = null | ||||
|   var enableDepthData = false | ||||
|   var enableHighQualityPhotos: Boolean? = null | ||||
|   var enablePortraitEffectsMatteDelivery = false | ||||
|  | ||||
|   // use-cases | ||||
|   var photo: Boolean? = null | ||||
|   var video: Boolean? = null | ||||
|   var audio: Boolean? = null | ||||
|   var photo = false | ||||
|   var video = false | ||||
|   var audio = false | ||||
|   var enableFrameProcessor = false | ||||
|   var pixelFormat: PixelFormat = PixelFormat.NATIVE | ||||
|  | ||||
|   // props that require format reconfiguring | ||||
|   var format: ReadableMap? = null | ||||
|   var format: CameraDeviceFormat? = null | ||||
|   var fps: Int? = null | ||||
|   var videoStabilizationMode: VideoStabilizationMode? = null | ||||
|   var videoHdr = false | ||||
|   var photoHdr = false | ||||
|   var lowLightBoost: Boolean? = null // nullable bool | ||||
|   var lowLightBoost = false | ||||
|   var enableGpuBuffers = false | ||||
|  | ||||
|   // other props | ||||
|   var isActive = false | ||||
| @@ -72,7 +72,11 @@ class CameraView(context: Context) : | ||||
|   var zoom: Float = 1f // in "factor" | ||||
|   var exposure: Double = 1.0 | ||||
|   var orientation: Orientation = Orientation.PORTRAIT | ||||
|   var enableZoomGesture: Boolean = false | ||||
|       set(value) { | ||||
|         field = value | ||||
|         previewView.orientation = value | ||||
|       } | ||||
|   var enableZoomGesture = false | ||||
|     set(value) { | ||||
|       field = value | ||||
|       updateZoomGesture() | ||||
| @@ -82,7 +86,7 @@ class CameraView(context: Context) : | ||||
|       previewView.resizeMode = value | ||||
|       field = value | ||||
|     } | ||||
|   var enableFpsGraph: Boolean = false | ||||
|   var enableFpsGraph = false | ||||
|     set(value) { | ||||
|       field = value | ||||
|       updateFpsGraph() | ||||
| @@ -98,7 +102,7 @@ class CameraView(context: Context) : | ||||
|  | ||||
|   // session | ||||
|   internal val cameraSession: CameraSession | ||||
|   private val previewView: PreviewView | ||||
|   val previewView: PreviewView | ||||
|   private var currentConfigureCall: Long = System.currentTimeMillis() | ||||
|   internal var frameProcessor: FrameProcessor? = null | ||||
|  | ||||
| @@ -110,21 +114,26 @@ class CameraView(context: Context) : | ||||
|     clipToOutline = true | ||||
|     cameraSession = CameraSession(context, cameraManager, this) | ||||
|     previewView = cameraSession.createPreviewView(context) | ||||
|     previewView.layoutParams = LayoutParams( | ||||
|       LayoutParams.MATCH_PARENT, | ||||
|       LayoutParams.MATCH_PARENT, | ||||
|       Gravity.CENTER | ||||
|     ) | ||||
|     addView(previewView) | ||||
|   } | ||||
|  | ||||
|   override fun onAttachedToWindow() { | ||||
|     super.onAttachedToWindow() | ||||
|     if (!isMounted) { | ||||
|       isMounted = true | ||||
|       invokeOnViewReady() | ||||
|     } | ||||
|     update() | ||||
|     super.onAttachedToWindow() | ||||
|   } | ||||
|  | ||||
|   override fun onDetachedFromWindow() { | ||||
|     update() | ||||
|     super.onDetachedFromWindow() | ||||
|     update() | ||||
|   } | ||||
|  | ||||
|   fun destroy() { | ||||
| @@ -149,19 +158,20 @@ class CameraView(context: Context) : | ||||
|         config.cameraId = cameraId | ||||
|  | ||||
|         // Photo | ||||
|         if (photo == true) { | ||||
|         if (photo) { | ||||
|           config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr)) | ||||
|         } else { | ||||
|           config.photo = CameraConfiguration.Output.Disabled.create() | ||||
|         } | ||||
|  | ||||
|         // Video/Frame Processor | ||||
|         if (video == true || enableFrameProcessor) { | ||||
|         if (video || enableFrameProcessor) { | ||||
|           config.video = CameraConfiguration.Output.Enabled.create( | ||||
|             CameraConfiguration.Video( | ||||
|               videoHdr, | ||||
|               pixelFormat, | ||||
|               enableFrameProcessor | ||||
|               enableFrameProcessor, | ||||
|               enableGpuBuffers | ||||
|             ) | ||||
|           ) | ||||
|         } else { | ||||
| @@ -169,7 +179,7 @@ class CameraView(context: Context) : | ||||
|         } | ||||
|  | ||||
|         // Audio | ||||
|         if (audio == true) { | ||||
|         if (audio) { | ||||
|           config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit)) | ||||
|         } else { | ||||
|           config.audio = CameraConfiguration.Output.Disabled.create() | ||||
| @@ -189,12 +199,7 @@ class CameraView(context: Context) : | ||||
|         config.orientation = orientation | ||||
|  | ||||
|         // Format | ||||
|         val format = format | ||||
|         if (format != null) { | ||||
|           config.format = CameraDeviceFormat.fromJSValue(format) | ||||
|         } else { | ||||
|           config.format = null | ||||
|         } | ||||
|         config.format = format | ||||
|  | ||||
|         // Side-Props | ||||
|         config.fps = fps | ||||
|   | ||||
| @@ -5,10 +5,12 @@ import com.facebook.react.common.MapBuilder | ||||
| import com.facebook.react.uimanager.ThemedReactContext | ||||
| import com.facebook.react.uimanager.ViewGroupManager | ||||
| import com.facebook.react.uimanager.annotations.ReactProp | ||||
| import com.mrousavy.camera.types.CameraDeviceFormat | ||||
| import com.mrousavy.camera.types.CodeScannerOptions | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.PixelFormat | ||||
| import com.mrousavy.camera.types.ResizeMode | ||||
| import android.util.Log | ||||
| import com.mrousavy.camera.types.Torch | ||||
| import com.mrousavy.camera.types.VideoStabilizationMode | ||||
|  | ||||
| @@ -45,17 +47,17 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "photo") | ||||
|   fun setPhoto(view: CameraView, photo: Boolean?) { | ||||
|   fun setPhoto(view: CameraView, photo: Boolean) { | ||||
|     view.photo = photo | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "video") | ||||
|   fun setVideo(view: CameraView, video: Boolean?) { | ||||
|   fun setVideo(view: CameraView, video: Boolean) { | ||||
|     view.video = video | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "audio") | ||||
|   fun setAudio(view: CameraView, audio: Boolean?) { | ||||
|   fun setAudio(view: CameraView, audio: Boolean) { | ||||
|     view.audio = audio | ||||
|   } | ||||
|  | ||||
| @@ -66,8 +68,12 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|  | ||||
|   @ReactProp(name = "pixelFormat") | ||||
|   fun setPixelFormat(view: CameraView, pixelFormat: String?) { | ||||
|     val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) | ||||
|     view.pixelFormat = newPixelFormat | ||||
|     if (pixelFormat != null) { | ||||
|       val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) | ||||
|       view.pixelFormat = newPixelFormat | ||||
|     } else { | ||||
|       view.pixelFormat = PixelFormat.NATIVE | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "enableDepthData") | ||||
| @@ -85,15 +91,19 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|     view.enableFpsGraph = enableFpsGraph | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "videoStabilizationMode") | ||||
|   fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { | ||||
|     val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) | ||||
|     view.videoStabilizationMode = newMode | ||||
|   @ReactProp(name = "enableGpuBuffers") | ||||
|   fun setEnableGpuBuffers(view: CameraView, enableGpuBuffers: Boolean) { | ||||
|     view.enableGpuBuffers = enableGpuBuffers | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "enableHighQualityPhotos") | ||||
|   fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) { | ||||
|     view.enableHighQualityPhotos = enableHighQualityPhotos | ||||
|   @ReactProp(name = "videoStabilizationMode") | ||||
|   fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { | ||||
|     if (videoStabilizationMode != null) { | ||||
|       val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) | ||||
|       view.videoStabilizationMode = newMode | ||||
|     } else { | ||||
|       view.videoStabilizationMode = null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "enablePortraitEffectsMatteDelivery") | ||||
| @@ -103,13 +113,22 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|  | ||||
|   @ReactProp(name = "format") | ||||
|   fun setFormat(view: CameraView, format: ReadableMap?) { | ||||
|     view.format = format | ||||
|     if (format != null) { | ||||
|       val newFormat = CameraDeviceFormat.fromJSValue(format) | ||||
|       view.format = newFormat | ||||
|     } else { | ||||
|       view.format = null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "resizeMode") | ||||
|   fun setResizeMode(view: CameraView, resizeMode: String) { | ||||
|     val newMode = ResizeMode.fromUnionValue(resizeMode) | ||||
|     view.resizeMode = newMode | ||||
|   fun setResizeMode(view: CameraView, resizeMode: String?) { | ||||
|     if (resizeMode != null) { | ||||
|       val newMode = ResizeMode.fromUnionValue(resizeMode) | ||||
|       view.resizeMode = newMode | ||||
|     } else { | ||||
|       view.resizeMode = ResizeMode.COVER | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // TODO: Change when TurboModules release. | ||||
| @@ -120,30 +139,34 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|     view.fps = if (fps > 0) fps else null | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "photoHdr", defaultBoolean = false) | ||||
|   @ReactProp(name = "photoHdr") | ||||
|   fun setPhotoHdr(view: CameraView, photoHdr: Boolean) { | ||||
|     view.photoHdr = photoHdr | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "videoHdr", defaultBoolean = false) | ||||
|   @ReactProp(name = "videoHdr") | ||||
|   fun setVideoHdr(view: CameraView, videoHdr: Boolean) { | ||||
|     view.videoHdr = videoHdr | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "lowLightBoost") | ||||
|   fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) { | ||||
|   fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean) { | ||||
|     view.lowLightBoost = lowLightBoost | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "isActive", defaultBoolean = false) | ||||
|   @ReactProp(name = "isActive") | ||||
|   fun setIsActive(view: CameraView, isActive: Boolean) { | ||||
|     view.isActive = isActive | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "torch") | ||||
|   fun setTorch(view: CameraView, torch: String) { | ||||
|     val newMode = Torch.fromUnionValue(torch) | ||||
|     view.torch = newMode | ||||
|   fun setTorch(view: CameraView, torch: String?) { | ||||
|     if (torch != null) { | ||||
|       val newMode = Torch.fromUnionValue(torch) | ||||
|       view.torch = newMode | ||||
|     } else { | ||||
|       view.torch = Torch.OFF | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "zoom") | ||||
| @@ -158,14 +181,23 @@ class CameraViewManager : ViewGroupManager<CameraView>() { | ||||
|  | ||||
|   @ReactProp(name = "orientation") | ||||
|   fun setOrientation(view: CameraView, orientation: String?) { | ||||
|     val newMode = Orientation.fromUnionValue(orientation) | ||||
|     view.orientation = newMode | ||||
|     if (orientation != null) { | ||||
|       val newMode = Orientation.fromUnionValue(orientation) | ||||
|       Log.i(TAG, "Orientation set to: $newMode") | ||||
|       view.orientation = newMode | ||||
|     } else { | ||||
|       view.orientation = Orientation.PORTRAIT | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @ReactProp(name = "codeScannerOptions") | ||||
|   fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) { | ||||
|     val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions) | ||||
|     view.codeScannerOptions = newCodeScannerOptions | ||||
|   fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap?) { | ||||
|     if (codeScannerOptions != null) { | ||||
|       val newCodeScannerOptions = CodeScannerOptions.fromJSValue(codeScannerOptions) | ||||
|       view.codeScannerOptions = newCodeScannerOptions | ||||
|     } else { | ||||
|       view.codeScannerOptions = null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   companion object { | ||||
|   | ||||
| @@ -44,7 +44,7 @@ data class CameraConfiguration( | ||||
|   // Output<T> types, those need to be comparable | ||||
|   data class CodeScanner(val codeTypes: List<CodeType>) | ||||
|   data class Photo(val enableHdr: Boolean) | ||||
|   data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean) | ||||
|   data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean, val enableGpuBuffers: Boolean) | ||||
|   data class Audio(val nothing: Unit) | ||||
|   data class Preview(val surface: Surface) | ||||
|  | ||||
| @@ -67,7 +67,7 @@ data class CameraConfiguration( | ||||
|   } | ||||
|  | ||||
|   data class Difference( | ||||
|     // Input Camera (cameraId, isActive) | ||||
|     // Input Camera (cameraId) | ||||
|     val deviceChanged: Boolean, | ||||
|     // Outputs & Session (Photo, Video, CodeScanner, HDR, Format) | ||||
|     val outputsChanged: Boolean, | ||||
| @@ -75,14 +75,17 @@ data class CameraConfiguration( | ||||
|     val sidePropsChanged: Boolean, | ||||
|     // (isActive) changed | ||||
|     val isActiveChanged: Boolean | ||||
|   ) | ||||
|   ) { | ||||
|     val hasChanges: Boolean | ||||
|       get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged | ||||
|   } | ||||
|  | ||||
|   companion object { | ||||
|     fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration() | ||||
|  | ||||
|     fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference { | ||||
|       // input device | ||||
|       val deviceChanged = left?.cameraId != right.cameraId || left?.isActive != right.isActive | ||||
|       val deviceChanged = left?.cameraId != right.cameraId | ||||
|  | ||||
|       // outputs | ||||
|       val outputsChanged = deviceChanged || | ||||
| @@ -101,7 +104,7 @@ data class CameraConfiguration( | ||||
|         left.videoStabilizationMode != right.videoStabilizationMode || | ||||
|         left.exposure != right.exposure | ||||
|  | ||||
|       val isActiveChanged = left?.isActive != right.isActive | ||||
|       val isActiveChanged = sidePropsChanged || left?.isActive != right.isActive | ||||
|  | ||||
|       return Difference( | ||||
|         deviceChanged, | ||||
|   | ||||
| @@ -1,17 +1,24 @@ | ||||
| package com.mrousavy.camera.core | ||||
|  | ||||
| import android.content.res.Resources | ||||
| import android.graphics.ImageFormat | ||||
| import android.hardware.camera2.CameraCharacteristics | ||||
| import android.hardware.camera2.CameraExtensionCharacteristics | ||||
| import android.hardware.camera2.CameraManager | ||||
| import android.hardware.camera2.CameraMetadata | ||||
| import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Range | ||||
| import android.util.Size | ||||
| import android.util.SizeF | ||||
| import android.view.SurfaceHolder | ||||
| import com.facebook.react.bridge.Arguments | ||||
| import com.facebook.react.bridge.ReadableArray | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import com.mrousavy.camera.extensions.bigger | ||||
| import com.mrousavy.camera.extensions.getPhotoSizes | ||||
| import com.mrousavy.camera.extensions.getVideoSizes | ||||
| import com.mrousavy.camera.extensions.smaller | ||||
| import com.mrousavy.camera.extensions.toJSValue | ||||
| import com.mrousavy.camera.types.AutoFocusSystem | ||||
| import com.mrousavy.camera.types.DeviceType | ||||
| @@ -20,65 +27,127 @@ import com.mrousavy.camera.types.LensFacing | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.PixelFormat | ||||
| import com.mrousavy.camera.types.VideoStabilizationMode | ||||
| import com.mrousavy.camera.utils.CamcorderProfileUtils | ||||
| import kotlin.math.atan2 | ||||
| import kotlin.math.sqrt | ||||
|  | ||||
| class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) { | ||||
|   val characteristics = cameraManager.getCameraCharacteristics(cameraId) | ||||
|   val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics) | ||||
|   val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) | ||||
|   val extensions = getSupportedExtensions() | ||||
| class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) { | ||||
|   companion object { | ||||
|     private const val TAG = "CameraDeviceDetails" | ||||
|  | ||||
|     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(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 | ||||
|   val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA | ||||
|   val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT | ||||
|   val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) | ||||
|   val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT | ||||
|   val lensFacing = LensFacing.fromCameraCharacteristics(characteristics) | ||||
|   val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false | ||||
|   val focalLengths = | ||||
|     characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) | ||||
|       // 35mm is the film standard sensor size | ||||
|       ?: floatArrayOf(35f) | ||||
|   val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! | ||||
|   val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! | ||||
|   val minFocusDistance = getMinFocusDistanceCm() | ||||
|   val name = ( | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | ||||
|       characteristics.get(CameraCharacteristics.INFO_VERSION) | ||||
|     } else { | ||||
|       null | ||||
|     } | ||||
|     ) ?: "$lensFacing ($cameraId)" | ||||
|   val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) } | ||||
|   val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) } | ||||
|   val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) } | ||||
|   val supportsLowLightBoost by lazy { | ||||
|     extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) && | ||||
|       modes.contains(CameraCharacteristics.CONTROL_MODE_USE_SCENE_MODE) | ||||
|   } | ||||
|   val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) } | ||||
|   val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false } | ||||
|   val focalLengths by lazy { | ||||
|     // 35mm is the film standard sensor size | ||||
|     characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f) | ||||
|   } | ||||
|   val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) ?: SizeF(0f, 0f) } | ||||
|   val activeSize | ||||
|     get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! | ||||
|   val sensorOrientation by lazy { | ||||
|     val degrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 | ||||
|     return@lazy Orientation.fromRotationDegrees(degrees) | ||||
|   } | ||||
|   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) | ||||
|   val zoomRange = ( | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||||
|   val maxDigitalZoom by lazy { characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f } | ||||
|   val zoomRange by lazy { | ||||
|     val range = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||||
|       characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) | ||||
|     } else { | ||||
|       null | ||||
|     } | ||||
|     ) ?: 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) | ||||
|     return@lazy range ?: Range(1f, maxDigitalZoom) | ||||
|   } | ||||
|   val minZoom = zoomRange.lower.toDouble() | ||||
|   val maxZoom = zoomRange.upper.toDouble() | ||||
|   val physicalDevices by lazy { | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) { | ||||
|       characteristics.physicalCameraIds | ||||
|     } else { | ||||
|       setOf(cameraId) | ||||
|     } | ||||
|   } | ||||
|   val minZoom by lazy { zoomRange.lower.toDouble() } | ||||
|   val maxZoom by lazy { zoomRange.upper.toDouble() } | ||||
|  | ||||
|   val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! | ||||
|   val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) | ||||
|   val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) | ||||
|   val digitalStabilizationModes = | ||||
|   val cameraConfig by lazy { characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! } | ||||
|   val isoRange by lazy { 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 digitalStabilizationModes by lazy { | ||||
|     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) | ||||
|   val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR | ||||
|   val supportsVideoHdr = getHasVideoHdr() | ||||
|   val autoFocusSystem = getAutoFocusSystemMode() | ||||
|   } | ||||
|   val supportsPhotoHdr by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_HDR) } | ||||
|   val supportsVideoHdr by lazy { getHasVideoHdr() } | ||||
|   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 photoFormat = ImageFormat.JPEG | ||||
|  | ||||
|   // get extensions (HDR, Night Mode, ..) | ||||
|   private fun getSupportedExtensions(): List<Int> = | ||||
| @@ -89,6 +158,13 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|       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 { | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||||
|       if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) { | ||||
| @@ -102,10 +178,19 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|   private fun getMinFocusDistanceCm(): Double { | ||||
|     val distance = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE) | ||||
|     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 | ||||
|     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 { | ||||
|     val array = Arguments.createArray() | ||||
|     digitalStabilizationModes.forEach { videoStabilizationMode -> | ||||
| @@ -146,6 +231,9 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|   } | ||||
|  | ||||
|   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 fovRadians = 2.0 * atan2(sensorDiagonal, (2.0 * focalLength)) | ||||
|     return Math.toDegrees(fovRadians) | ||||
| @@ -156,18 +244,31 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|     return getFieldOfView(smallestFocalLength) | ||||
|   } | ||||
|  | ||||
|   private fun getVideoSizes(): List<Size> = characteristics.getVideoSizes(cameraId, videoFormat) | ||||
|   private fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(ImageFormat.JPEG) | ||||
|   fun getVideoSizes(format: Int): List<Size> = characteristics.getVideoSizes(cameraId, format) | ||||
|   fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(photoFormat) | ||||
|   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 { | ||||
|     val array = Arguments.createArray() | ||||
|  | ||||
|     val videoSizes = getVideoSizes() | ||||
|     val videoSizes = getVideoSizes(videoFormat) | ||||
|     val photoSizes = getPhotoSizes() | ||||
|  | ||||
|     videoSizes.forEach { videoSize -> | ||||
|       val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize) | ||||
|       val maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt() | ||||
|       var 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 -> | ||||
|         val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps)) | ||||
| @@ -175,8 +276,6 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // TODO: Add high-speed video ranges (high-fps / slow-motion) | ||||
|  | ||||
|     return array | ||||
|   } | ||||
|  | ||||
| @@ -223,14 +322,14 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String | ||||
|     map.putBoolean("isMultiCam", isMultiCam) | ||||
|     map.putBoolean("supportsRawCapture", supportsRawCapture) | ||||
|     map.putBoolean("supportsLowLightBoost", supportsLowLightBoost) | ||||
|     map.putBoolean("supportsFocus", true) // I believe every device here supports focussing | ||||
|     map.putBoolean("supportsFocus", supportsFocusRegions) | ||||
|     map.putDouble("minZoom", minZoom) | ||||
|     map.putDouble("maxZoom", maxZoom) | ||||
|     map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android | ||||
|     map.putDouble("minExposure", exposureRange.lower.toDouble()) | ||||
|     map.putDouble("maxExposure", exposureRange.upper.toDouble()) | ||||
|     map.putString("hardwareLevel", hardwareLevel.unionValue) | ||||
|     map.putString("sensorOrientation", Orientation.fromRotationDegrees(sensorOrientation).unionValue) | ||||
|     map.putString("sensorOrientation", sensorOrientation.unionValue) | ||||
|     map.putArray("formats", getFormats()) | ||||
|     return map | ||||
|   } | ||||
|   | ||||
| @@ -62,6 +62,8 @@ class FlashUnavailableError : | ||||
|     "flash-unavailable", | ||||
|     "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 : | ||||
|   CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!") | ||||
| @@ -71,6 +73,8 @@ class CameraSessionCannotBeConfiguredError(cameraId: String) : | ||||
|   CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!") | ||||
| class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) : | ||||
|   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) : | ||||
|   CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!") | ||||
| @@ -100,6 +104,8 @@ class PhotoNotEnabledError : | ||||
|   CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.") | ||||
| class CaptureAbortedError(wasImageCaptured: Boolean) : | ||||
|   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) : | ||||
|   CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured") | ||||
| class RecorderError(name: String, extra: Int) : | ||||
| @@ -113,6 +119,16 @@ class RecordingInProgressError : | ||||
|     "recording-in-progress", | ||||
|     "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) : | ||||
|   CameraError( | ||||
|   | ||||
| @@ -5,50 +5,33 @@ import android.content.Context | ||||
| import android.content.pm.PackageManager | ||||
| import android.graphics.ImageFormat | ||||
| import android.graphics.Point | ||||
| import android.hardware.camera2.CameraCaptureSession | ||||
| import android.hardware.camera2.CameraCharacteristics | ||||
| import android.hardware.camera2.CameraDevice | ||||
| import android.hardware.camera2.CameraManager | ||||
| import android.hardware.camera2.CameraMetadata | ||||
| import android.hardware.camera2.CaptureRequest | ||||
| import android.hardware.camera2.CaptureResult | ||||
| import android.hardware.camera2.TotalCaptureResult | ||||
| import android.hardware.camera2.params.MeteringRectangle | ||||
| import android.media.Image | ||||
| import android.media.ImageReader | ||||
| import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Range | ||||
| import android.util.Size | ||||
| import android.view.Surface | ||||
| import android.view.SurfaceHolder | ||||
| import androidx.core.content.ContextCompat | ||||
| 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.PhotoOutput | ||||
| import com.mrousavy.camera.core.outputs.SurfaceOutput | ||||
| import com.mrousavy.camera.core.outputs.VideoPipelineOutput | ||||
| import com.mrousavy.camera.extensions.capture | ||||
| 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.FrameProcessor | ||||
| import com.mrousavy.camera.types.Flash | ||||
| import com.mrousavy.camera.types.LensFacing | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.QualityPrioritization | ||||
| 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 java.io.Closeable | ||||
| import java.lang.IllegalStateException | ||||
| import java.util.concurrent.CancellationException | ||||
| import kotlin.coroutines.cancellation.CancellationException | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| @@ -57,8 +40,8 @@ import kotlinx.coroutines.sync.withLock | ||||
| import java.io.File | ||||
|  | ||||
| class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) : | ||||
|   CameraManager.AvailabilityCallback(), | ||||
|   Closeable { | ||||
|   Closeable, | ||||
|   PersistentCameraCaptureSession.Callback { | ||||
|   companion object { | ||||
|     private const val TAG = "CameraSession" | ||||
|   } | ||||
| @@ -67,14 +50,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|   private var configuration: CameraConfiguration? = null | ||||
|  | ||||
|   // Camera State | ||||
|   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 val captureSession = PersistentCameraCaptureSession(cameraManager, this) | ||||
|   private var photoOutput: PhotoOutput? = null | ||||
|   private var videoOutput: VideoPipelineOutput? = null | ||||
|   private var codeScannerOutput: BarcodeScannerOutput? = null | ||||
| @@ -102,11 +78,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|       field = value | ||||
|       updateVideoOutputs() | ||||
|     } | ||||
|   var frameProcessor: FrameProcessor? = null | ||||
|     set(value) { | ||||
|       field = value | ||||
|       updateVideoOutputs() | ||||
|     } | ||||
|  | ||||
|   val orientation: Orientation | ||||
|     get() { | ||||
| @@ -116,14 +87,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|       return Orientation.fromRotationDegrees(sensorRotation) | ||||
|     } | ||||
|  | ||||
|   init { | ||||
|     cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler) | ||||
|   } | ||||
|  | ||||
|   override fun close() { | ||||
|     Log.i(TAG, "Closing CameraSession...") | ||||
|     isDestroyed = true | ||||
|     cameraManager.unregisterAvailabilityCallback(this) | ||||
|     runBlocking { | ||||
|       mutex.withLock { | ||||
|         destroy() | ||||
| @@ -133,18 +99,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     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) { | ||||
|     Log.i(TAG, "configure { ... }: Waiting for lock...") | ||||
|  | ||||
| @@ -153,6 +107,12 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|       val config = CameraConfiguration.copyOf(this.configuration) | ||||
|       lambda(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) { | ||||
|         Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }") | ||||
| @@ -162,29 +122,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|       Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff") | ||||
|  | ||||
|       try { | ||||
|         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 | ||||
|         captureSession.withConfiguration { | ||||
|           // Build up session or update any props | ||||
|           if (diff.deviceChanged) { | ||||
|             // 1. cameraId changed, open device | ||||
|             configureCameraDevice(config) | ||||
|             configureInput(config) | ||||
|           } | ||||
|           if (diff.outputsChanged) { | ||||
|             // 2. outputs changed, build new session | ||||
| @@ -194,10 +136,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|             // 3. zoom etc changed, update repeating request | ||||
|             configureCaptureRequest(config) | ||||
|           } | ||||
|           if (diff.isActiveChanged) { | ||||
|             // 4. Either start or stop the session | ||||
|             val isActive = config.isActive && config.preview.isEnabled | ||||
|             captureSession.setIsActive(isActive) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         Log.i(TAG, "Successfully updated CameraSession Configuration! isActive: ${config.isActive}") | ||||
|         this.configuration = config | ||||
|         Log.i( | ||||
|           TAG, | ||||
|           "configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})" | ||||
|         ) | ||||
|         isRunning = captureSession.isRunning | ||||
|  | ||||
|         // Notify about Camera initialization | ||||
|         if (diff.deviceChanged) { | ||||
| @@ -212,8 +162,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|  | ||||
|   private fun destroy() { | ||||
|     Log.i(TAG, "Destroying session..") | ||||
|     cameraDevice?.close() | ||||
|     cameraDevice = null | ||||
|     captureSession.close() | ||||
|  | ||||
|     photoOutput?.close() | ||||
|     photoOutput = null | ||||
| @@ -269,66 +218,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     Log.i(TAG, "Preview Output destroyed!") | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set up the `CameraDevice` (`cameraId`) | ||||
|    */ | ||||
|   private suspend fun configureCameraDevice(configuration: CameraConfiguration) { | ||||
|     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 | ||||
|  | ||||
|     // Check Camera Permission | ||||
|     val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) | ||||
|     if (cameraPermission != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError() | ||||
|  | ||||
|     // Open new device | ||||
|   private fun configureInput(configuration: CameraConfiguration) { | ||||
|     Log.i(TAG, "Configuring inputs for CameraSession...") | ||||
|     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!") | ||||
|     val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) | ||||
|     if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError() | ||||
|     isRunning = false | ||||
|     captureSession.setInput(cameraId) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings. | ||||
|    */ | ||||
|   private suspend fun configureOutputs(configuration: CameraConfiguration) { | ||||
|     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 | ||||
|     } | ||||
|     val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() | ||||
|  | ||||
|     // Destroy previous outputs | ||||
|     Log.i(TAG, "Destroying previous outputs...") | ||||
| @@ -340,20 +243,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     codeScannerOutput = null | ||||
|     isRunning = false | ||||
|  | ||||
|     val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id) | ||||
|     val deviceDetails = CameraDeviceDetails(cameraManager, cameraId) | ||||
|     val format = configuration.format | ||||
|  | ||||
|     Log.i(TAG, "Creating outputs for Camera #${cameraDevice.id}...") | ||||
|     Log.i(TAG, "Creating outputs for Camera #$cameraId...") | ||||
|  | ||||
|     val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT | ||||
|     val isSelfie = deviceDetails.lensFacing == LensFacing.FRONT | ||||
|  | ||||
|     val outputs = mutableListOf<SurfaceOutput>() | ||||
|  | ||||
|     // Photo Output | ||||
|     val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo> | ||||
|     if (photo != null) { | ||||
|       val imageFormat = ImageFormat.JPEG | ||||
|       val sizes = characteristics.getPhotoSizes(imageFormat) | ||||
|       val imageFormat = deviceDetails.photoFormat | ||||
|       val sizes = deviceDetails.getPhotoSizes() | ||||
|       val size = sizes.closestToOrMax(format?.photoSize) | ||||
|       val maxImages = 10 | ||||
|  | ||||
| @@ -373,7 +276,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video> | ||||
|     if (video != null) { | ||||
|       val imageFormat = video.config.pixelFormat.toImageFormat() | ||||
|       val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat) | ||||
|       val sizes = deviceDetails.getVideoSizes(imageFormat) | ||||
|       val size = sizes.closestToOrMax(format?.videoSize) | ||||
|  | ||||
|       Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") | ||||
| @@ -383,6 +286,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|         video.config.pixelFormat, | ||||
|         isSelfie, | ||||
|         video.config.enableFrameProcessor, | ||||
|         video.config.enableGpuBuffers, | ||||
|         callback | ||||
|       ) | ||||
|       val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr) | ||||
| @@ -395,7 +299,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     if (preview != null) { | ||||
|       // Compute Preview Size based on chosen video size | ||||
|       val videoSize = videoOutput?.size ?: format?.videoSize | ||||
|       val size = characteristics.getPreviewTargetSize(videoSize) | ||||
|       val sizes = deviceDetails.getPreviewSizes() | ||||
|       val size = sizes.closestToOrMax(videoSize) | ||||
|  | ||||
|       val enableHdr = video?.config?.enableHdr ?: false | ||||
|  | ||||
| @@ -407,8 +312,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|         enableHdr | ||||
|       ) | ||||
|       outputs.add(output) | ||||
|       // Size is usually landscape, so we flip it here | ||||
|       previewView?.size = Size(size.height, size.width) | ||||
|  | ||||
|       previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation) | ||||
|     } | ||||
|  | ||||
|     // CodeScanner Output | ||||
| @@ -421,7 +326,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|       } | ||||
|  | ||||
|       val imageFormat = ImageFormat.YUV_420_888 | ||||
|       val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat) | ||||
|       val sizes = deviceDetails.getVideoSizes(imageFormat) | ||||
|       val size = sizes.closestToOrMax(Size(1280, 720)) | ||||
|  | ||||
|       Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") | ||||
| @@ -432,175 +337,63 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     } | ||||
|  | ||||
|     // Create session | ||||
|     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 | ||||
|       } | ||||
|     captureSession.setOutputs(outputs) | ||||
|  | ||||
|       // 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}!") | ||||
|     Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!") | ||||
|  | ||||
|     // Update Frame Processor and RecordingSession for newly changed output | ||||
|     updateVideoOutputs() | ||||
|   } | ||||
|  | ||||
|   private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest { | ||||
|     val deviceDetails = cameraDeviceDetails ?: CameraDeviceDetails(cameraManager, device.id) | ||||
|  | ||||
|     val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW | ||||
|     val captureRequest = device.createCaptureRequest(template) | ||||
|  | ||||
|     targets.forEach { t -> captureRequest.addTarget(t) } | ||||
|  | ||||
|     val format = config.format | ||||
|  | ||||
|     // Set FPS | ||||
|     val fps = config.fps | ||||
|     if (fps != null) { | ||||
|       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 | ||||
|     val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video> | ||||
|     val enableVideo = video != null | ||||
|     val enableVideoHdr = video?.config?.enableHdr == true | ||||
|  | ||||
|     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 | ||||
|     captureSession.setRepeatingRequest( | ||||
|       RepeatingCaptureRequest( | ||||
|         enableVideo, | ||||
|         config.torch, | ||||
|         config.fps, | ||||
|         config.videoStabilizationMode, | ||||
|         enableVideoHdr, | ||||
|         config.enableLowLightBoost, | ||||
|         config.exposure, | ||||
|         config.zoom, | ||||
|         config.format | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   suspend fun takePhoto( | ||||
|     qualityPrioritization: QualityPrioritization, | ||||
|     flashMode: Flash, | ||||
|     flash: Flash, | ||||
|     enableShutterSound: Boolean, | ||||
|     enableRedEyeReduction: Boolean, | ||||
|     enableAutoStabilization: Boolean, | ||||
|     enablePrecapture: Boolean, | ||||
|     outputOrientation: Orientation | ||||
|   ): CapturedPhoto { | ||||
|     val captureSession = captureSession ?: throw CameraNotReadyError() | ||||
|     val photoOutput = photoOutput ?: throw PhotoNotEnabledError() | ||||
|  | ||||
|     Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...") | ||||
|  | ||||
|     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, | ||||
|     Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...") | ||||
|     val result = captureSession.capture( | ||||
|       qualityPrioritization, | ||||
|       flashMode, | ||||
|       enableRedEyeReduction, | ||||
|       flash, | ||||
|       enableAutoStabilization, | ||||
|       photoOutput.enableHdr, | ||||
|       orientation | ||||
|       outputOrientation, | ||||
|       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 { | ||||
|       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 isMirrored = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT | ||||
|  | ||||
|       Log.i(TAG, "Photo capture 3/3 complete - received ${image.width} x ${image.height} image.") | ||||
|       Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...") | ||||
|       val deviceDetails = captureSession.getActiveDeviceDetails() | ||||
|       val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT | ||||
|       return CapturedPhoto(image, result, orientation, isMirrored, image.format) | ||||
|     } catch (e: CancellationException) { | ||||
|       throw CaptureAbortedError(false) | ||||
| @@ -628,13 +421,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     mutex.withLock { | ||||
|       if (recording != null) throw RecordingInProgressError() | ||||
|       val videoOutput = videoOutput ?: throw VideoNotEnabledError() | ||||
|       val cameraDevice = cameraDevice ?: throw CameraNotReadyError() | ||||
|       val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError() | ||||
|  | ||||
|       val fps = configuration?.fps ?: 30 | ||||
|  | ||||
|       val recording = RecordingSession( | ||||
|         context, | ||||
|         cameraDevice.id, | ||||
|         cameraId, | ||||
|         videoOutput.size, | ||||
|         enableAudio, | ||||
|         fps, | ||||
| @@ -674,40 +467,16 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!") | ||||
|   override fun onError(error: Throwable) { | ||||
|     callback.onError(error) | ||||
|   } | ||||
|  | ||||
|   private suspend fun focus(point: Point) { | ||||
|     mutex.withLock { | ||||
|       // TODO: Fix this method | ||||
|       val captureSession = captureSession ?: throw CameraNotReadyError() | ||||
|       val request = previewRequest ?: throw CameraNotReadyError() | ||||
|   suspend fun focus(x: Int, y: Int) { | ||||
|     val previewView = previewView ?: throw CameraNotReadyError() | ||||
|     val deviceDetails = captureSession.getActiveDeviceDetails() ?: throw CameraNotReadyError() | ||||
|  | ||||
|       val weight = MeteringRectangle.METERING_WEIGHT_MAX - 1 | ||||
|       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) | ||||
|     } | ||||
|     val cameraPoint = previewView.convertLayerPointToCameraCoordinates(Point(x, y), deviceDetails) | ||||
|     captureSession.focus(cameraPoint) | ||||
|   } | ||||
|  | ||||
|   data class CapturedPhoto( | ||||
|   | ||||
| @@ -83,7 +83,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu | ||||
|   } | ||||
|  | ||||
|   // Muxer specific | ||||
|   private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat) { | ||||
|   private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat, val callbacks: CameraSession.Callback,) { | ||||
|     val videoTrack: Int = muxer.addTrack(encodedFormat) | ||||
|     val startTimeUs: Long = startTimeUs | ||||
|  | ||||
| @@ -95,16 +95,14 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu | ||||
|     fun finish() { | ||||
|       muxer.stop() | ||||
|       muxer.release() | ||||
|       callbacks.onVideoChunkReady(filepath, chunkIndex) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private var muxerContext: MuxerContext? = null | ||||
|  | ||||
|   private fun createNextMuxer(bufferInfo: BufferInfo) { | ||||
|     muxerContext?.let { | ||||
|       it.finish() | ||||
|       this.callbacks.onVideoChunkReady(it.filepath, it.chunkIndex) | ||||
|     } | ||||
|     muxerContext?.finish() | ||||
|     chunkIndex++ | ||||
|  | ||||
|     val newFileName = "$chunkIndex.mp4" | ||||
| @@ -116,7 +114,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu | ||||
|     ) | ||||
|     muxer.setOrientationHint(orientationHint) | ||||
|     muxerContext = MuxerContext( | ||||
|       muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!! | ||||
|         muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!, this.callbacks | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class CodeScannerPipeline( | ||||
|     var isBusy = false | ||||
|     imageReader = ImageReader.newInstance(size.width, size.height, format, MAX_IMAGES) | ||||
|     imageReader.setOnImageAvailableListener({ reader -> | ||||
|       val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener | ||||
|       val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener | ||||
|  | ||||
|       if (isBusy) { | ||||
|         // We're currently executing on a previous Frame, so we skip this one. | ||||
|   | ||||
| @@ -0,0 +1,378 @@ | ||||
| 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) | ||||
| } | ||||
| @@ -2,51 +2,207 @@ package com.mrousavy.camera.core | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.res.Configuration | ||||
| import android.graphics.Point | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.util.Log | ||||
| import android.util.Size | ||||
| import android.view.Gravity | ||||
| import android.view.PixelCopy | ||||
| import android.view.SurfaceHolder | ||||
| import android.view.SurfaceView | ||||
| import android.widget.FrameLayout | ||||
| import com.facebook.react.bridge.UiThreadUtil | ||||
| import com.mrousavy.camera.extensions.getMaximumPreviewSize | ||||
| import com.mrousavy.camera.extensions.resize | ||||
| import com.mrousavy.camera.extensions.rotatedBy | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.ResizeMode | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
| import kotlin.math.roundToInt | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||||
| import kotlinx.coroutines.withContext | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.Matrix | ||||
|  | ||||
| fun rotateBitmap90CounterClockwise(source: Bitmap): Bitmap { | ||||
|     val width = source.width | ||||
|     val height = source.height | ||||
|  | ||||
|     // Create a new Bitmap with swapped width and height | ||||
|     val rotatedBitmap = Bitmap.createBitmap(height, width, source.config ?: Bitmap.Config.ARGB_8888) | ||||
|  | ||||
|     for (y in 0 until height) { | ||||
|         for (x in 0 until width) { | ||||
|             // Set the pixel in the new position | ||||
|             rotatedBitmap.setPixel(y, width - 1 - x, source.getPixel(x, y)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return rotatedBitmap | ||||
| } | ||||
|  | ||||
|  | ||||
| fun Bitmap.transformBitmap(orientation: Orientation): Bitmap { | ||||
|     return when (orientation) { | ||||
|         Orientation.PORTRAIT -> this // No transformation needed | ||||
|         Orientation.LANDSCAPE_LEFT -> { | ||||
|             // Transpose (swap width and height) | ||||
|             val transposedBitmap = Bitmap.createBitmap(height, width, config ?: Bitmap.Config.ARGB_8888) | ||||
|             for (y in 0 until height) { | ||||
|                 for (x in 0 until width) { | ||||
|                     transposedBitmap.setPixel(y, width - 1 - x, getPixel(x, y)) | ||||
|                 } | ||||
|             } | ||||
|             transposedBitmap | ||||
|         } | ||||
|         Orientation.PORTRAIT_UPSIDE_DOWN -> { | ||||
|             // Invert vertically and horizontally (180-degree rotation) | ||||
|             val invertedBitmap = Bitmap.createBitmap(width, height, config ?: Bitmap.Config.ARGB_8888) | ||||
|             for (y in 0 until height) { | ||||
|                 for (x in 0 until width) { | ||||
|                     invertedBitmap.setPixel(width - 1 - x, height - 1 - y, getPixel(x, y)) | ||||
|                 } | ||||
|             } | ||||
|             invertedBitmap | ||||
|         } | ||||
|         Orientation.LANDSCAPE_RIGHT -> { | ||||
|             // Transpose (swap width and height) and invert vertically | ||||
|             val transposedBitmap = Bitmap.createBitmap(height, width, config ?: Bitmap.Config.ARGB_8888) | ||||
|             for (y in 0 until height) { | ||||
|                 for (x in 0 until width) { | ||||
|                     transposedBitmap.setPixel(height - 1 - y, x, getPixel(x, y)) | ||||
|                 } | ||||
|             } | ||||
|             transposedBitmap | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @SuppressLint("ViewConstructor") | ||||
| class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) { | ||||
|   var size: Size = getMaximumPreviewSize() | ||||
| class PreviewView(context: Context, callback: SurfaceHolder.Callback) : | ||||
|   SurfaceView(context), | ||||
|   SurfaceHolder.Callback { | ||||
|   var size: Size = CameraDeviceDetails.getMaximumPreviewSize() | ||||
|     set(value) { | ||||
|       field = value | ||||
|       UiThreadUtil.runOnUiThread { | ||||
|         Log.i(TAG, "Setting PreviewView Surface Size to $width x $height...") | ||||
|         holder.setFixedSize(value.height, value.width) | ||||
|         requestLayout() | ||||
|         invalidate() | ||||
|       if (field != value) { | ||||
|         Log.i(TAG, "Surface Size changed: $field -> $value") | ||||
|         field = value | ||||
|         updateLayout() | ||||
|       } | ||||
|     } | ||||
|   var resizeMode: ResizeMode = ResizeMode.COVER | ||||
|     set(value) { | ||||
|       field = value | ||||
|       UiThreadUtil.runOnUiThread { | ||||
|         requestLayout() | ||||
|         invalidate() | ||||
|       if (field != value) { | ||||
|         Log.i(TAG, "Resize Mode changed: $field -> $value") | ||||
|         field = value | ||||
|         updateLayout() | ||||
|       } | ||||
|     } | ||||
|   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 { | ||||
|     Log.i(TAG, "Creating PreviewView...") | ||||
|     layoutParams = FrameLayout.LayoutParams( | ||||
|       FrameLayout.LayoutParams.MATCH_PARENT, | ||||
|       FrameLayout.LayoutParams.MATCH_PARENT, | ||||
|       Gravity.CENTER | ||||
|     ) | ||||
|     holder.setKeepScreenOn(true) | ||||
|     holder.addCallback(this) | ||||
|     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) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   suspend fun getBitmap(): Bitmap? = withContext(Dispatchers.Main) { | ||||
|     val frame = holder.getSurfaceFrame() | ||||
|  | ||||
|     val width = frame.width() | ||||
|     val height = frame.height() | ||||
|  | ||||
|     val bitmap = Bitmap.createBitmap(height, width, Bitmap.Config.ARGB_8888) | ||||
|  | ||||
|     // Use a coroutine to suspend until the PixelCopy request is complete | ||||
|     suspendCancellableCoroutine<Bitmap?> { continuation -> | ||||
|     PixelCopy.request( | ||||
|         holder.surface, | ||||
|         bitmap, | ||||
|         { copyResult -> | ||||
|           if (copyResult == PixelCopy.SUCCESS) { | ||||
|             continuation.resume(rotateBitmap90CounterClockwise(bitmap)) | ||||
|           } else { | ||||
|             continuation.resumeWithException( | ||||
|                 RuntimeException("PixelCopy failed with error code $copyResult") | ||||
|             ) | ||||
|           } | ||||
|         }, | ||||
|         Handler(Looper.getMainLooper()) | ||||
|     ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   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 { | ||||
|     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 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) { | ||||
|       ResizeMode.COVER -> contentAspectRatio > containerAspectRatio | ||||
| @@ -68,10 +224,11 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV | ||||
|   override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { | ||||
|     super.onMeasure(widthMeasureSpec, heightMeasureSpec) | ||||
|  | ||||
|     val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) | ||||
|     val fittedSize = getSize(size, viewSize, resizeMode) | ||||
|     val measuredViewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) | ||||
|     val surfaceSize = size.rotatedBy(inputOrientation) | ||||
|     val fittedSize = getSize(surfaceSize, measuredViewSize, resizeMode) | ||||
|  | ||||
|     Log.i(TAG, "PreviewView is $viewSize, rendering $size content. Resizing to: $fittedSize ($resizeMode)") | ||||
|     Log.i(TAG, "PreviewView is $measuredViewSize rendering $surfaceSize orientation ($orientation). Resizing to: $fittedSize ($resizeMode)") | ||||
|     setMeasuredDimension(fittedSize.width, fittedSize.height) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,7 @@ class VideoPipeline( | ||||
|   val format: PixelFormat = PixelFormat.NATIVE, | ||||
|   private val isMirrored: Boolean = false, | ||||
|   private val enableFrameProcessor: Boolean = false, | ||||
|   enableGpuBuffers: Boolean = false, | ||||
|   private val callback: CameraSession.Callback | ||||
| ) : SurfaceTexture.OnFrameAvailableListener, | ||||
|   Closeable { | ||||
| @@ -79,17 +80,25 @@ class VideoPipeline( | ||||
|       val format = getImageReaderFormat() | ||||
|       Log.i(TAG, "Using ImageReader round-trip (format: #$format)") | ||||
|  | ||||
|       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||||
|         Log.i(TAG, "Using API 29 for GPU ImageReader...") | ||||
|       // Create ImageReader | ||||
|       if (enableGpuBuffers && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||||
|         val usageFlags = getRecommendedHardwareBufferFlags() | ||||
|         Log.i(TAG, "Using ImageReader flags: $usageFlags") | ||||
|         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) { | ||||
|         Log.i(TAG, "Creating ImageWriter with format #$format...") | ||||
|         imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES, format) | ||||
|       } else { | ||||
|         Log.i(TAG, "Using legacy API for CPU ImageReader...") | ||||
|         imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES) | ||||
|         Log.i(TAG, "Creating ImageWriter with default format...") | ||||
|         imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES) | ||||
|       } | ||||
|  | ||||
|       imageReader!!.setOnImageAvailableListener({ reader -> | ||||
|         // Log.i(TAG, "ImageReader::onImageAvailable!")s | ||||
|         val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener | ||||
| @@ -107,7 +116,7 @@ class VideoPipeline( | ||||
|           } | ||||
|         } catch (e: Throwable) { | ||||
|           Log.e(TAG, "FrameProcessor/ImageReader pipeline threw an error!", e) | ||||
|           throw e | ||||
|           callback.onError(e) | ||||
|         } finally { | ||||
|           frame.decrementRefCount() | ||||
|         } | ||||
| @@ -125,8 +134,11 @@ class VideoPipeline( | ||||
|       isActive = false | ||||
|       imageWriter?.close() | ||||
|       imageReader?.close() | ||||
|       removeRecordingSessionOutputSurface() | ||||
|       recordingSession = null | ||||
|       surfaceTexture.setOnFrameAvailableListener(null, null) | ||||
|       surfaceTexture.release() | ||||
|       surface.release() | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -173,7 +185,7 @@ class VideoPipeline( | ||||
|     synchronized(this) { | ||||
|       if (recordingSession != null) { | ||||
|         // Configure OpenGL pipeline to stream Frames into the Recording Session's surface | ||||
|         Log.i(TAG, "Setting $width x $height RecordingSession Output...") | ||||
|         Log.i(TAG, "Setting ${recordingSession.size} RecordingSession Output...") | ||||
|         setRecordingSessionOutputSurface(recordingSession.surface) | ||||
|         this.recordingSession = recordingSession | ||||
|       } else { | ||||
| @@ -228,7 +240,11 @@ class VideoPipeline( | ||||
|   @RequiresApi(Build.VERSION_CODES.Q) | ||||
|   private fun supportsHardwareBufferFlags(flags: Long): Boolean { | ||||
|     val hardwareBufferFormat = format.toHardwareBufferFormat() | ||||
|     return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) | ||||
|     try { | ||||
|       return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) | ||||
|     } catch (_: Throwable) { | ||||
|       return false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private external fun getInputTextureId(): Int | ||||
|   | ||||
| @@ -0,0 +1,88 @@ | ||||
| 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 | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,149 @@ | ||||
| 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 | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,113 @@ | ||||
| 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,13 +10,7 @@ import android.view.Surface | ||||
| import androidx.annotation.RequiresApi | ||||
| import java.io.Closeable | ||||
|  | ||||
| open class SurfaceOutput( | ||||
|   val surface: Surface, | ||||
|   val size: Size, | ||||
|   val outputType: OutputType, | ||||
|   val enableHdr: Boolean = false, | ||||
|   private val closeSurfaceOnEnd: Boolean = false | ||||
| ) : Closeable { | ||||
| open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: OutputType, val enableHdr: Boolean = false) : Closeable { | ||||
|   companion object { | ||||
|     const val TAG = "SurfaceOutput" | ||||
|  | ||||
| @@ -52,12 +46,18 @@ open class SurfaceOutput( | ||||
|     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 close() { | ||||
|     if (closeSurfaceOnEnd) { | ||||
|       surface.release() | ||||
|     } | ||||
|     // close() does nothing by default | ||||
|   } | ||||
|  | ||||
|   enum class OutputType { | ||||
|   | ||||
| @@ -5,47 +5,70 @@ import android.hardware.camera2.CaptureFailure | ||||
| import android.hardware.camera2.CaptureRequest | ||||
| import android.hardware.camera2.TotalCaptureResult | ||||
| import android.media.MediaActionSound | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import android.util.Log | ||||
| import com.mrousavy.camera.core.CaptureAbortedError | ||||
| import com.mrousavy.camera.core.CaptureTimedOutError | ||||
| import com.mrousavy.camera.core.UnknownCaptureError | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
| 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" | ||||
|  | ||||
| suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult = | ||||
|   suspendCoroutine { continuation -> | ||||
|   suspendCancellableCoroutine { continuation -> | ||||
|     val shutterSound = if (enableShutterSound) MediaActionSound() else null | ||||
|     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( | ||||
|       captureRequest, | ||||
|       object : CameraCaptureSession.CaptureCallback() { | ||||
|         override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { | ||||
|           super.onCaptureCompleted(session, request, result) | ||||
|  | ||||
|           continuation.resume(result) | ||||
|           shutterSound?.release() | ||||
|           if (request == captureRequest) { | ||||
|             continuation.resume(result) | ||||
|             shutterSound?.release() | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) { | ||||
|           super.onCaptureStarted(session, request, timestamp, frameNumber) | ||||
|  | ||||
|           if (enableShutterSound) { | ||||
|             shutterSound?.play(MediaActionSound.SHUTTER_CLICK) | ||||
|           if (request == captureRequest) { | ||||
|             if (enableShutterSound) { | ||||
|               shutterSound?.play(MediaActionSound.SHUTTER_CLICK) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { | ||||
|           super.onCaptureFailed(session, request, failure) | ||||
|           val wasImageCaptured = failure.wasImageCaptured() | ||||
|           val error = when (failure.reason) { | ||||
|             CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) | ||||
|             CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) | ||||
|             else -> UnknownCaptureError(wasImageCaptured) | ||||
|  | ||||
|           if (request == captureRequest) { | ||||
|             val wasImageCaptured = failure.wasImageCaptured() | ||||
|             val error = when (failure.reason) { | ||||
|               CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) | ||||
|               CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) | ||||
|               else -> UnknownCaptureError(wasImageCaptured) | ||||
|             } | ||||
|             continuation.resumeWithException(error) | ||||
|           } | ||||
|           continuation.resumeWithException(error) | ||||
|         } | ||||
|       }, | ||||
|       CameraQueues.cameraQueue.handler | ||||
|       null | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -0,0 +1,151 @@ | ||||
| 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) | ||||
| } | ||||
| @@ -0,0 +1,193 @@ | ||||
| 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 | ||||
|     ) | ||||
|   } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package com.mrousavy.camera.extensions | ||||
|  | ||||
| import android.hardware.camera2.CameraCaptureSession | ||||
|  | ||||
| fun CameraCaptureSession.tryAbortCaptures() { | ||||
|   try { | ||||
|     abortCaptures() | ||||
|   } catch (_: Throwable) {} | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package com.mrousavy.camera.extensions | ||||
|  | ||||
| import android.hardware.camera2.CameraCaptureSession | ||||
|  | ||||
| fun CameraCaptureSession.tryStopRepeating() { | ||||
|   try { | ||||
|     stopRepeating() | ||||
|   } catch (_: Throwable) {} | ||||
| } | ||||
| @@ -1,39 +1,13 @@ | ||||
| package com.mrousavy.camera.extensions | ||||
|  | ||||
| import android.hardware.camera2.CameraCharacteristics | ||||
| import android.media.CamcorderProfile | ||||
| import android.os.Build | ||||
| import android.util.Size | ||||
|  | ||||
| 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 | ||||
|   } | ||||
| } | ||||
| import com.mrousavy.camera.utils.CamcorderProfileUtils | ||||
|  | ||||
| fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> { | ||||
|   val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! | ||||
|   val sizes = config.getOutputSizes(format) ?: emptyArray() | ||||
|   val maxVideoSize = getMaximumVideoSize(cameraId) | ||||
|   val maxVideoSize = CamcorderProfileUtils.getMaximumVideoSize(cameraId) | ||||
|   if (maxVideoSize != null) { | ||||
|     return sizes.filter { it.bigger <= maxVideoSize.bigger } | ||||
|   } | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| 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) | ||||
| } | ||||
| @@ -1,104 +0,0 @@ | ||||
| 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() | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| 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,20 +1,18 @@ | ||||
| package com.mrousavy.camera.extensions | ||||
|  | ||||
| import android.hardware.camera2.CameraCharacteristics | ||||
| import android.hardware.camera2.CaptureRequest | ||||
| import android.os.Build | ||||
| import android.util.Range | ||||
| import com.mrousavy.camera.core.CameraDeviceDetails | ||||
| import com.mrousavy.camera.types.HardwareLevel | ||||
|  | ||||
| fun CaptureRequest.Builder.setZoom(zoom: Float, cameraCharacteristics: CameraCharacteristics) { | ||||
|   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||||
|     val zoomRange = cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) ?: Range(1f, 1f) | ||||
|     val zoomClamped = zoomRange.clamp(zoom) | ||||
| fun CaptureRequest.Builder.setZoom(zoom: Float, deviceDetails: CameraDeviceDetails) { | ||||
|   val zoomRange = deviceDetails.zoomRange | ||||
|   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) | ||||
|   } else { | ||||
|     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) | ||||
|     val size = deviceDetails.activeSize | ||||
|     this.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoomClamped)) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| 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.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Size | ||||
| import com.mrousavy.camera.core.RecordingSession | ||||
| import com.mrousavy.camera.types.VideoCodec | ||||
| import com.mrousavy.camera.utils.CamcorderProfileUtils | ||||
| import kotlin.math.abs | ||||
|  | ||||
| data class RecommendedProfile( | ||||
| @@ -23,7 +23,7 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo | ||||
|   val targetResolution = size | ||||
|   val encoder = codec.toVideoEncoder() | ||||
|   val bitDepth = if (hdr) 10 else 8 | ||||
|   val quality = findClosestCamcorderProfileQuality(cameraId, targetResolution) | ||||
|   val quality = CamcorderProfileUtils.findClosestCamcorderProfileQuality(cameraId, targetResolution, true) | ||||
|   Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality") | ||||
|  | ||||
|   var recommendedProfile: RecommendedProfile? = null | ||||
| @@ -75,39 +75,3 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo | ||||
|   } | ||||
|   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.SizeF | ||||
| import android.view.Surface | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.max | ||||
| import kotlin.math.min | ||||
| @@ -14,13 +14,10 @@ fun List<Size>.closestToOrMax(size: Size?): Size = | ||||
|     this.maxBy { it.width * it.height } | ||||
|   } | ||||
|  | ||||
| fun Size.rotated(surfaceRotation: Int): Size = | ||||
|   when (surfaceRotation) { | ||||
|     Surface.ROTATION_0 -> Size(width, height) | ||||
|     Surface.ROTATION_90 -> Size(height, width) | ||||
|     Surface.ROTATION_180 -> Size(width, height) | ||||
|     Surface.ROTATION_270 -> Size(height, width) | ||||
|     else -> Size(width, height) | ||||
| fun Size.rotatedBy(orientation: Orientation): Size = | ||||
|   when (orientation) { | ||||
|     Orientation.PORTRAIT, Orientation.PORTRAIT_UPSIDE_DOWN -> this | ||||
|     Orientation.LANDSCAPE_LEFT, Orientation.LANDSCAPE_RIGHT -> Size(height, width) | ||||
|   } | ||||
|  | ||||
| val Size.bigger: Int | ||||
|   | ||||
| @@ -0,0 +1,41 @@ | ||||
| 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,6 +4,7 @@ import android.hardware.HardwareBuffer; | ||||
| import android.media.Image; | ||||
| import android.os.Build; | ||||
| import com.facebook.proguard.annotations.DoNotStrip; | ||||
| import com.mrousavy.camera.core.FrameInvalidError; | ||||
| import com.mrousavy.camera.core.HardwareBuffersNotAvailableError; | ||||
| import com.mrousavy.camera.types.PixelFormat; | ||||
| import com.mrousavy.camera.types.Orientation; | ||||
| @@ -23,42 +24,17 @@ public class Frame { | ||||
|         this.isMirrored = isMirrored; | ||||
|     } | ||||
|  | ||||
|     public Image getImage() { | ||||
|         synchronized (this) { | ||||
|             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 void assertIsValid() throws FrameInvalidError { | ||||
|         if (!getIsImageValid(image)) { | ||||
|             throw new FrameInvalidError(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @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) { | ||||
|     private synchronized boolean getIsImageValid(Image image) { | ||||
|         if (refCount <= 0) return false; | ||||
|         try { | ||||
|             // will throw an exception if the image is already closed | ||||
|             synchronized (this) { image.getFormat(); } | ||||
|             image.getFormat(); | ||||
|             // no exception thrown, image must still be valid. | ||||
|             return true; | ||||
|         } catch (IllegalStateException e) { | ||||
| @@ -67,78 +43,104 @@ public class Frame { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public synchronized Image getImage() { | ||||
|         return image; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public boolean getIsMirrored() { | ||||
|     public synchronized int getWidth() throws FrameInvalidError { | ||||
|         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; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public long getTimestamp() { | ||||
|     public synchronized long getTimestamp() throws FrameInvalidError { | ||||
|         assertIsValid(); | ||||
|         return timestamp; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public Orientation getOrientation() { | ||||
|     public synchronized Orientation getOrientation() throws FrameInvalidError { | ||||
|         assertIsValid(); | ||||
|         return orientation; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public PixelFormat getPixelFormat() { | ||||
|         return PixelFormat.Companion.fromImageFormat(getImage().getFormat()); | ||||
|     public synchronized PixelFormat getPixelFormat() throws FrameInvalidError { | ||||
|         assertIsValid(); | ||||
|         return PixelFormat.Companion.fromImageFormat(image.getFormat()); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public int getPlanesCount() { | ||||
|         return getImage().getPlanes().length; | ||||
|     public synchronized int getPlanesCount() throws FrameInvalidError { | ||||
|         assertIsValid(); | ||||
|         return image.getPlanes().length; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public int getBytesPerRow() { | ||||
|         return getImage().getPlanes()[0].getRowStride(); | ||||
|     public synchronized int getBytesPerRow() throws FrameInvalidError { | ||||
|         assertIsValid(); | ||||
|         return image.getPlanes()[0].getRowStride(); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError { | ||||
|     private Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError, FrameInvalidError { | ||||
|         return getHardwareBuffer(); | ||||
|     } | ||||
|  | ||||
|     public HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError { | ||||
|     public synchronized HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError, FrameInvalidError { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { | ||||
|             throw new HardwareBuffersNotAvailableError(); | ||||
|         } | ||||
|         return getImage().getHardwareBuffer(); | ||||
|         assertIsValid(); | ||||
|         return image.getHardwareBuffer(); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public void incrementRefCount() { | ||||
|         synchronized (this) { | ||||
|             refCount++; | ||||
|         } | ||||
|     public synchronized void incrementRefCount() { | ||||
|         refCount++; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     @DoNotStrip | ||||
|     public void decrementRefCount() { | ||||
|         synchronized (this) { | ||||
|             refCount--; | ||||
|             if (refCount <= 0) { | ||||
|                 // If no reference is held on this Image, close it. | ||||
|                 close(); | ||||
|             } | ||||
|     public synchronized void decrementRefCount() { | ||||
|         refCount--; | ||||
|         if (refCount <= 0) { | ||||
|             // If no reference is held on this Image, close it. | ||||
|             close(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void close() { | ||||
|         synchronized (this) { | ||||
|             image.close(); | ||||
|         } | ||||
|     private synchronized void close() { | ||||
|         image.close(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,6 +21,8 @@ public final class FrameProcessor { | ||||
|     @Keep | ||||
|     private final HybridData mHybridData; | ||||
|  | ||||
|     @DoNotStrip | ||||
|     @Keep | ||||
|     public FrameProcessor(HybridData hybridData) { | ||||
|         mHybridData = hybridData; | ||||
|     } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import com.mrousavy.camera.core.ViewNotFoundError | ||||
| import java.lang.ref.WeakReference | ||||
|  | ||||
| @Suppress("KotlinJniMissingFunction") // we use fbjni. | ||||
| class VisionCameraProxy(context: ReactApplicationContext) { | ||||
| class VisionCameraProxy(private val reactContext: ReactApplicationContext) { | ||||
|   companion object { | ||||
|     const val TAG = "VisionCameraProxy" | ||||
|   } | ||||
| @@ -24,6 +24,8 @@ class VisionCameraProxy(context: ReactApplicationContext) { | ||||
|   private var mHybridData: HybridData | ||||
|   private var mContext: WeakReference<ReactApplicationContext> | ||||
|   private var mScheduler: VisionCameraScheduler | ||||
|   val context: ReactApplicationContext | ||||
|     get() = reactContext | ||||
|  | ||||
|   init { | ||||
|     val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl | ||||
|   | ||||
| @@ -3,20 +3,12 @@ package com.mrousavy.camera.types | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| class CodeScannerOptions(map: ReadableMap) { | ||||
|   val codeTypes: List<CodeType> | ||||
|  | ||||
|   init { | ||||
|     val codeTypes = map.getArray("codeTypes")?.toArrayList() ?: throw InvalidTypeScriptUnionError("codeScanner", map.toString()) | ||||
|     this.codeTypes = codeTypes.map { | ||||
|       return@map CodeType.fromUnionValue(it as String) | ||||
| data class CodeScannerOptions(val codeTypes: List<CodeType>) { | ||||
|   companion object { | ||||
|     fun fromJSValue(value: ReadableMap): CodeScannerOptions { | ||||
|       val jsCodeTypes = value.getArray("codeTypes") ?: throw InvalidTypeScriptUnionError("codeScanner", value.toString()) | ||||
|       val codeTypes = jsCodeTypes.toArrayList().map { CodeType.fromUnionValue(it as String) } | ||||
|       return CodeScannerOptions(codeTypes) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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,6 +13,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||
|   EAN_8("ean-8"), | ||||
|   ITF("itf"), | ||||
|   UPC_E("upc-e"), | ||||
|   UPC_A("upc-a"), | ||||
|   QR("qr"), | ||||
|   PDF_417("pdf-417"), | ||||
|   AZTEC("aztec"), | ||||
| @@ -29,6 +30,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||
|       EAN_8 -> Barcode.FORMAT_EAN_8 | ||||
|       ITF -> Barcode.FORMAT_ITF | ||||
|       UPC_E -> Barcode.FORMAT_UPC_E | ||||
|       UPC_A -> Barcode.FORMAT_UPC_A | ||||
|       QR -> Barcode.FORMAT_QR_CODE | ||||
|       PDF_417 -> Barcode.FORMAT_PDF417 | ||||
|       AZTEC -> Barcode.FORMAT_AZTEC | ||||
| @@ -47,6 +49,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||
|         Barcode.FORMAT_EAN_8 -> EAN_8 | ||||
|         Barcode.FORMAT_ITF -> ITF | ||||
|         Barcode.FORMAT_UPC_E -> UPC_E | ||||
|         Barcode.FORMAT_UPC_A -> UPC_A | ||||
|         Barcode.FORMAT_QR_CODE -> QR | ||||
|         Barcode.FORMAT_PDF417 -> PDF_417 | ||||
|         Barcode.FORMAT_AZTEC -> AZTEC | ||||
| @@ -64,6 +67,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||
|         "ean-8" -> EAN_8 | ||||
|         "itf" -> ITF | ||||
|         "upc-e" -> UPC_E | ||||
|         "upc-a" -> UPC_A | ||||
|         "qr" -> QR | ||||
|         "pdf-417" -> PDF_417 | ||||
|         "aztec" -> AZTEC | ||||
|   | ||||
| @@ -0,0 +1,36 @@ | ||||
| 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,6 +9,19 @@ enum class HardwareLevel(override val unionValue: String) : JSUnionValue { | ||||
|   FULL("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 { | ||||
|     fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel = | ||||
|       when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package com.mrousavy.camera.types | ||||
|  | ||||
| import android.hardware.camera2.CameraCharacteristics | ||||
| import com.mrousavy.camera.core.CameraDeviceDetails | ||||
|  | ||||
| enum class Orientation(override val unionValue: String) : JSUnionValue { | ||||
|   PORTRAIT("portrait"), | ||||
| @@ -19,23 +19,22 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { | ||||
|   fun toDegrees(): Int = | ||||
|     when (this) { | ||||
|       PORTRAIT -> 0 | ||||
|       LANDSCAPE_RIGHT -> 90 | ||||
|       LANDSCAPE_LEFT -> 90 | ||||
|       PORTRAIT_UPSIDE_DOWN -> 180 | ||||
|       LANDSCAPE_LEFT -> 270 | ||||
|       LANDSCAPE_RIGHT -> 270 | ||||
|     } | ||||
|  | ||||
|   fun toSensorRelativeOrientation(cameraCharacteristics: CameraCharacteristics): Orientation { | ||||
|     val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! | ||||
|  | ||||
|   fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation { | ||||
|     // Convert target orientation to rotation degrees (0, 90, 180, 270) | ||||
|     var rotationDegrees = this.toDegrees() | ||||
|  | ||||
|     // Reverse device orientation for front-facing cameras | ||||
|     val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT | ||||
|     if (facingFront) rotationDegrees = -rotationDegrees | ||||
|     if (deviceDetails.lensFacing == LensFacing.FRONT) { | ||||
|       rotationDegrees = -rotationDegrees | ||||
|     } | ||||
|  | ||||
|     // Rotate sensor rotation by target rotation | ||||
|     val newRotationDegrees = (sensorOrientation + rotationDegrees + 360) % 360 | ||||
|     val newRotationDegrees = (deviceDetails.sensorOrientation.toDegrees() + rotationDegrees + 360) % 360 | ||||
|  | ||||
|     return fromRotationDegrees(newRotationDegrees) | ||||
|   } | ||||
| @@ -52,9 +51,9 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { | ||||
|  | ||||
|     fun fromRotationDegrees(rotationDegrees: Int): Orientation = | ||||
|       when (rotationDegrees) { | ||||
|         in 45..135 -> LANDSCAPE_RIGHT | ||||
|         in 45..135 -> LANDSCAPE_LEFT | ||||
|         in 135..225 -> PORTRAIT_UPSIDE_DOWN | ||||
|         in 225..315 -> LANDSCAPE_LEFT | ||||
|         in 225..315 -> LANDSCAPE_RIGHT | ||||
|         else -> PORTRAIT | ||||
|       } | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| package com.mrousavy.camera.types | ||||
|  | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| enum class ResizeMode(override val unionValue: String) : JSUnionValue { | ||||
|   COVER("cover"), | ||||
|   CONTAIN("contain"); | ||||
| @@ -9,7 +11,7 @@ enum class ResizeMode(override val unionValue: String) : JSUnionValue { | ||||
|       when (unionValue) { | ||||
|         "cover" -> COVER | ||||
|         "contain" -> CONTAIN | ||||
|         else -> COVER | ||||
|         else -> throw InvalidTypeScriptUnionError("resizeMode", unionValue) | ||||
|       } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,21 +13,6 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu | ||||
|   CINEMATIC("cinematic"), | ||||
|   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> { | ||||
|     override fun fromUnionValue(unionValue: String?): VideoStabilizationMode = | ||||
|       when (unionValue) { | ||||
|   | ||||
| @@ -0,0 +1,101 @@ | ||||
| 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 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,13 +1,33 @@ | ||||
| package com.mrousavy.camera.utils | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.BitmapFactory | ||||
| import android.util.Size | ||||
| import java.io.File | ||||
| import java.io.FileOutputStream | ||||
|  | ||||
| class FileUtils { | ||||
|   companion object { | ||||
|     fun createTempFile(context: Context, extension: String): File = | ||||
|       File.createTempFile("mrousavy", extension, context.cacheDir).also { | ||||
|         it.deleteOnExit() | ||||
|     fun writeBitmapTofile(bitmap: Bitmap, file: File, quality: Int) { | ||||
|       FileOutputStream(file).use { stream -> | ||||
|         bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     fun getImageSize(imagePath: String): Size { | ||||
|       val bitmapOptions = BitmapFactory.Options().also { | ||||
|         it.inJustDecodeBounds = true | ||||
|       } | ||||
|       BitmapFactory.decodeFile(imagePath, bitmapOptions) | ||||
|       val width = bitmapOptions.outWidth | ||||
|       val height = bitmapOptions.outHeight | ||||
|       return Size(width, height) | ||||
|     } | ||||
|  | ||||
|     fun createTempFile(context: Context, extension: String): File = | ||||
|         File.createTempFile("mrousavy", extension, context.cacheDir).also { | ||||
|       it.deleteOnExit() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ To try the playground out for yourself, run the following commands: | ||||
|  | ||||
| ```sh | ||||
| git clone https://github.com/mrousavy/react-native-vision-camera | ||||
| cd react-native-vision-camera | ||||
| cd react-native-vision-camera/package | ||||
| yarn bootstrap | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -484,7 +484,7 @@ PODS: | ||||
|     - libwebp (~> 1.0) | ||||
|     - SDWebImage/Core (~> 5.10) | ||||
|   - SocketRocket (0.6.1) | ||||
|   - VisionCamera (3.8.2): | ||||
|   - VisionCamera (3.9.0-beta.6): | ||||
|     - React | ||||
|     - React-callinvoker | ||||
|     - React-Core | ||||
| @@ -724,9 +724,9 @@ SPEC CHECKSUMS: | ||||
|   SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d | ||||
|   SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d | ||||
|   SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 | ||||
|   VisionCamera: edbcd00e27a438b2228f67823e2b8d15a189065f | ||||
|   VisionCamera: 33c90675adf75528199f840f81dfbe74a2fe6c3f | ||||
|   Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 | ||||
|  | ||||
| PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb | ||||
|  | ||||
| COCOAPODS: 1.11.3 | ||||
| COCOAPODS: 1.14.3 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import AVFoundation | ||||
| // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate | ||||
|  | ||||
| extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { | ||||
|   func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) { | ||||
|   func startRecording(options: NSDictionary, filePath: String, callback jsCallback: @escaping RCTResponseSenderBlock) { | ||||
|     // Type-safety | ||||
|     let callback = Callback(jsCallback) | ||||
|  | ||||
| @@ -21,6 +21,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud | ||||
|       // Start Recording with success and error callbacks | ||||
|       cameraSession.startRecording( | ||||
|         options: options, | ||||
|         filePath: filePath, | ||||
|         onVideoRecorded: { video in | ||||
|           callback.resolve(video.toJSValue()) | ||||
|         }, | ||||
| @@ -49,4 +50,12 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud | ||||
|   func resumeRecording(promise: Promise) { | ||||
|     cameraSession.resumeRecording(promise: promise) | ||||
|   } | ||||
|    | ||||
|   func lockExposure(promise: Promise) { | ||||
|     cameraSession.lockCurrentExposure(promise: promise) | ||||
|   } | ||||
|    | ||||
|   func unlockExposure(promise: Promise) { | ||||
|     cameraSession.unlockCurrentExposure(promise: promise) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -62,6 +62,8 @@ public final class CameraView: UIView, CameraSessionDelegate { | ||||
|   @objc var onStarted: RCTDirectEventBlock? | ||||
|   @objc var onStopped: RCTDirectEventBlock? | ||||
|   @objc var onViewReady: RCTDirectEventBlock? | ||||
|   @objc var onInitReady: RCTDirectEventBlock? | ||||
|   @objc var onVideoChunkReady: RCTDirectEventBlock? | ||||
|   @objc var onCodeScanned: RCTDirectEventBlock? | ||||
|   // zoom | ||||
|   @objc var enableZoomGesture = false { | ||||
| @@ -301,6 +303,15 @@ public final class CameraView: UIView, CameraSessionDelegate { | ||||
|     } | ||||
|     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() { | ||||
|     ReactLogger.log(level: .info, message: "Camera started!") | ||||
| @@ -335,6 +346,31 @@ public final class CameraView: UIView, CameraSessionDelegate { | ||||
|       } | ||||
|     #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) { | ||||
|     guard let onCodeScanned = onCodeScanned else { | ||||
|   | ||||
| @@ -55,6 +55,8 @@ RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onInitReady, RCTDirectEventBlock); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onVideoChunkReady, RCTDirectEventBlock); | ||||
| // Code Scanner | ||||
| RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary); | ||||
| RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock); | ||||
| @@ -62,7 +64,8 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock); | ||||
| // Camera View Functions | ||||
| RCT_EXTERN_METHOD(startRecording | ||||
|                   : (nonnull NSNumber*)node options | ||||
|                   : (NSDictionary*)options onRecordCallback | ||||
|                   : (NSDictionary*)options filePath | ||||
|                   : (NSString*)filePath onRecordCallback | ||||
|                   : (RCTResponseSenderBlock)onRecordCallback); | ||||
| RCT_EXTERN_METHOD(pauseRecording | ||||
|                   : (nonnull NSNumber*)node resolve | ||||
| @@ -83,5 +86,13 @@ RCT_EXTERN_METHOD(focus | ||||
|                   : (NSDictionary*)point resolve | ||||
|                   : (RCTPromiseResolveBlock)resolve 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 | ||||
|   | ||||
| @@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager { | ||||
|   //       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. | ||||
|   @objc | ||||
|   final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { | ||||
|   final func startRecording(_ node: NSNumber, options: NSDictionary, filePath: NSString, onRecordCallback: @escaping RCTResponseSenderBlock) { | ||||
|     let component = getCameraView(withTag: node) | ||||
|     component.startRecording(options: options, callback: onRecordCallback) | ||||
|     component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback) | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
| @@ -110,6 +110,18 @@ final class CameraViewManager: RCTViewManager { | ||||
|       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 | ||||
|  | ||||
|   | ||||
| @@ -93,7 +93,7 @@ enum DeviceError: String { | ||||
|     case .lowLightBoostNotSupported: | ||||
|       return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true." | ||||
|     case .focusNotSupported: | ||||
|       return "The currently selected camera device does not support focussing!" | ||||
|       return "The currently selected camera device does not support focusing!" | ||||
|     case .microphoneUnavailable: | ||||
|       return "The microphone was unavailable." | ||||
|     case .notAvailableOnSimulator: | ||||
| @@ -176,6 +176,7 @@ enum CaptureError { | ||||
|   case noRecordingInProgress | ||||
|   case fileError | ||||
|   case createTempFileError(message: String? = nil) | ||||
|   case createRecordingDirectoryError(message: String? = nil) | ||||
|   case createRecorderError(message: String? = nil) | ||||
|   case videoNotEnabled | ||||
|   case photoNotEnabled | ||||
| @@ -193,6 +194,8 @@ enum CaptureError { | ||||
|       return "file-io-error" | ||||
|     case .createTempFileError: | ||||
|       return "create-temp-file-error" | ||||
|     case .createRecordingDirectoryError: | ||||
|       return "create-recording-directory-error" | ||||
|     case .createRecorderError: | ||||
|       return "create-recorder-error" | ||||
|     case .videoNotEnabled: | ||||
| @@ -218,6 +221,8 @@ enum CaptureError { | ||||
|       return "An unexpected File IO error occured!" | ||||
|     case let .createTempFileError(message: 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): | ||||
|       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" | ||||
|     case .videoNotEnabled: | ||||
|   | ||||
| @@ -15,6 +15,7 @@ extension CameraSession { | ||||
|    Starts a video + audio recording with a custom Asset Writer. | ||||
|    */ | ||||
|   func startRecording(options: RecordVideoOptions, | ||||
|                       filePath: String, | ||||
|                       onVideoRecorded: @escaping (_ video: Video) -> Void, | ||||
|                       onError: @escaping (_ error: CameraError) -> Void) { | ||||
|     // Run on Camera Queue | ||||
| @@ -34,6 +35,14 @@ extension CameraSession { | ||||
|  | ||||
|       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 | ||||
|       let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in | ||||
|         defer { | ||||
| @@ -62,7 +71,7 @@ extension CameraSession { | ||||
|         } else { | ||||
|           if status == .completed { | ||||
|             // Recording was successfully saved | ||||
|             let video = Video(path: recordingSession.url.absoluteString, | ||||
|             let video = Video(path: recordingSession.outputDiretory.absoluteString, | ||||
|                               duration: recordingSession.duration, | ||||
|                               size: recordingSession.size ?? CGSize.zero) | ||||
|             onVideoRecorded(video) | ||||
| @@ -73,22 +82,22 @@ extension CameraSession { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Create temporary file | ||||
|       let errorPointer = ErrorPointer(nilLiteral: ()) | ||||
|       let fileExtension = options.fileType.descriptor ?? "mov" | ||||
|       guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else { | ||||
|         let message = errorPointer?.pointee?.description | ||||
|         onError(.capture(.createTempFileError(message: message))) | ||||
|         return | ||||
|       if !FileManager.default.fileExists(atPath: filePath) { | ||||
|         do { | ||||
|           try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true) | ||||
|         } catch { | ||||
|           onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription))) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)") | ||||
|       let tempURL = URL(string: "file://\(tempFilePath)")! | ||||
|       ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)") | ||||
|  | ||||
|       do { | ||||
|         // Create RecordingSession for the temp file | ||||
|         let recordingSession = try RecordingSession(url: tempURL, | ||||
|         let recordingSession = try RecordingSession(outputDiretory: filePath, | ||||
|                                                     fileType: options.fileType, | ||||
|                                                     onChunkReady: onChunkReady, | ||||
|                                                     completion: onFinish) | ||||
|  | ||||
|         // Init Audio + Activate Audio Session (optional) | ||||
| @@ -182,4 +191,68 @@ 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,6 +109,7 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC | ||||
|         try lambda(config) | ||||
|       } catch { | ||||
|         self.onConfigureError(error) | ||||
|         return | ||||
|       } | ||||
|       let difference = CameraConfiguration.Difference(between: self.configuration, and: config) | ||||
|  | ||||
| @@ -117,51 +118,64 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC | ||||
|       do { | ||||
|         // If needed, configure the AVCaptureSession (inputs, outputs) | ||||
|         if difference.isSessionConfigurationDirty { | ||||
|           try self.withSessionLock { | ||||
|             // 1. Update input device | ||||
|             if difference.inputChanged { | ||||
|               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) | ||||
|             } | ||||
|           self.captureSession.beginConfiguration() | ||||
|  | ||||
|           // 1. Update input device | ||||
|           if difference.inputChanged { | ||||
|             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) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         guard let device = self.videoDeviceInput?.device else { | ||||
|           throw CameraError.device(.noDevice) | ||||
|         } | ||||
|  | ||||
|         // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..) | ||||
|         if difference.isDeviceConfigurationDirty { | ||||
|           try self.withDeviceLock { device in | ||||
|             // 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) | ||||
|             } | ||||
|           try device.lockForConfiguration() | ||||
|           defer { | ||||
|             device.unlockForConfiguration() | ||||
|           } | ||||
|  | ||||
|           // 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 | ||||
| @@ -169,9 +183,11 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC | ||||
|  | ||||
|         // 10. Enable or disable the Torch if needed (requires session to be running) | ||||
|         if difference.torchChanged { | ||||
|           try self.withDeviceLock { device in | ||||
|             try self.configureTorch(configuration: config, device: device) | ||||
|           try device.lockForConfiguration() | ||||
|           defer { | ||||
|             device.unlockForConfiguration() | ||||
|           } | ||||
|           try self.configureTorch(configuration: config, device: device) | ||||
|         } | ||||
|  | ||||
|         // Notify about Camera initialization | ||||
| @@ -179,6 +195,7 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC | ||||
|           self.delegate?.onSessionInitialized() | ||||
|         } | ||||
|  | ||||
|         self.delegate?.onCameraConfigurationChanged(config, difference) | ||||
|         // After configuring, set this to the new configuration. | ||||
|         self.configuration = config | ||||
|       } catch { | ||||
| @@ -206,41 +223,6 @@ 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`) | ||||
|    */ | ||||
|   | ||||
| @@ -21,6 +21,8 @@ protocol CameraSessionDelegate: AnyObject { | ||||
|    Called when the [CameraSession] successfully initializes | ||||
|    */ | ||||
|   func onSessionInitialized() | ||||
|    | ||||
|   func onCameraConfigurationChanged(_ configuration: CameraConfiguration?, _ difference: CameraConfiguration.Difference?) | ||||
|   /** | ||||
|    Called when the [CameraSession] starts streaming frames. (isActive=true) | ||||
|    */ | ||||
| @@ -33,6 +35,10 @@ protocol CameraSessionDelegate: AnyObject { | ||||
|    Called for every frame (if video or frameProcessor is enabled) | ||||
|    */ | ||||
|   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 | ||||
|    */ | ||||
|   | ||||
							
								
								
									
										88
									
								
								package/ios/Core/ChunkedRecorder.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								package/ios/Core/ChunkedRecorder.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  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,6 +29,7 @@ class RecordingSession { | ||||
|   private let assetWriter: AVAssetWriter | ||||
|   private var audioWriter: AVAssetWriterInput? | ||||
|   private var videoWriter: AVAssetWriterInput? | ||||
|   private let recorder: ChunkedRecorder | ||||
|   private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void | ||||
|  | ||||
|   private var startTimestamp: CMTime? | ||||
| @@ -48,8 +49,8 @@ class RecordingSession { | ||||
|   /** | ||||
|    Gets the file URL of the recorded video. | ||||
|    */ | ||||
|   var url: URL { | ||||
|     return assetWriter.outputURL | ||||
|   var outputDiretory: URL { | ||||
|     return recorder.outputURL | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -70,14 +71,32 @@ class RecordingSession { | ||||
|     return (lastWrittenTimestamp - startTimestamp).seconds | ||||
|   } | ||||
|  | ||||
|   init(url: URL, | ||||
|   init(outputDiretory: String, | ||||
|        fileType: AVFileType, | ||||
|        onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), | ||||
|        completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { | ||||
|     completionHandler = completion | ||||
|  | ||||
|     do { | ||||
|       assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType) | ||||
|       let outputURL = URL(fileURLWithPath: outputDiretory) | ||||
|       recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady) | ||||
|       assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!) | ||||
|       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 { | ||||
|       throw CameraError.capture(.createRecorderError(message: error.description)) | ||||
|     } | ||||
|   | ||||
| @@ -32,28 +32,36 @@ extension AVCaptureOutput { | ||||
|   func setOrientation(_ orientation: Orientation) { | ||||
|     // Set orientation for each connection | ||||
|     for connection in connections { | ||||
|       #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 | ||||
|       connection.setOrientation(orientation) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| 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,6 +40,9 @@ extension AVMetadataObject.ObjectType { | ||||
|     case "upc-e": | ||||
|       self = .upce | ||||
|       return | ||||
|     case "upc-a": | ||||
|       self = .ean13 | ||||
|       return | ||||
|     case "qr": | ||||
|       self = .qr | ||||
|       return | ||||
|   | ||||
							
								
								
									
										37
									
								
								package/ios/TestRecorder/AppDelegate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								package/ios/TestRecorder/AppDelegate.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  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. | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "colors" : [ | ||||
|     { | ||||
|       "idiom" : "universal" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "size" : "1024x1024" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								package/ios/TestRecorder/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package/ios/TestRecorder/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								package/ios/TestRecorder/Base.lproj/LaunchScreen.storyboard
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								package/ios/TestRecorder/Base.lproj/LaunchScreen.storyboard
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <?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> | ||||
							
								
								
									
										51
									
								
								package/ios/TestRecorder/Base.lproj/Main.storyboard
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								package/ios/TestRecorder/Base.lproj/Main.storyboard
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <?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> | ||||
							
								
								
									
										25
									
								
								package/ios/TestRecorder/Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								package/ios/TestRecorder/Info.plist
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <?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> | ||||
							
								
								
									
										15
									
								
								package/ios/TestRecorder/ReactStubs.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								package/ios/TestRecorder/ReactStubs.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  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 | ||||
							
								
								
									
										17
									
								
								package/ios/TestRecorder/ReactStubs.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								package/ios/TestRecorder/ReactStubs.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  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 | ||||
							
								
								
									
										102
									
								
								package/ios/TestRecorder/ReactStubs.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								package/ios/TestRecorder/ReactStubs.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| // | ||||
| //  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() | ||||
| } | ||||
							
								
								
									
										53
									
								
								package/ios/TestRecorder/SceneDelegate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								package/ios/TestRecorder/SceneDelegate.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  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. | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								package/ios/TestRecorder/TestRecorder-Bridging-Header.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package/ios/TestRecorder/TestRecorder-Bridging-Header.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| // | ||||
| //  Use this file to import your target's public headers that you would like to expose to Swift. | ||||
| // | ||||
|  | ||||
|  | ||||
| #import "ReactStubs.h" | ||||
							
								
								
									
										131
									
								
								package/ios/TestRecorder/ViewController.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								package/ios/TestRecorder/ViewController.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| // | ||||
| //  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 | ||||
|  | ||||
| struct RecordVideoOptions { | ||||
|   var fileType: AVFileType = .mov | ||||
|   var fileType: AVFileType = .mp4 | ||||
|   var flash: Torch = .off | ||||
|   var codec: AVVideoCodecType? | ||||
|   /** | ||||
|   | ||||
| @@ -7,6 +7,79 @@ | ||||
| 	objects = { | ||||
|  | ||||
| /* 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 */; }; | ||||
| 		B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; }; | ||||
| 		B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; }; | ||||
| @@ -94,6 +167,19 @@ | ||||
|  | ||||
| /* Begin PBXFileReference section */ | ||||
| 		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>"; }; | ||||
| 		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>"; }; | ||||
| @@ -191,6 +277,13 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		B3EF9F072C3FBD8300832EE7 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXFrameworksBuildPhase section */ | ||||
|  | ||||
| /* Begin PBXGroup section */ | ||||
| @@ -221,10 +314,30 @@ | ||||
| 				B887516125E0102000DB86D6 /* Extensions */, | ||||
| 				B887517225E0102000DB86D6 /* Parsers */, | ||||
| 				B887516D25E0102000DB86D6 /* React Utils */, | ||||
| 				B3EF9F0B2C3FBD8300832EE7 /* TestRecorder */, | ||||
| 				134814211AA4EA7D00B7C361 /* Products */, | ||||
| 				B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */, | ||||
| 			); | ||||
| 			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 */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| @@ -257,6 +370,7 @@ | ||||
| 				B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */, | ||||
| 				B83D5EE629377117000AFD2F /* PreviewView.swift */, | ||||
| 				B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, | ||||
| 				B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */, | ||||
| 				B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, | ||||
| 				B84760DE2608F57D004C3180 /* CameraQueues.swift */, | ||||
| 				B887518325E0102000DB86D6 /* CameraError.swift */, | ||||
| @@ -366,18 +480,42 @@ | ||||
| 			productReference = 134814201AA4EA6300B7C361 /* libVisionCamera.a */; | ||||
| 			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 */ | ||||
|  | ||||
| /* Begin PBXProject section */ | ||||
| 		58B511D31A9E6C8500147676 /* Project object */ = { | ||||
| 			isa = PBXProject; | ||||
| 			attributes = { | ||||
| 				LastSwiftUpdateCheck = 1540; | ||||
| 				LastUpgradeCheck = 1240; | ||||
| 				ORGANIZATIONNAME = mrousavy; | ||||
| 				TargetAttributes = { | ||||
| 					58B511DA1A9E6C8500147676 = { | ||||
| 						CreatedOnToolsVersion = 6.1.1; | ||||
| 					}; | ||||
| 					B3EF9F092C3FBD8300832EE7 = { | ||||
| 						CreatedOnToolsVersion = 15.4; | ||||
| 						DevelopmentTeam = HP3AMBWJGS; | ||||
| 						LastSwiftMigration = 1540; | ||||
| 						ProvisioningStyle = Automatic; | ||||
| 					}; | ||||
| 				}; | ||||
| 			}; | ||||
| 			buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "VisionCamera" */; | ||||
| @@ -387,6 +525,7 @@ | ||||
| 			knownRegions = ( | ||||
| 				English, | ||||
| 				en, | ||||
| 				Base, | ||||
| 			); | ||||
| 			mainGroup = 58B511D21A9E6C8500147676; | ||||
| 			productRefGroup = 58B511D21A9E6C8500147676; | ||||
| @@ -394,10 +533,24 @@ | ||||
| 			projectRoot = ""; | ||||
| 			targets = ( | ||||
| 				58B511DA1A9E6C8500147676 /* VisionCamera */, | ||||
| 				B3EF9F092C3FBD8300832EE7 /* TestRecorder */, | ||||
| 			); | ||||
| 		}; | ||||
| /* 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 */ | ||||
| 		B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| @@ -490,6 +643,7 @@ | ||||
| 				B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */, | ||||
| 				B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, | ||||
| 				B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, | ||||
| 				B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */, | ||||
| 				B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */, | ||||
| 				B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */, | ||||
| 				B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, | ||||
| @@ -516,8 +670,103 @@ | ||||
| 			); | ||||
| 			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 */ | ||||
|  | ||||
| /* 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 */ | ||||
| 		58B511ED1A9E6C8500147676 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| @@ -660,6 +909,94 @@ | ||||
| 			}; | ||||
| 			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 */ | ||||
|  | ||||
| /* Begin XCConfigurationList section */ | ||||
| @@ -681,6 +1018,15 @@ | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		B3EF9F1D2C3FBD8400832EE7 /* Build configuration list for PBXNativeTarget "TestRecorder" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				B3EF9F1B2C3FBD8400832EE7 /* Debug */, | ||||
| 				B3EF9F1C2C3FBD8400832EE7 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| /* End XCConfigurationList section */ | ||||
| 	}; | ||||
| 	rootObject = 58B511D31A9E6C8500147676 /* Project object */; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "react-native-vision-camera", | ||||
|   "version": "3.8.2", | ||||
|   "version": "3.9.2", | ||||
|   "description": "A powerful, high-performance React Native Camera library.", | ||||
|   "main": "lib/commonjs/index", | ||||
|   "module": "lib/module/index", | ||||
| @@ -49,26 +49,33 @@ | ||||
|     "postpack": "rm ./README.md" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "react-native", | ||||
|     "ios", | ||||
|     "android", | ||||
|     "camera", | ||||
|     "vision", | ||||
|     "native", | ||||
|     "module", | ||||
|     "react", | ||||
|     "native", | ||||
|     "camera", | ||||
|     "react-native", | ||||
|     "react-native-camera", | ||||
|     "vision", | ||||
|     "javascript", | ||||
|     "typescript", | ||||
|     "android", | ||||
|     "ios", | ||||
|     "library", | ||||
|     "instagram", | ||||
|     "snapchat", | ||||
|     "ai", | ||||
|     "ar", | ||||
|     "qr", | ||||
|     "qr-code", | ||||
|     "barcode", | ||||
|     "scanner", | ||||
|     "qrcode", | ||||
|     "barcode", | ||||
|     "qr-code", | ||||
|     "jsi", | ||||
|     "worklet", | ||||
|     "module", | ||||
|     "frame", | ||||
|     "processing", | ||||
|     "realtime" | ||||
|   ], | ||||
|   "repository": "https://github.com/mrousavy/react-native-vision-camera", | ||||
|   "author": "Marc Rousavy <marcrousavy@hotmail.com> (https://github.com/mrousavy)", | ||||
|   "author": "Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)", | ||||
|   "license": "MIT", | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/mrousavy/react-native-vision-camera/issues" | ||||
| @@ -159,5 +166,6 @@ | ||||
|         } | ||||
|       ] | ||||
|     ] | ||||
|   } | ||||
|   }, | ||||
|   "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" | ||||
| } | ||||
|   | ||||
| @@ -5,5 +5,6 @@ if which clang-format >/dev/null; then | ||||
|     clang-format -style=file:./cpp/.clang-format -i "$file" | ||||
|   done | ||||
| else | ||||
|   echo "warning: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)" | ||||
|   echo "error: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)" | ||||
|   exit 1 | ||||
| fi | ||||
|   | ||||
| @@ -3,5 +3,6 @@ | ||||
| if which ktlint >/dev/null; then | ||||
|   cd android && ktlint --color --relative --editorconfig=./.editorconfig -F ./**/*.kt* | ||||
| else | ||||
|   echo "warning: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)" | ||||
|   echo "error: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)" | ||||
|   exit 1 | ||||
| fi | ||||
|   | ||||
| @@ -3,5 +3,6 @@ | ||||
| if which swiftformat >/dev/null; then | ||||
|   cd ios && swiftformat --quiet . | ||||
| else | ||||
|   echo "warning: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)" | ||||
|   echo "error: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)" | ||||
|   exit 1 | ||||
| fi | ||||
|   | ||||
| @@ -3,5 +3,6 @@ | ||||
| if which swiftlint >/dev/null; then | ||||
|   cd ios && swiftlint --quiet --fix && swiftlint --quiet | ||||
| else | ||||
|   echo "warning: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)" | ||||
|   echo "error: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)" | ||||
|   exit 1 | ||||
| fi | ||||
|   | ||||
| @@ -26,6 +26,9 @@ interface OnErrorEvent { | ||||
|   message: string | ||||
|   cause?: ErrorWithCause | ||||
| } | ||||
| interface OnInitReadyEvent { | ||||
|   filepath: string | ||||
| } | ||||
| interface OnVideoChunkReadyEvent { | ||||
|   filepath: string | ||||
|   index: number | ||||
| @@ -39,6 +42,7 @@ type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onE | ||||
|   onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void | ||||
|   onStarted?: (event: NativeSyntheticEvent<void>) => void | ||||
|   onStopped?: (event: NativeSyntheticEvent<void>) => void | ||||
|   onInitReady?: (event: NativeSyntheticEvent<OnInitReadyEvent>) => void | ||||
|   onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void | ||||
|   onViewReady: () => void | ||||
| } | ||||
| @@ -315,6 +319,22 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> { | ||||
|       throw tryParseNativeCameraError(e) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async lockCurrentExposure(): Promise<void> { | ||||
|     try { | ||||
|       return await CameraModule.lockCurrentExposure(this.handle) | ||||
|     } catch (e) { | ||||
|       throw tryParseNativeCameraError(e) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async unlockCurrentExposure(): Promise<void> { | ||||
|     try { | ||||
|       return await CameraModule.unlockCurrentExposure(this.handle) | ||||
|     } catch (e) { | ||||
|       throw tryParseNativeCameraError(e) | ||||
|     } | ||||
|   } | ||||
|   //#endregion | ||||
|  | ||||
|   //#region Static Functions (NativeModule) | ||||
| @@ -469,7 +489,8 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> { | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||
|     if (device == null) { | ||||
|       throw new Error( | ||||
|       throw new CameraRuntimeError( | ||||
|         'device/no-device', | ||||
|         'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices', | ||||
|       ) | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,7 @@ export type SessionError = | ||||
|   | 'session/camera-cannot-be-opened' | ||||
|   | 'session/camera-has-been-disconnected' | ||||
|   | 'session/audio-in-use-by-other-app' | ||||
|   | 'session/no-outputs' | ||||
|   | 'session/audio-session-failed-to-activate' | ||||
| export type CodeScannerError = | ||||
|   | 'code-scanner/not-compatible-with-outputs' | ||||
| @@ -40,7 +41,10 @@ export type CaptureError = | ||||
|   | 'capture/recorder-error' | ||||
|   | 'capture/video-not-enabled' | ||||
|   | 'capture/photo-not-enabled' | ||||
|   | 'capture/frame-invalid' | ||||
|   | 'capture/aborted' | ||||
|   | 'capture/focus-canceled' | ||||
|   | 'capture/timed-out' | ||||
|   | 'capture/unknown' | ||||
| export type SystemError = | ||||
|   | 'system/camera-module-not-found' | ||||
|   | ||||
| @@ -183,10 +183,29 @@ export interface CameraProps extends ViewProps { | ||||
|    * | ||||
|    * @platform iOS | ||||
|    * @default | ||||
|    * - true // if video={true} and frameProcessor={undefined} | ||||
|    * - true // if frameProcessor={undefined} | ||||
|    * - false // otherwise | ||||
|    */ | ||||
|   enableBufferCompression?: boolean | ||||
|   /** | ||||
|    * Enables or disables GPU-sampled buffers for the video stream. This only takes effect when using a {@linkcode frameProcessor}. | ||||
|    * | ||||
|    * When recording a Video ({@linkcode video}) while a Frame Processor is running ({@linkcode frameProcessor}), | ||||
|    * the {@linkcode Frame | Frames} will need to be forwarded to the Media Encoder. | ||||
|    * | ||||
|    * - When `enableGpuBuffers` is `false`, the Video Pipeline will use CPU buffers causing an additional copy | ||||
|    * from the Frame Processor to the Media Encoder, which potentially results in increased latency. | ||||
|    * - When `enableGpuBuffers` is `true`, the Video Pipeline will use shared GPU buffers which greatly increases | ||||
|    * it's efficiency as an additional buffer copy is avoided. | ||||
|    * (See [`USAGE_GPU_SAMPLED_IMAGE`](https://developer.android.com/reference/android/hardware/HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE)) | ||||
|    * | ||||
|    * In general, it is recommended to set this to `true` if possible, as this can increase performance and efficiency of the Video Pipeline. | ||||
|    * | ||||
|    * @experimental This is an experimental feature flag, use at your own risk. Some devices (especially Samsungs) may crash when trying to use GPU buffers. | ||||
|    * @platform Android (API 29+) | ||||
|    * @default false | ||||
|    */ | ||||
|   enableGpuBuffers?: boolean | ||||
|   /** | ||||
|    * Enables or disables low-light boost on this camera device. | ||||
|    * | ||||
| @@ -227,6 +246,7 @@ export interface CameraProps extends ViewProps { | ||||
|    * * Dual Device fusion for greater detail ([`isDualCameraDualPhotoDeliveryEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/2873917-isdualcameradualphotodeliveryena)) | ||||
|    * * Sets the maximum quality prioritization to `.quality` ([`maxPhotoQualityPrioritization`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization)) | ||||
|    * | ||||
|    * @platform iOS | ||||
|    * @default false | ||||
|    */ | ||||
|   enableHighQualityPhotos?: boolean | ||||
|   | ||||
| @@ -12,6 +12,7 @@ export type CodeType = | ||||
|   | 'ean-8' | ||||
|   | 'itf' | ||||
|   | 'upc-e' | ||||
|   | 'upc-a' | ||||
|   | 'qr' | ||||
|   | 'pdf-417' | ||||
|   | 'aztec' | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user