feat: Support focus()
on Android (#1713)
* feat: Support `focus()` on Android * perf: Cache RequestBuilder * Use 30 FPS as default * feat: Implement focus(x, y) * fix: Fix ByteBuffer resizing
This commit is contained in:
parent
c88605e230
commit
23af74aaf1
@ -1,17 +1,21 @@
|
|||||||
package com.mrousavy.camera
|
package com.mrousavy.camera
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Point
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CameraDevice
|
import android.hardware.camera2.CameraDevice
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CameraMetadata
|
||||||
import android.hardware.camera2.CaptureRequest
|
import android.hardware.camera2.CaptureRequest
|
||||||
import android.hardware.camera2.CaptureResult
|
import android.hardware.camera2.CaptureResult
|
||||||
import android.hardware.camera2.TotalCaptureResult
|
import android.hardware.camera2.TotalCaptureResult
|
||||||
|
import android.hardware.camera2.params.MeteringRectangle
|
||||||
import android.media.Image
|
import android.media.Image
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Range
|
import android.util.Range
|
||||||
|
import android.util.Size
|
||||||
import com.mrousavy.camera.extensions.SessionType
|
import com.mrousavy.camera.extensions.SessionType
|
||||||
import com.mrousavy.camera.extensions.capture
|
import com.mrousavy.camera.extensions.capture
|
||||||
import com.mrousavy.camera.extensions.createCaptureSession
|
import com.mrousavy.camera.extensions.createCaptureSession
|
||||||
@ -44,6 +48,9 @@ class CameraSession(private val context: Context,
|
|||||||
private val onError: (e: Throwable) -> Unit): CoroutineScope, Closeable, CameraOutputs.Callback, CameraManager.AvailabilityCallback() {
|
private val onError: (e: Throwable) -> Unit): CoroutineScope, Closeable, CameraOutputs.Callback, CameraManager.AvailabilityCallback() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CameraSession"
|
private const val TAG = "CameraSession"
|
||||||
|
|
||||||
|
// TODO: Samsung advertises 60 FPS but only allows 30 FPS for some reason.
|
||||||
|
private val CAN_SET_FPS = Build.MANUFACTURER != "samsung"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CapturedPhoto(val image: Image,
|
data class CapturedPhoto(val image: Image,
|
||||||
@ -76,6 +83,7 @@ class CameraSession(private val context: Context,
|
|||||||
|
|
||||||
private var captureSession: CameraCaptureSession? = null
|
private var captureSession: CameraCaptureSession? = null
|
||||||
private var cameraDevice: CameraDevice? = null
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
private var previewRequest: CaptureRequest.Builder? = null
|
||||||
private val photoOutputSynchronizer = PhotoOutputSynchronizer()
|
private val photoOutputSynchronizer = PhotoOutputSynchronizer()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
@ -290,6 +298,20 @@ class CameraSession(private val context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun focus(x: Int, y: Int) {
|
||||||
|
val captureSession = captureSession ?: throw CameraNotReadyError()
|
||||||
|
val previewOutput = outputs?.previewOutput ?: throw CameraNotReadyError()
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
|
||||||
|
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
||||||
|
val previewSize = previewOutput.size
|
||||||
|
val pX = x.toDouble() / previewSize.width * sensorSize.height()
|
||||||
|
val pY = y.toDouble() / previewSize.height * sensorSize.width()
|
||||||
|
val point = Point(pX.toInt(), pY.toInt())
|
||||||
|
|
||||||
|
Log.i(TAG, "Focusing (${point.x}, ${point.y})...")
|
||||||
|
focus(point)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCameraAvailable(cameraId: String) {
|
override fun onCameraAvailable(cameraId: String) {
|
||||||
super.onCameraAvailable(cameraId)
|
super.onCameraAvailable(cameraId)
|
||||||
Log.i(TAG, "Camera became available: $cameraId")
|
Log.i(TAG, "Camera became available: $cameraId")
|
||||||
@ -300,6 +322,39 @@ class CameraSession(private val context: Context,
|
|||||||
Log.i(TAG, "Camera became un-available: $cameraId")
|
Log.i(TAG, "Camera became un-available: $cameraId")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun focus(point: Point) {
|
||||||
|
mutex.withLock {
|
||||||
|
val captureSession = captureSession ?: throw CameraNotReadyError()
|
||||||
|
val request = previewRequest ?: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a [CameraDevice]. If there already is an open Camera for the given [cameraId], use that.
|
* Opens a [CameraDevice]. If there already is an open Camera for the given [cameraId], use that.
|
||||||
*/
|
*/
|
||||||
@ -362,43 +417,27 @@ class CameraSession(private val context: Context,
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPreviewCaptureRequest(captureSession: CameraCaptureSession,
|
private fun getPreviewCaptureRequest(fps: Int? = null,
|
||||||
outputs: CameraOutputs,
|
|
||||||
fps: Int? = null,
|
|
||||||
videoStabilizationMode: VideoStabilizationMode? = null,
|
videoStabilizationMode: VideoStabilizationMode? = null,
|
||||||
lowLightBoost: Boolean? = null,
|
lowLightBoost: Boolean? = null,
|
||||||
hdr: Boolean? = null,
|
hdr: Boolean? = null,
|
||||||
torch: Boolean? = null): CaptureRequest {
|
torch: Boolean? = null): CaptureRequest {
|
||||||
val template = if (outputs.videoOutput != null) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
|
val captureRequest = previewRequest ?: throw CameraNotReadyError()
|
||||||
val captureRequest = captureSession.device.createCaptureRequest(template)
|
|
||||||
outputs.previewOutput?.let { output ->
|
|
||||||
Log.i(TAG, "Adding output surface ${output.outputType}..")
|
|
||||||
captureRequest.addTarget(output.surface)
|
|
||||||
}
|
|
||||||
outputs.videoOutput?.let { output ->
|
|
||||||
Log.i(TAG, "Adding output surface ${output.outputType}..")
|
|
||||||
captureRequest.addTarget(output.surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fps != null) {
|
// FPS
|
||||||
// TODO: Samsung advertises 60 FPS but only allows 30 FPS for some reason.
|
val fpsRange = if (fps != null && CAN_SET_FPS) Range(fps, fps) else Range(30, 30)
|
||||||
val isSamsung = Build.MANUFACTURER == "samsung"
|
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange)
|
||||||
val targetFps = if (isSamsung) 30 else fps
|
|
||||||
|
|
||||||
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(targetFps, targetFps))
|
// Video Stabilization
|
||||||
}
|
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, videoStabilizationMode?.toDigitalStabilizationMode())
|
||||||
if (videoStabilizationMode != null) {
|
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, videoStabilizationMode?.toOpticalStabilizationMode())
|
||||||
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, videoStabilizationMode.toDigitalStabilizationMode())
|
|
||||||
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, videoStabilizationMode.toOpticalStabilizationMode())
|
// Night/HDR Mode
|
||||||
}
|
val sceneMode = if (hdr == true) CaptureRequest.CONTROL_SCENE_MODE_HDR else if (lowLightBoost == true) CaptureRequest.CONTROL_SCENE_MODE_NIGHT else null
|
||||||
if (lowLightBoost == true) {
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, sceneMode)
|
||||||
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
|
captureRequest.set(CaptureRequest.CONTROL_MODE, if (sceneMode != null) CaptureRequest.CONTROL_MODE_USE_SCENE_MODE else null)
|
||||||
}
|
|
||||||
if (hdr == true) {
|
// Zoom
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
captureRequest.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom)
|
captureRequest.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom)
|
||||||
} else {
|
} else {
|
||||||
@ -407,6 +446,7 @@ class CameraSession(private val context: Context,
|
|||||||
captureRequest.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoom))
|
captureRequest.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoom))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Torch Mode
|
||||||
val torchMode = if (torch == true) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF
|
val torchMode = if (torch == true) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF
|
||||||
captureRequest.set(CaptureRequest.FLASH_MODE, torchMode)
|
captureRequest.set(CaptureRequest.FLASH_MODE, torchMode)
|
||||||
|
|
||||||
@ -441,19 +481,32 @@ class CameraSession(private val context: Context,
|
|||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Open Camera Device
|
// 1. Open Camera Device
|
||||||
val camera = getCameraDevice(cameraId) { reason ->
|
val camera = getCameraDevice(cameraId) { reason ->
|
||||||
isRunning = false
|
isRunning = false
|
||||||
onError(reason)
|
onError(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create capture session with outputs
|
// 2. Create capture session with outputs
|
||||||
val session = getCaptureSession(camera, outputs) {
|
val session = getCaptureSession(camera, outputs) {
|
||||||
isRunning = false
|
isRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Create request template
|
||||||
|
val template = if (outputs.videoOutput != null) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
|
||||||
|
val captureRequest = camera.createCaptureRequest(template)
|
||||||
|
outputs.previewOutput?.let { output ->
|
||||||
|
Log.i(TAG, "Adding output surface ${output.outputType}..")
|
||||||
|
captureRequest.addTarget(output.surface)
|
||||||
|
}
|
||||||
|
outputs.videoOutput?.let { output ->
|
||||||
|
Log.i(TAG, "Adding output surface ${output.outputType}..")
|
||||||
|
captureRequest.addTarget(output.surface)
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Camera Session initialized! Starting repeating request..")
|
Log.i(TAG, "Camera Session initialized! Starting repeating request..")
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
this.previewRequest = captureRequest
|
||||||
this.captureSession = session
|
this.captureSession = session
|
||||||
this.cameraDevice = camera
|
this.cameraDevice = camera
|
||||||
}
|
}
|
||||||
@ -477,13 +530,9 @@ class CameraSession(private val context: Context,
|
|||||||
val videoStabilizationMode = videoStabilizationMode
|
val videoStabilizationMode = videoStabilizationMode
|
||||||
val lowLightBoost = lowLightBoost
|
val lowLightBoost = lowLightBoost
|
||||||
val hdr = hdr
|
val hdr = hdr
|
||||||
val outputs = outputs
|
|
||||||
if (outputs == null || outputs.size == 0) {
|
|
||||||
Log.i(TAG, "CameraSession doesn't have any Outputs, canceling..")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val repeatingRequest = getPreviewCaptureRequest(session, outputs, fps, videoStabilizationMode, lowLightBoost, hdr)
|
val repeatingRequest = getPreviewCaptureRequest(fps, videoStabilizationMode, lowLightBoost, hdr)
|
||||||
|
Log.d(TAG, "Setting Repeating Request..")
|
||||||
session.setRepeatingRequest(repeatingRequest, null, null)
|
session.setRepeatingRequest(repeatingRequest, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,7 @@ package com.mrousavy.camera
|
|||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
|
||||||
suspend fun CameraView.focus(pointMap: ReadableMap) {
|
suspend fun CameraView.focus(pointMap: ReadableMap) {
|
||||||
// TODO: CameraView.focus!!
|
val x = pointMap.getInt("x")
|
||||||
|
val y = pointMap.getInt("y")
|
||||||
|
cameraSession.focus(x, y)
|
||||||
}
|
}
|
||||||
|
@ -104,11 +104,11 @@ public class Frame {
|
|||||||
int vSize = vBuffer.remaining();
|
int vSize = vBuffer.remaining();
|
||||||
int totalSize = ySize + uSize + vSize;
|
int totalSize = ySize + uSize + vSize;
|
||||||
|
|
||||||
if (byteArrayCache == null) {
|
if (byteArrayCache != null) byteArrayCache.rewind();
|
||||||
|
if (byteArrayCache == null || byteArrayCache.remaining() != totalSize) {
|
||||||
byteArrayCache = ByteBuffer.allocateDirect(totalSize);
|
byteArrayCache = ByteBuffer.allocateDirect(totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
byteArrayCache.rewind();
|
|
||||||
byteArrayCache.put(yBuffer).put(uBuffer).put(vBuffer);
|
byteArrayCache.put(yBuffer).put(uBuffer).put(vBuffer);
|
||||||
|
|
||||||
return byteArrayCache;
|
return byteArrayCache;
|
||||||
|
Loading…
Reference in New Issue
Block a user