Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: michaelwolz
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example-app
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ let package = Package(
dependencies: ["CameraViewPlugin"],
path: "ios/Tests/CameraViewPluginTests")
]
)
)
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,16 +272,16 @@ Check if the camera view is currently running.
### capture(...)

```typescript
capture(options: { quality: number; }) => Promise<CaptureResponse>
capture<T extends CaptureOptions>(options: T) => Promise<CaptureResponse<T>>
```

Capture a photo using the current camera configuration.

| Param | Type | Description |
| ------------- | --------------------------------- | ------------------------------- |
| **`options`** | <code>{ quality: number; }</code> | - Capture configuration options |
| Param | Type | Description |
| ------------- | -------------- | ------------------------------- |
| **`options`** | <code>T</code> | - Capture configuration options |

**Returns:** <code>Promise&lt;<a href="#captureresponse">CaptureResponse</a>&gt;</code>
**Returns:** <code>Promise&lt;<a href="#captureresponse">CaptureResponse</a>&lt;T&gt;&gt;</code>

**Since:** 1.0.0

Expand All @@ -291,7 +291,7 @@ Capture a photo using the current camera configuration.
### captureSample(...)

```typescript
captureSample(options: { quality: number; }) => Promise<CaptureResponse>
captureSample<T extends CaptureOptions>(options: T) => Promise<CaptureResponse<T>>
```

Captures a frame from the current camera preview without using the full camera capture pipeline.
Expand All @@ -304,11 +304,11 @@ On web this method does exactly the same as `capture()` as it only captures a fr
because unfortunately [ImageCapture API](https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture) is
not yet well supported on the web.

| Param | Type | Description |
| ------------- | --------------------------------- | ------------------------------- |
| **`options`** | <code>{ quality: number; }</code> | - Capture configuration options |
| Param | Type | Description |
| ------------- | -------------- | ------------------------------- |
| **`options`** | <code>T</code> | - Capture configuration options |

**Returns:** <code>Promise&lt;<a href="#captureresponse">CaptureResponse</a>&gt;</code>
**Returns:** <code>Promise&lt;<a href="#captureresponse">CaptureResponse</a>&lt;T&gt;&gt;</code>

**Since:** 1.0.0

Expand Down Expand Up @@ -517,13 +517,14 @@ Response for checking if the camera view is running.
| **`isRunning`** | <code>boolean</code> | Indicates if the camera view is currently active and running |


#### CaptureResponse
#### CaptureOptions

Response for capturing a photo.
Configuration options for capturing photos and samples.

| Prop | Type | Description |
| ----------- | ------------------- | ----------------------------------------------- |
| **`photo`** | <code>string</code> | The base64 encoded string of the captured photo |
| Prop | Type | Description | Default | Since |
| ---------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
| **`quality`** | <code>number</code> | The JPEG quality of the captured photo/sample on a scale of 0-100 | | 1.1.0 |
| **`saveToFile`** | <code>boolean</code> | If true, saves to a temporary file and returns the web path instead of base64. The web path can be used to set the src attribute of an image for efficient loading and rendering. This reduces the data that needs to be transferred over the bridge, which can improve performance especially for high-resolution images. | <code>false</code> | 1.1.0 |


#### GetAvailableDevicesResponse
Expand Down Expand Up @@ -637,6 +638,15 @@ Maps to AVCaptureDevice DeviceTypes in iOS.
<code>'wideAngle' | 'ultraWide' | 'telephoto' | 'dual' | 'dualWide' | 'triple' | 'trueDepth'</code>


#### CaptureResponse

Response for capturing a photo
This will contain either a base64 encoded string or a web path to the captured photo,
depending on the `saveToFile` option in the <a href="#captureoptions">CaptureOptions</a>.

<code>T['saveToFile'] extends true ? { /** The web path to the captured photo that can be used to set the src attribute of an image for efficient loading and rendering (when saveToFile is true) */ webPath: string; } : { /** The base64 encoded string of the captured photo (when saveToFile is false or undefined) */ photo: string; }</code>


#### FlashMode

Flash mode options for the camera.
Expand Down
154 changes: 112 additions & 42 deletions android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context.CAMERA_SERVICE
import android.graphics.Bitmap
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.net.Uri
import android.util.Base64
import android.util.Log
import android.view.Surface
Expand All @@ -25,6 +26,8 @@ import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.getcapacitor.FileUtils
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
Expand All @@ -35,6 +38,8 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

Expand Down Expand Up @@ -118,7 +123,11 @@ class CameraView(plugin: Plugin) {
}

/** Capture a photo with the current camera configuration */
fun capturePhoto(quality: Int, callback: (String?, Exception?) -> Unit) {
fun capturePhoto(
quality: Int,
saveToFile: Boolean = false,
callback: (JSObject?, Exception?) -> Unit
) {
val startTime = System.currentTimeMillis()
val controller =
this.cameraController
Expand All @@ -145,23 +154,58 @@ class CameraView(plugin: Plugin) {
)

try {
controller.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
Log.d(
TAG,
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
)
handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
if (saveToFile) {
// Direct file capture - much more efficient!
val tempFile =
File.createTempFile("camera_capture_photo", ".jpg", context.cacheDir)
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build()

controller.takePicture(
outputFileOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val processingTime = System.currentTimeMillis() - startTime
Log.d(TAG, "Image saved directly to file in ${processingTime}ms")

val result = JSObject().apply {
val capacitorFilePath = FileUtils.getPortablePath(
context,
pluginDelegate.bridge.localUrl,
Uri.fromFile(tempFile)
)

put("webPath", capacitorFilePath)
}
callback(result, null)
}

override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "Error saving image to file", exception)
callback(null, exception)
}
}

override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "Error capturing image", exception)
callback(null, exception)
)
} else {
// Base64 capture using ImageProxy
controller.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
Log.d(
TAG,
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
)
handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
}

