Fire the LED on the 0.5x still
0.5x is a bare Camera2 session, so CameraX never saw the flash setting and the ultra-wide still came off unlit. The native module now reads FLASH_INFO_AVAILABLE off the lens, maps off/auto/on to CONTROL_AE_MODE plus FLASH_MODE_SINGLE, and starts an AE precapture so the HAL can meter the flash before the frame. The viewfinder hands its flashMode down beside the shutter, with a Zap control that only appears where a unit exists.
This commit is contained in:
+56
-5
@@ -9,6 +9,8 @@ import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraDevice
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CaptureRequest
|
||||
import android.hardware.camera2.CaptureResult
|
||||
import android.hardware.camera2.TotalCaptureResult
|
||||
import android.hardware.camera2.params.MeteringRectangle
|
||||
import android.media.ImageReader
|
||||
import android.os.Handler
|
||||
@@ -160,6 +162,11 @@ class UltraWidePreviewView(context: Context, appContext: AppContext) : ExpoView(
|
||||
// Region of the last tap-to-focus, so an EV change can re-issue the repeating
|
||||
// request without dropping the lock.
|
||||
private var lockedRegion: Rect? = null
|
||||
// Whether this lens may drive the flash unit at all. The LED physically
|
||||
// belongs to the back flash unit the main camera owns, and the spec rejects a
|
||||
// flash request on a camera whose FLASH_INFO_AVAILABLE is false, so the still
|
||||
// request clamps to OFF rather than failing the shot on such a lens.
|
||||
private var flashAvailable = false
|
||||
private var busy = false
|
||||
private var pending: Promise? = null
|
||||
// Held by closeAsync() until the HAL has really let go of the lens; see
|
||||
@@ -399,6 +406,8 @@ class UltraWidePreviewView(context: Context, appContext: AppContext) : ExpoView(
|
||||
val ps = pick4x3(map?.getOutputSizes(SurfaceTexture::class.java), 1920 * 1440)
|
||||
val fs = pick4x3(map?.getOutputSizes(ImageFormat.JPEG), Int.MAX_VALUE)
|
||||
activeArray = cs.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
|
||||
flashAvailable = cs.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
||||
Log.i(TAG, "ultra-wide $id flash=$flashAvailable")
|
||||
cs.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)?.let {
|
||||
evRange = it.lower..it.upper
|
||||
}
|
||||
@@ -501,7 +510,29 @@ class UltraWidePreviewView(context: Context, appContext: AppContext) : ExpoView(
|
||||
}
|
||||
}
|
||||
|
||||
fun capture(promise: Promise) {
|
||||
/**
|
||||
* The still request's flash settings for the JS 'off' | 'auto' | 'on'.
|
||||
* Camera2 has no FLASH_MODE_AUTO: the auto behaviour lives in
|
||||
* CONTROL_AE_MODE, where the HAL owns the unit and fires it only when AE
|
||||
* comes back with CONTROL_AE_STATE_FLASH_REQUIRED; 'on' is the same knob with
|
||||
* the firing forced. Both ride next to FLASH_MODE_SINGLE so a HAL that
|
||||
* ignores the AE override still fires on this one request; the spec has the
|
||||
* AE flash modes win over FLASH_MODE, so the pair cannot double-fire.
|
||||
* A lens with no unit reports FLASH_INFO_AVAILABLE false, and the flash
|
||||
* controls only exist where it is true, so every mode collapses to OFF there.
|
||||
*/
|
||||
private fun flashAeMode(mode: String): Int = when {
|
||||
!flashAvailable -> CaptureRequest.CONTROL_AE_MODE_ON
|
||||
mode == "on" -> CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
|
||||
mode == "auto" -> CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
|
||||
else -> CaptureRequest.CONTROL_AE_MODE_ON
|
||||
}
|
||||
|
||||
private fun flashMode(mode: String): Int =
|
||||
if (flashAeMode(mode) == CaptureRequest.CONTROL_AE_MODE_ON) CaptureRequest.FLASH_MODE_OFF
|
||||
else CaptureRequest.FLASH_MODE_SINGLE
|
||||
|
||||
fun capture(promise: Promise, flash: String) {
|
||||
val dev = device
|
||||
val s = session
|
||||
val ir = reader
|
||||
@@ -521,17 +552,34 @@ class UltraWidePreviewView(context: Context, appContext: AppContext) : ExpoView(
|
||||
req.addTarget(ir.surface)
|
||||
req.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
||||
req.set(CaptureRequest.JPEG_ORIENTATION, sensorOrientation)
|
||||
req.set(CaptureRequest.FLASH_MODE, flashMode(flash))
|
||||
req.set(CaptureRequest.CONTROL_AE_MODE, flashAeMode(flash))
|
||||
// The live preview carries the EV as a hardware AE bias (see setEv); the
|
||||
// file must not, or the lens bias and the export's software gain would
|
||||
// stack into a double EV. Pin the bias off and let the precapture trigger
|
||||
// settle AE on it before the frame is taken — without the trigger the
|
||||
// first frames can still come off the biased exposure.
|
||||
// first frames can still come off the biased exposure. That same trigger
|
||||
// is what lets a flash mode on this request be metered at all: the
|
||||
// precapture sequence is what returns AE_STATE_FLASH_REQUIRED, the answer
|
||||
// the HAL fires on in 'auto'.
|
||||
req.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 0)
|
||||
req.set(
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
|
||||
)
|
||||
s.capture(req.build(), null, camHandler)
|
||||
s.capture(req.build(), object : CameraCaptureSession.CaptureCallback() {
|
||||
override fun onCaptureCompleted(
|
||||
s2: CameraCaptureSession,
|
||||
request: CaptureRequest,
|
||||
result: TotalCaptureResult
|
||||
) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"still flash=${result.get(CaptureResult.FLASH_STATE)} ae=" +
|
||||
"${result.get(CaptureResult.CONTROL_AE_STATE)}"
|
||||
)
|
||||
}
|
||||
}, camHandler)
|
||||
} catch (e: Throwable) {
|
||||
fail("ERR_CAPTURE", "Capture request failed: ${e.message}")
|
||||
return
|
||||
@@ -729,12 +777,15 @@ class UltraWideModule : Module() {
|
||||
Prop("cover") { view: UltraWidePreviewView, cover: Boolean -> view.setCover(cover) }
|
||||
}
|
||||
|
||||
AsyncFunction("captureAsync") { promise: Promise ->
|
||||
// flashMode is the JS 'off' | 'auto' | 'on' the viewfinder is showing; the
|
||||
// view clamps it to what this lens can actually drive. Expo only recognises
|
||||
// the trailing Promise as the resolve handle, so it goes last.
|
||||
AsyncFunction("captureAsync") { flashMode: String, promise: Promise ->
|
||||
val view = UltraWidePreviewView.current()
|
||||
if (view == null) {
|
||||
promise.reject("ERR_NO_VIEW", "Ultra-wide preview is not mounted", null)
|
||||
} else {
|
||||
view.capture(promise)
|
||||
view.capture(promise, flashMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ViewProps } from 'react-native';
|
||||
import type { FlashMode } from '../../src/types';
|
||||
|
||||
export interface UltraWideModule {
|
||||
/** Hidden back ultra-wide camera id ('' when this device has none). */
|
||||
availableAsync(): Promise<string>;
|
||||
/** Still capture from the ultra-wide lens; resolves to an absolute file path. */
|
||||
captureAsync(): Promise<string>;
|
||||
/**
|
||||
* Still capture from the ultra-wide lens; resolves to an absolute file path.
|
||||
* `flashMode` is the viewfinder's setting, driven straight into the still
|
||||
* request's FLASH_MODE — the lens falls back to 'off' on its own when it has
|
||||
* no flash unit to fire.
|
||||
*/
|
||||
captureAsync(flashMode: FlashMode): Promise<string>;
|
||||
/**
|
||||
* Tap-to-focus + spot metering on the ultra-wide lens, at a point in the
|
||||
* preview view (React Native dp, same space as a touch's locationX/Y). The
|
||||
|
||||
Reference in New Issue
Block a user