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 currentLens: String get() = lens
|
||||||
val audioClaimed: Boolean get() = enableAudio
|
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
|
/// Bind the provider against [lens]. Resolves the
|
||||||
/// [ProcessCameraProvider.getInstance] future on the main executor
|
/// [ProcessCameraProvider.getInstance] future on the main executor
|
||||||
/// before invoking [onReady]. [onReady] fires with success or the
|
/// before invoking [onReady]. [onReady] fires with success or the
|
||||||
@@ -59,6 +72,16 @@ class CameraInstance(
|
|||||||
this.lens = lens
|
this.lens = lens
|
||||||
this.enableAudio = enableAudio
|
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)
|
val future = ProcessCameraProvider.getInstance(context)
|
||||||
future.addListener({
|
future.addListener({
|
||||||
if (disposed) return@addListener
|
if (disposed) return@addListener
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ class CameraPlugin :
|
|||||||
"width" to instance.previewSize.width.toDouble(),
|
"width" to instance.previewSize.width.toDouble(),
|
||||||
"height" to instance.previewSize.height.toDouble(),
|
"height" to instance.previewSize.height.toDouble(),
|
||||||
),
|
),
|
||||||
|
"previewRotationQuarterTurns" to instance.previewRotationQuarterTurns,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +279,7 @@ class CameraPlugin :
|
|||||||
"width" to size.width.toDouble(),
|
"width" to size.width.toDouble(),
|
||||||
"height" to size.height.toDouble(),
|
"height" to size.height.toDouble(),
|
||||||
),
|
),
|
||||||
|
"previewRotationQuarterTurns" to instance.previewRotationQuarterTurns,
|
||||||
))
|
))
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
if (oldLens != newLens) lensesInUse.remove(newLens)
|
if (oldLens != newLens) lensesInUse.remove(newLens)
|
||||||
|
|||||||
@@ -29,12 +29,21 @@ class PreviewSink(
|
|||||||
var previewSize: android.util.Size = android.util.Size(0, 0)
|
var previewSize: android.util.Size = android.util.Size(0, 0)
|
||||||
private set
|
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
|
/// `Preview.SurfaceProvider` callback. Pass directly to
|
||||||
/// `previewUseCase.setSurfaceProvider(sink::provideSurface)`.
|
/// `previewUseCase.setSurfaceProvider(sink::provideSurface)`.
|
||||||
fun provideSurface(request: SurfaceRequest) {
|
fun provideSurface(request: SurfaceRequest) {
|
||||||
val res = request.resolution
|
val res = request.resolution
|
||||||
surfaceTexture.setDefaultBufferSize(res.width, res.height)
|
surfaceTexture.setDefaultBufferSize(res.width, res.height)
|
||||||
|
val changed = res != previewSize
|
||||||
previewSize = res
|
previewSize = res
|
||||||
|
if (changed) onResize?.invoke(res)
|
||||||
|
|
||||||
// Tear down any prior surface BEFORE handing out the new one —
|
// Tear down any prior surface BEFORE handing out the new one —
|
||||||
// CameraX retains it until provideSurface's listener fires.
|
// CameraX retains it until provideSurface's listener fires.
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
|
|||||||
"previewSize": [
|
"previewSize": [
|
||||||
"width": size.width,
|
"width": size.width,
|
||||||
"height": size.height,
|
"height": size.height,
|
||||||
]
|
],
|
||||||
|
"previewRotationQuarterTurns": 0,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
@@ -344,6 +345,10 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
|
|||||||
"width": size.width,
|
"width": size.width,
|
||||||
"height": size.height,
|
"height": size.height,
|
||||||
],
|
],
|
||||||
|
// iOS pre-rotates frames via the data-output
|
||||||
|
// connection's videoOrientation, so the Flutter
|
||||||
|
// Texture displays upright as-is.
|
||||||
|
"previewRotationQuarterTurns": 0,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class UxCameraValue {
|
|||||||
this.deviceOrientation = DeviceOrientation.portraitUp,
|
this.deviceOrientation = DeviceOrientation.portraitUp,
|
||||||
this.enableAudio = false,
|
this.enableAudio = false,
|
||||||
this.audioPermissionGranted = false,
|
this.audioPermissionGranted = false,
|
||||||
|
this.previewRotationQuarterTurns = 0,
|
||||||
this.errorDescription,
|
this.errorDescription,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,6 +104,16 @@ class UxCameraValue {
|
|||||||
/// no audio track and callers should surface a hint.
|
/// no audio track and callers should surface a hint.
|
||||||
final bool audioPermissionGranted;
|
final bool audioPermissionGranted;
|
||||||
|
|
||||||
|
/// Number of 90° clockwise rotations the preview Texture needs so
|
||||||
|
/// the sensor frames display upright. On iOS the data output
|
||||||
|
/// pre-rotates via `AVCaptureConnection.videoOrientation` and this
|
||||||
|
/// is always 0. On Android CameraX delivers sensor-native frames to
|
||||||
|
/// the Surface and the consumer rotates — typically 1 for
|
||||||
|
/// `sensorOrientation: 90` back cams and 3 for `sensorOrientation:
|
||||||
|
/// 270` front cams. Applied inside [UxCameraPreview]; consumers
|
||||||
|
/// don't read this directly.
|
||||||
|
final int previewRotationQuarterTurns;
|
||||||
|
|
||||||
/// Set to the last native session error's message when one fires.
|
/// Set to the last native session error's message when one fires.
|
||||||
/// Cleared on the next successful state transition.
|
/// Cleared on the next successful state transition.
|
||||||
final String? errorDescription;
|
final String? errorDescription;
|
||||||
@@ -117,6 +128,7 @@ class UxCameraValue {
|
|||||||
DeviceOrientation? deviceOrientation,
|
DeviceOrientation? deviceOrientation,
|
||||||
bool? enableAudio,
|
bool? enableAudio,
|
||||||
bool? audioPermissionGranted,
|
bool? audioPermissionGranted,
|
||||||
|
int? previewRotationQuarterTurns,
|
||||||
Object? errorDescription = _unset,
|
Object? errorDescription = _unset,
|
||||||
}) =>
|
}) =>
|
||||||
UxCameraValue(
|
UxCameraValue(
|
||||||
@@ -127,6 +139,8 @@ class UxCameraValue {
|
|||||||
deviceOrientation: deviceOrientation ?? this.deviceOrientation,
|
deviceOrientation: deviceOrientation ?? this.deviceOrientation,
|
||||||
enableAudio: enableAudio ?? this.enableAudio,
|
enableAudio: enableAudio ?? this.enableAudio,
|
||||||
audioPermissionGranted: audioPermissionGranted ?? this.audioPermissionGranted,
|
audioPermissionGranted: audioPermissionGranted ?? this.audioPermissionGranted,
|
||||||
|
previewRotationQuarterTurns:
|
||||||
|
previewRotationQuarterTurns ?? this.previewRotationQuarterTurns,
|
||||||
errorDescription: identical(errorDescription, _unset)
|
errorDescription: identical(errorDescription, _unset)
|
||||||
? this.errorDescription
|
? this.errorDescription
|
||||||
: errorDescription as String?,
|
: errorDescription as String?,
|
||||||
@@ -211,6 +225,7 @@ class UxCameraController extends ValueNotifier<UxCameraValue> {
|
|||||||
value = value.copyWith(
|
value = value.copyWith(
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
previewSize: result.previewSize,
|
previewSize: result.previewSize,
|
||||||
|
previewRotationQuarterTurns: result.previewRotationQuarterTurns,
|
||||||
audioPermissionGranted: audioGranted,
|
audioPermissionGranted: audioGranted,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -235,6 +250,8 @@ class UxCameraController extends ValueNotifier<UxCameraValue> {
|
|||||||
break;
|
break;
|
||||||
case UxCameraDiagnostic(:final message):
|
case UxCameraDiagnostic(:final message):
|
||||||
_log.i('recorder: $message');
|
_log.i('recorder: $message');
|
||||||
|
case UxCameraPreviewSizeChanged(:final previewSize):
|
||||||
|
value = value.copyWith(previewSize: previewSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +274,12 @@ class UxCameraController extends ValueNotifier<UxCameraValue> {
|
|||||||
/// recording is in flight — call [stopVideoRecording] first.
|
/// recording is in flight — call [stopVideoRecording] first.
|
||||||
Future<void> setDescription(UxCameraDescription description) async {
|
Future<void> setDescription(UxCameraDescription description) async {
|
||||||
final handle = _requireHandle('setDescription');
|
final handle = _requireHandle('setDescription');
|
||||||
final size = await UxCameraBackend.instance.setDescription(handle, description.id);
|
final r = await UxCameraBackend.instance.setDescription(handle, description.id);
|
||||||
value = value.copyWith(description: description, previewSize: size);
|
value = value.copyWith(
|
||||||
|
description: description,
|
||||||
|
previewSize: r.previewSize,
|
||||||
|
previewRotationQuarterTurns: r.previewRotationQuarterTurns,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setFlashMode(UxFlashMode mode) async {
|
Future<void> setFlashMode(UxFlashMode mode) async {
|
||||||
|
|||||||
@@ -47,8 +47,13 @@ abstract class UxCameraBackend {
|
|||||||
Future<void> disposeInstance(int handle);
|
Future<void> disposeInstance(int handle);
|
||||||
|
|
||||||
/// Swap to a different camera mid-session. Resets the lock and clears
|
/// Swap to a different camera mid-session. Resets the lock and clears
|
||||||
/// any pending recording. Returns the new preview size.
|
/// any pending recording. Returns the new preview size and rotation;
|
||||||
Future<Size> setDescription(int handle, String cameraId);
|
/// 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
|
/// Set the flash mode used for the next [takePicture]. On front cameras
|
||||||
/// without a screen-flash fallback the backend silently no-ops; the
|
/// without a screen-flash fallback the backend silently no-ops; the
|
||||||
@@ -104,11 +109,21 @@ class UxCameraCreateResult {
|
|||||||
required this.handle,
|
required this.handle,
|
||||||
required this.textureId,
|
required this.textureId,
|
||||||
required this.previewSize,
|
required this.previewSize,
|
||||||
|
this.previewRotationQuarterTurns = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int handle;
|
final int handle;
|
||||||
final int textureId;
|
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;
|
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 —
|
/// Events pushed by the native side over `ux/camera/events`. Sealed —
|
||||||
@@ -138,6 +153,16 @@ class UxCameraSessionResumed extends UxCameraEvent {
|
|||||||
const UxCameraSessionResumed(super.handle);
|
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
|
/// Free-text diagnostic message from the native recorder. Routed by
|
||||||
/// the controller to `Log.tag('camera').i(...)` so it lands in the
|
/// the controller to `Log.tag('camera').i(...)` so it lands in the
|
||||||
/// log_server pipeline (`~/banlu/tools/log_server/data/banlu.jsonl`).
|
/// log_server pipeline (`~/banlu/tools/log_server/data/banlu.jsonl`).
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class MethodChannelUxCameraBackend implements UxCameraBackend {
|
|||||||
(size['width'] as num).toDouble(),
|
(size['width'] as num).toDouble(),
|
||||||
(size['height'] as num).toDouble(),
|
(size['height'] as num).toDouble(),
|
||||||
),
|
),
|
||||||
|
previewRotationQuarterTurns:
|
||||||
|
(m['previewRotationQuarterTurns'] as num?)?.toInt() ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,15 +72,22 @@ class MethodChannelUxCameraBackend implements UxCameraBackend {
|
|||||||
_invokeVoid('dispose', {'handle': handle});
|
_invokeVoid('dispose', {'handle': handle});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Size> setDescription(int handle, String cameraId) async {
|
Future<({Size previewSize, int previewRotationQuarterTurns})> setDescription(
|
||||||
|
int handle,
|
||||||
|
String cameraId,
|
||||||
|
) async {
|
||||||
final m = await _invokeMap('setDescription', {
|
final m = await _invokeMap('setDescription', {
|
||||||
'handle': handle,
|
'handle': handle,
|
||||||
'cameraId': cameraId,
|
'cameraId': cameraId,
|
||||||
});
|
});
|
||||||
final s = (m['previewSize'] as Map).cast<Object?, Object?>();
|
final s = (m['previewSize'] as Map).cast<Object?, Object?>();
|
||||||
return Size(
|
return (
|
||||||
(s['width'] as num).toDouble(),
|
previewSize: Size(
|
||||||
(s['height'] as num).toDouble(),
|
(s['width'] as num).toDouble(),
|
||||||
|
(s['height'] as num).toDouble(),
|
||||||
|
),
|
||||||
|
previewRotationQuarterTurns:
|
||||||
|
(m['previewRotationQuarterTurns'] as num?)?.toInt() ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +182,15 @@ class MethodChannelUxCameraBackend implements UxCameraBackend {
|
|||||||
handle,
|
handle,
|
||||||
m['message'] as String? ?? '',
|
m['message'] as String? ?? '',
|
||||||
);
|
);
|
||||||
|
case 'previewSizeChanged':
|
||||||
|
final s = (m['previewSize'] as Map).cast<Object?, Object?>();
|
||||||
|
return UxCameraPreviewSizeChanged(
|
||||||
|
handle,
|
||||||
|
Size(
|
||||||
|
(s['width'] as num).toDouble(),
|
||||||
|
(s['height'] as num).toDouble(),
|
||||||
|
),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return UxCameraSessionError(handle, 'unknown_event', null);
|
return UxCameraSessionError(handle, 'unknown_event', null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,18 @@ class UxCameraPreview extends StatelessWidget {
|
|||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
final id = controller.textureId;
|
final id = controller.textureId;
|
||||||
if (id == null) return SizedBox.expand();
|
if (id == null) return SizedBox.expand();
|
||||||
final mirror = value.description.lens == UxCameraLens.front;
|
Widget child = Texture(textureId: id);
|
||||||
final texture = Texture(textureId: id);
|
// Order matters: rotate first (sensor → screen frame), then
|
||||||
return mirror
|
// mirror in screen space. Reversing them would mirror across
|
||||||
? Transform.flip(flipX: true, child: texture)
|
// an axis that's still aligned with the sensor.
|
||||||
: texture;
|
final turns = value.previewRotationQuarterTurns;
|
||||||
|
if (turns != 0) {
|
||||||
|
child = RotatedBox(quarterTurns: turns, child: child);
|
||||||
|
}
|
||||||
|
if (value.description.lens == UxCameraLens.front) {
|
||||||
|
child = Transform.flip(flipX: true, child: child);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,9 +148,18 @@ class FakeUxCameraBackend implements UxCameraBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Size> setDescription(int handle, String cameraId) async {
|
Future<({Size previewSize, int previewRotationQuarterTurns})> setDescription(
|
||||||
|
int handle,
|
||||||
|
String cameraId,
|
||||||
|
) async {
|
||||||
setDescriptionCalls.add((handle: handle, cameraId: cameraId));
|
setDescriptionCalls.add((handle: handle, cameraId: cameraId));
|
||||||
return previewSize;
|
return (previewSize: previewSize, previewRotationQuarterTurns: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a `previewSizeChanged` event for [handle]. Tests that need
|
||||||
|
/// to simulate Android's async preview-size resolve drive this.
|
||||||
|
void emitPreviewSizeChanged(int handle, Size size) {
|
||||||
|
_controllerFor(handle).add(UxCameraPreviewSizeChanged(handle, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export 'src/bend_box.dart';
|
|||||||
export 'src/json_extension.dart';
|
export 'src/json_extension.dart';
|
||||||
export 'src/bezier.dart';
|
export 'src/bezier.dart';
|
||||||
export 'src/camera/camera.dart';
|
export 'src/camera/camera.dart';
|
||||||
export 'src/camera/camera_backend.dart' show UxCameraBackend, UxCameraCreateResult, UxCameraEvent, UxCameraDeviceOrientationChanged, UxCameraSessionError, UxCameraSessionInterrupted, UxCameraSessionResumed, UxCameraDiagnostic;
|
export 'src/camera/camera_backend.dart' show UxCameraBackend, UxCameraCreateResult, UxCameraEvent, UxCameraDeviceOrientationChanged, UxCameraSessionError, UxCameraSessionInterrupted, UxCameraSessionResumed, UxCameraDiagnostic, UxCameraPreviewSizeChanged;
|
||||||
export 'src/camera/camera_channel.dart' show MethodChannelUxCameraBackend;
|
export 'src/camera/camera_channel.dart' show MethodChannelUxCameraBackend;
|
||||||
export 'src/camera/camera_preview.dart';
|
export 'src/camera/camera_preview.dart';
|
||||||
export 'src/clipboard.dart';
|
export 'src/clipboard.dart';
|
||||||
|
|||||||
@@ -81,16 +81,18 @@ void main() {
|
|||||||
expect(calls.every((c) => (c.arguments as Map)['handle'] == 11), isTrue);
|
expect(calls.every((c) => (c.arguments as Map)['handle'] == 11), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setDescription returns the new previewSize', () async {
|
test('setDescription returns previewSize + rotation', () async {
|
||||||
handle((_) => {
|
handle((_) => {
|
||||||
'previewSize': {'width': 1280, 'height': 720},
|
'previewSize': {'width': 1280, 'height': 720},
|
||||||
|
'previewRotationQuarterTurns': 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
final size = await backend.setDescription(7, 'next');
|
final r = await backend.setDescription(7, 'next');
|
||||||
|
|
||||||
expect(calls.single.method, 'setDescription');
|
expect(calls.single.method, 'setDescription');
|
||||||
expect(calls.single.arguments, {'handle': 7, 'cameraId': 'next'});
|
expect(calls.single.arguments, {'handle': 7, 'cameraId': 'next'});
|
||||||
expect(size, const Size(1280, 720));
|
expect(r.previewSize, const Size(1280, 720));
|
||||||
|
expect(r.previewRotationQuarterTurns, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setFlashMode encodes the enum', () async {
|
test('setFlashMode encodes the enum', () async {
|
||||||
|
|||||||
Reference in New Issue
Block a user