override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "Error capturing image", exception)
callback(null, exception)
}
}
}
)
)
}
} catch (e: Exception) {
Log.e(TAG, "Error setting up image capture", e)
callback(null, e)
Expand All @@ -170,20 +214,22 @@ class CameraView(plugin: Plugin) {
}

/**
* Handles the successful capture of an image, converting it to a Base64 string
* Handles the successful capture of an image for base64 conversion
*/
fun handleCaptureSuccess(
image: ImageProxy,
quality: Int,
rotationDegrees: Int,
callback: (String?, Exception?) -> Unit
callback: (JSObject?, Exception?) -> Unit
) {
val startTime = System.currentTimeMillis()
try {
// Turn the image into a Base64 encoded string and apply rotation if necessary
val base64String = imageProxyToBase64(image, quality, rotationDegrees)
val result = JSObject().apply {
put("photo", base64String)
}
Log.d(TAG, "Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms")
callback(base64String, null)
callback(result, null)
} catch (e: Exception) {
Log.e(TAG, "Error processing captured image", e)
callback(null, e)
Expand All @@ -196,7 +242,11 @@ class CameraView(plugin: Plugin) {
* Capture a frame directly from the preview without using the full photo pipeline which is
* faster but has lower quality.
*/
fun captureSampleFromPreview(quality: Int, callback: (String?, Exception?) -> Unit) {
fun captureSampleFromPreview(
quality: Int,
saveToFile: Boolean = false,
callback: (JSObject?, Exception?) -> Unit
) {
val previewView =
this.previewView
?: run {
Expand All @@ -205,7 +255,6 @@ class CameraView(plugin: Plugin) {
}

mainHandler.post {
val outputStream = ByteArrayOutputStream()
try {
val bitmap =
previewView.bitmap
Expand All @@ -214,27 +263,48 @@ class CameraView(plugin: Plugin) {
return@post
}

// Convert bitmap to Base64
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val byteArray = outputStream.toByteArray()
val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
val result = JSObject()

if (saveToFile) {
val tempFile =
File.createTempFile("camera_capture_sample", ".jpg", context.cacheDir)

FileOutputStream(tempFile).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
}

val capacitorFilePath = FileUtils.getPortablePath(
context,
pluginDelegate.bridge.localUrl,
Uri.fromFile(tempFile)
)

result.put("webPath", capacitorFilePath)
} else {
// Convert bitmap to Base64
val outputStream = ByteArrayOutputStream()
outputStream.use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
val byteArray = stream.toByteArray()
val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
result.put("photo", base64String)
}
}

callback(base64String, null)
callback(result, null)
} catch (e: Exception) {
Log.e(TAG, "Error capturing preview frame", e)
callback(null, e)
} finally {
outputStream.close()
}
}
}

/** Flip between front and back cameras */
fun flipCamera(callback: (Exception?) -> Unit) {
currentCameraSelector = when (currentCameraSelector) {
CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
else -> CameraSelector.DEFAULT_FRONT_CAMERA
}
CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
else -> CameraSelector.DEFAULT_FRONT_CAMERA
}

val controller =
this.cameraController
Expand Down Expand Up @@ -420,21 +490,21 @@ class CameraView(plugin: Plugin) {
setupPreviewView(context)

currentCameraSelector = if (config.position == "front") {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}

if (config.deviceId != null) {
// Prefer specific device id over position
currentCameraSelector = CameraSelector.Builder()
.addCameraFilter { cameraInfos ->
cameraInfos.filter { info ->
val cameraId = Camera2CameraInfo.from(info).cameraId
cameraId == config.deviceId
}
.addCameraFilter { cameraInfos ->
cameraInfos.filter { info ->
val cameraId = Camera2CameraInfo.from(info).cameraId
cameraId == config.deviceId
}
.build()
}
.build()
}

// Initialize camera controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,18 @@ class CameraViewPlugin : Plugin() {
fun capture(call: PluginCall) {
val timeStart = System.currentTimeMillis();
val quality = call.getInt("quality") ?: 90
val saveToFile = call.getBoolean("saveToFile") ?: false

if (quality !in 0..100) {
call.reject("Quality must be between 0 and 100")
return
}

implementation.capturePhoto(quality) { photo, error ->
implementation.capturePhoto(quality, saveToFile) { result, error ->
when {
error != null -> call.reject("Failed to capture image: ${error.message}", error)
photo == null -> call.reject("No image data")
else -> call.resolve(JSObject().apply { put("photo", photo) })
result == null -> call.reject("No image data")
else -> call.resolve(result)
}
Log.d(TAG, "capture took ${System.currentTimeMillis() - timeStart}ms")
}
Expand All @@ -96,17 +97,18 @@ class CameraViewPlugin : Plugin() {
fun captureSample(call: PluginCall) {
val timeStart = System.currentTimeMillis();
val quality = call.getInt("quality") ?: 90
val saveToFile = call.getBoolean("saveToFile") ?: false

if (quality !in 0..100) {
call.reject("Quality must be between 0 and 100")
return
}

implementation.captureSampleFromPreview(quality) { photo, error ->
implementation.captureSampleFromPreview(quality, saveToFile) { result, error ->
when {
error != null -> call.reject("Failed to capture frame: ${error.message}", error)
photo == null -> call.reject("No frame data")
else -> call.resolve(JSObject().apply { put("photo", photo) })
result == null -> call.reject("No frame data")
else -> call.resolve(result)
}
Log.d(TAG, "captureSample took ${System.currentTimeMillis() - timeStart}ms")
}
Expand Down
Loading
Loading