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:
		@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user