Phase 1c+1d of the ux.camera plan (see ~/banlu/plans/ux_camera.md).
lib/src/camera/camera.dart — UxCameraController (ValueNotifier),
UxCameraValue, UxCameraDescription,
enums, UxCameraException,
uxAvailableCameras().
lib/src/camera/camera_backend.dart — abstract UxCameraBackend +
UxCameraCreateResult + sealed
UxCameraEvent variants.
lib/src/camera/camera_channel.dart — MethodChannelUxCameraBackend over
ux/camera + ux/camera/events. Per-
handle event demux. Maps
PlatformException → UxCameraException.
lib/src/camera/camera_preview.dart — UxCameraPreview: Texture-backed,
Hero-flightable preview widget.
lib/src/testing/fake_camera.dart — FakeUxCameraBackend with per-method
call lists + emitXxx event injection.
Exported from package:ux/testing.dart.
test/camera/camera_controller_test — 16 tests covering init/dispose,
orientation events, takePicture
(explicit + UxSensor fallback),
startVideoRecording / stop,
flip, flash, lock/unlock,
multi-instance, error propagation.
test/camera/camera_channel_test — 10 tests pinning the wire format
for every method + PlatformException
mapping.
Orientation snapshot for capture is computed Dart-side and passed in as an
explicit arg to takePicture / startVideoRecording (default falls back to
UxSensor.orientation at call time). Native never queries UIDevice itself
for the snapshot — Dart-side fakes drive orientation deterministically.
Native plugin code lands in Phase 2+; today every channel call throws
MissingPluginException at runtime, which is fine — the controller is only
mounted from the camera page once Phase 5 cuts over. The test backend
already exercises the full controller surface.
199 lines
6.2 KiB
Dart
199 lines
6.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:ui' show Size;
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:ux/src/camera/camera.dart';
|
|
import 'package:ux/src/camera/camera_backend.dart';
|
|
import 'package:ux/src/file.dart' show UxFile;
|
|
|
|
/// In-memory backend for [UxCameraController] tests. Swap in via
|
|
/// `UxCameraBackend.instance = FakeUxCameraBackend(...)` before any UI
|
|
/// mounts; restore with `UxCameraBackend.instance =
|
|
/// MethodChannelUxCameraBackend()` in `tearDown`.
|
|
///
|
|
/// Captures every call into per-method lists for assertions, and lets
|
|
/// tests drive events deterministically via [emitOrientationChanged]
|
|
/// and friends.
|
|
class FakeUxCameraBackend implements UxCameraBackend {
|
|
FakeUxCameraBackend({
|
|
List<UxCameraDescription> cameras = const [],
|
|
this.previewSize = const Size(1920, 1080),
|
|
this.picturePath = '/tmp/fake_picture.jpg',
|
|
this.videoPath = '/tmp/fake_video.mp4',
|
|
}) : cameras = List.unmodifiable(cameras);
|
|
|
|
// ---- captured calls ---------------------------------------------
|
|
|
|
final List<({String cameraId, bool enableAudio, UxResolutionPreset preset})>
|
|
createCalls = [];
|
|
final List<int> initializeCalls = [];
|
|
final List<int> disposeCalls = [];
|
|
final List<({int handle, String cameraId})> setDescriptionCalls = [];
|
|
final List<({int handle, UxFlashMode mode})> setFlashModeCalls = [];
|
|
final List<({int handle, DeviceOrientation orientation})>
|
|
lockCaptureOrientationCalls = [];
|
|
final List<int> unlockCaptureOrientationCalls = [];
|
|
final List<({int handle, DeviceOrientation snapshotOrientation})>
|
|
takePictureCalls = [];
|
|
final List<({int handle, DeviceOrientation snapshotOrientation})>
|
|
startVideoRecordingCalls = [];
|
|
final List<int> stopVideoRecordingCalls = [];
|
|
int availableCamerasCalls = 0;
|
|
|
|
// ---- configurable returns ---------------------------------------
|
|
|
|
/// Cameras returned by [availableCameras]. Mutable so tests can swap
|
|
/// the set between assertions.
|
|
List<UxCameraDescription> cameras;
|
|
Size previewSize;
|
|
String picturePath;
|
|
String videoPath;
|
|
|
|
/// If non-null, [create] throws this exception instead of returning
|
|
/// a result — exercises permission denial / device busy paths.
|
|
UxCameraException? createError;
|
|
|
|
/// Optional override; set to throw on any particular method to
|
|
/// drive failure paths.
|
|
UxCameraException? initializeError;
|
|
UxCameraException? takePictureError;
|
|
UxCameraException? startVideoRecordingError;
|
|
UxCameraException? stopVideoRecordingError;
|
|
|
|
// ---- internal ---------------------------------------------------
|
|
|
|
int _nextHandle = 1;
|
|
int _nextTextureId = 100;
|
|
final Map<int, StreamController<UxCameraEvent>> _eventControllers = {};
|
|
|
|
StreamController<UxCameraEvent> _controllerFor(int handle) {
|
|
return _eventControllers.putIfAbsent(
|
|
handle,
|
|
() => StreamController<UxCameraEvent>.broadcast(),
|
|
);
|
|
}
|
|
|
|
// ---- event injection --------------------------------------------
|
|
|
|
/// Push a `deviceOrientationChanged` event for [handle]. The
|
|
/// subscribed controller will update its `value.deviceOrientation`.
|
|
void emitOrientationChanged(int handle, DeviceOrientation orientation) {
|
|
_controllerFor(handle)
|
|
.add(UxCameraDeviceOrientationChanged(handle, orientation));
|
|
}
|
|
|
|
void emitSessionError(int handle, String code, [String? description]) {
|
|
_controllerFor(handle).add(UxCameraSessionError(handle, code, description));
|
|
}
|
|
|
|
void emitSessionInterrupted(int handle, String reason) {
|
|
_controllerFor(handle).add(UxCameraSessionInterrupted(handle, reason));
|
|
}
|
|
|
|
void emitSessionResumed(int handle) {
|
|
_controllerFor(handle).add(UxCameraSessionResumed(handle));
|
|
}
|
|
|
|
// ---- UxCameraBackend impl --------------------------------------
|
|
|
|
@override
|
|
Future<List<UxCameraDescription>> availableCameras() async {
|
|
availableCamerasCalls += 1;
|
|
return cameras;
|
|
}
|
|
|
|
@override
|
|
Future<UxCameraCreateResult> create({
|
|
required String cameraId,
|
|
required bool enableAudio,
|
|
required UxResolutionPreset preset,
|
|
}) async {
|
|
createCalls.add((
|
|
cameraId: cameraId,
|
|
enableAudio: enableAudio,
|
|
preset: preset,
|
|
));
|
|
final err = createError;
|
|
if (err != null) throw err;
|
|
final handle = _nextHandle++;
|
|
final textureId = _nextTextureId++;
|
|
return UxCameraCreateResult(
|
|
handle: handle,
|
|
textureId: textureId,
|
|
previewSize: previewSize,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> initialize(int handle) async {
|
|
initializeCalls.add(handle);
|
|
final err = initializeError;
|
|
if (err != null) throw err;
|
|
}
|
|
|
|
@override
|
|
Future<void> disposeInstance(int handle) async {
|
|
disposeCalls.add(handle);
|
|
await _eventControllers.remove(handle)?.close();
|
|
}
|
|
|
|
@override
|
|
Future<Size> setDescription(int handle, String cameraId) async {
|
|
setDescriptionCalls.add((handle: handle, cameraId: cameraId));
|
|
return previewSize;
|
|
}
|
|
|
|
@override
|
|
Future<void> setFlashMode(int handle, UxFlashMode mode) async {
|
|
setFlashModeCalls.add((handle: handle, mode: mode));
|
|
}
|
|
|
|
@override
|
|
Future<void> lockCaptureOrientation(
|
|
int handle,
|
|
DeviceOrientation orientation,
|
|
) async {
|
|
lockCaptureOrientationCalls
|
|
.add((handle: handle, orientation: orientation));
|
|
}
|
|
|
|
@override
|
|
Future<void> unlockCaptureOrientation(int handle) async {
|
|
unlockCaptureOrientationCalls.add(handle);
|
|
}
|
|
|
|
@override
|
|
Future<UxFile> takePicture(
|
|
int handle,
|
|
DeviceOrientation snapshotOrientation,
|
|
) async {
|
|
takePictureCalls
|
|
.add((handle: handle, snapshotOrientation: snapshotOrientation));
|
|
final err = takePictureError;
|
|
if (err != null) throw err;
|
|
return UxFile(picturePath);
|
|
}
|
|
|
|
@override
|
|
Future<void> startVideoRecording(
|
|
int handle,
|
|
DeviceOrientation snapshotOrientation,
|
|
) async {
|
|
startVideoRecordingCalls
|
|
.add((handle: handle, snapshotOrientation: snapshotOrientation));
|
|
final err = startVideoRecordingError;
|
|
if (err != null) throw err;
|
|
}
|
|
|
|
@override
|
|
Future<UxFile> stopVideoRecording(int handle) async {
|
|
stopVideoRecordingCalls.add(handle);
|
|
final err = stopVideoRecordingError;
|
|
if (err != null) throw err;
|
|
return UxFile(videoPath);
|
|
}
|
|
|
|
@override
|
|
Stream<UxCameraEvent> events(int handle) => _controllerFor(handle).stream;
|
|
}
|