camera: previewRotationQuarterTurns + async previewSize event
Black-screen + extra-90°-rotation on Android both came from
AVFoundation vs CameraX behaving differently at the preview output:
- AVFoundation: data-output connection's `videoOrientation`
pre-rotates sample buffers. The Flutter Texture displays them
upright; `device.activeFormat` reports the sensor-native size
synchronously.
- CameraX: the SurfaceProvider hands back a Surface; CameraX
writes raw sensor frames into it. Rotation is a *transform hint*
via Preview.setTargetRotation that consumers must apply
themselves. And the final negotiated resolution isn't known
until the first SurfaceRequest fires — which happens AFTER
bindToLifecycle, AFTER lifecycle.start, async on the camera
executor. So `create` was returning Size(0,0).
Surface extension to bridge the gap:
- UxCameraValue.previewRotationQuarterTurns (int 0/1/2/3).
iOS native always emits 0; Android native emits
`(sensorRotationDegrees / 90) % 4` for the active camera.
[UxCameraPreview] wraps the Texture in a RotatedBox by that many
quarter-turns (applied *before* the front-cam mirror so the
flip lives in screen space, not sensor space).
- UxCameraPreviewSizeChanged event. Android emits this from
PreviewSink.onResize whenever a SurfaceRequest carries a new
resolution; the controller copies it into value.previewSize.
First emission is what unblocks the camera_thumb's SizedBox
from its initial 0x0 = "render nothing" state.
- UxCameraBackend.setDescription's return changed from `Size` to
`({Size previewSize, int previewRotationQuarterTurns})` so
a lens swap can both update the rotation and signal that a new
previewSizeChanged event is incoming.
iOS continues to send previewSize in the create result (the active
format is known synchronously); no previewSizeChanged emission is
needed there. The new field is set to 0 in both create and
setDescription results on iOS.
This commit is contained in:
@@ -46,6 +46,19 @@ class CameraInstance(
|
||||
val currentLens: String get() = lens
|
||||
val audioClaimed: Boolean get() = enableAudio
|
||||
|
||||
/// Number of 90° CW rotations the Flutter Texture needs so the
|
||||
/// sensor frames display upright on a portrait screen. Derived
|
||||
/// from the active camera's `sensorRotationDegrees`.
|
||||
val previewRotationQuarterTurns: Int get() {
|
||||
val degrees = cameraProvider?.let {
|
||||
val selector = if (lens == "front") CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
else CameraSelector.DEFAULT_BACK_CAMERA
|
||||
selector.filter(it.availableCameraInfos).firstOrNull()
|
||||
?.sensorRotationDegrees ?: 0
|
||||
} ?: 0
|
||||
return ((degrees % 360 + 360) % 360) / 90
|
||||
}
|
||||
|
||||
/// Bind the provider against [lens]. Resolves the
|
||||
/// [ProcessCameraProvider.getInstance] future on the main executor
|
||||
/// before invoking [onReady]. [onReady] fires with success or the
|
||||
@@ -59,6 +72,16 @@ class CameraInstance(
|
||||
this.lens = lens
|
||||
this.enableAudio = enableAudio
|
||||
|
||||
previewSink.onResize = { size ->
|
||||
emit(mapOf(
|
||||
"event" to "previewSizeChanged",
|
||||
"previewSize" to mapOf(
|
||||
"width" to size.width.toDouble(),
|
||||
"height" to size.height.toDouble(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
val future = ProcessCameraProvider.getInstance(context)
|
||||
future.addListener({
|
||||
if (disposed) return@addListener
|
||||
|
||||
@@ -224,6 +224,7 @@ class CameraPlugin :
|
||||
"width" to instance.previewSize.width.toDouble(),
|
||||
"height" to instance.previewSize.height.toDouble(),
|
||||
),
|
||||
"previewRotationQuarterTurns" to instance.previewRotationQuarterTurns,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -278,6 +279,7 @@ class CameraPlugin :
|
||||
"width" to size.width.toDouble(),
|
||||
"height" to size.height.toDouble(),
|
||||
),
|
||||
"previewRotationQuarterTurns" to instance.previewRotationQuarterTurns,
|
||||
))
|
||||
} catch (t: Throwable) {
|
||||
if (oldLens != newLens) lensesInUse.remove(newLens)
|
||||
|
||||
@@ -29,12 +29,21 @@ class PreviewSink(
|
||||
var previewSize: android.util.Size = android.util.Size(0, 0)
|
||||
private set
|
||||
|
||||
/// Fired every time CameraX hands us a [SurfaceRequest] with a
|
||||
/// (possibly new) resolution. The [CameraInstance] forwards this
|
||||
/// to Dart as a `previewSizeChanged` event so the controller's
|
||||
/// `value.previewSize` updates after CameraX's first negotiation
|
||||
/// (which lands after `create` has already returned).
|
||||
var onResize: ((android.util.Size) -> Unit)? = null
|
||||
|
||||
/// `Preview.SurfaceProvider` callback. Pass directly to
|
||||
/// `previewUseCase.setSurfaceProvider(sink::provideSurface)`.
|
||||
fun provideSurface(request: SurfaceRequest) {
|
||||
val res = request.resolution
|
||||
surfaceTexture.setDefaultBufferSize(res.width, res.height)
|
||||
val changed = res != previewSize
|
||||
previewSize = res
|
||||
if (changed) onResize?.invoke(res)
|
||||
|
||||
// Tear down any prior surface BEFORE handing out the new one —
|
||||
// CameraX retains it until provideSurface's listener fires.
|
||||
|
||||
Reference in New Issue
Block a user