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:
@@ -47,8 +47,13 @@ abstract class UxCameraBackend {
|
||||
Future<void> disposeInstance(int handle);
|
||||
|
||||
/// Swap to a different camera mid-session. Resets the lock and clears
|
||||
/// any pending recording. Returns the new preview size.
|
||||
Future<Size> setDescription(int handle, String cameraId);
|
||||
/// any pending recording. Returns the new preview size and rotation;
|
||||
/// the size may again be `Size.zero` initially on Android, with a
|
||||
/// follow-up [UxCameraPreviewSizeChanged] event.
|
||||
Future<({Size previewSize, int previewRotationQuarterTurns})> setDescription(
|
||||
int handle,
|
||||
String cameraId,
|
||||
);
|
||||
|
||||
/// Set the flash mode used for the next [takePicture]. On front cameras
|
||||
/// without a screen-flash fallback the backend silently no-ops; the
|
||||
@@ -104,11 +109,21 @@ class UxCameraCreateResult {
|
||||
required this.handle,
|
||||
required this.textureId,
|
||||
required this.previewSize,
|
||||
this.previewRotationQuarterTurns = 0,
|
||||
});
|
||||
|
||||
final int handle;
|
||||
final int textureId;
|
||||
|
||||
/// Initial preview size; may be `Size.zero` when the native side
|
||||
/// can't determine it synchronously (Android CameraX needs the
|
||||
/// first `SurfaceRequest` to fire before it knows). In that case
|
||||
/// a [UxCameraPreviewSizeChanged] event follows.
|
||||
final Size previewSize;
|
||||
|
||||
/// Number of 90° CW rotations the Texture widget needs. iOS: 0.
|
||||
/// Android: derived from the selected camera's sensor orientation.
|
||||
final int previewRotationQuarterTurns;
|
||||
}
|
||||
|
||||
/// Events pushed by the native side over `ux/camera/events`. Sealed —
|
||||
@@ -138,6 +153,16 @@ class UxCameraSessionResumed extends UxCameraEvent {
|
||||
const UxCameraSessionResumed(super.handle);
|
||||
}
|
||||
|
||||
/// Fired when the native side learns or revises the preview's pixel
|
||||
/// dimensions. Android emits this after CameraX's first
|
||||
/// `SurfaceRequest` resolves (the size isn't known at `create` time);
|
||||
/// iOS doesn't emit it (the size lands in the `create` result
|
||||
/// synchronously from `device.activeFormat`).
|
||||
class UxCameraPreviewSizeChanged extends UxCameraEvent {
|
||||
const UxCameraPreviewSizeChanged(super.handle, this.previewSize);
|
||||
final Size previewSize;
|
||||
}
|
||||
|
||||
/// Free-text diagnostic message from the native recorder. Routed by
|
||||
/// the controller to `Log.tag('camera').i(...)` so it lands in the
|
||||
/// log_server pipeline (`~/banlu/tools/log_server/data/banlu.jsonl`).
|
||||
|
||||
Reference in New Issue
Block a user