camera: iOS implementation (Phase 2+3)

Native plugin owning AVCaptureSession + AVAssetWriter, mirroring
telegram-iOS's Camera module decomposition. Photo + video capture with
the writer-track transform set from a per-call orientation snapshot
(the three-way preview/capture/device split that camera_avfoundation
can't give us).

Modules:
  CameraPlugin           channels + per-handle instance map
  CameraInstance         session + texture + outputs + recorder
  CameraSession          AVCaptureSession + runtime-error/interrupt obs
  CaptureDevice          front/back discovery, per-device config
  PhotoOutput            AVCapturePhotoOutput, per-shot orientation
  VideoRecorder          AVAssetWriter, lazy inputs, pending-audio queue,
                         stop()/cancel() pair (matches telegram)
  PreviewSink            CVPixelBuffer → FlutterTexture
  AudioSession           setCategory + setActive(true) (only-widen)
  DeviceOrientationBridge

Recorder details:
  - Lazy videoInput/audioInput on first sample, sourceFormatHint:.
  - Audio settings derived from CMAudioFormatDescriptionGet*
    + recommendedAudioSettingsForAssetWriter, gated startWriting.
  - Stop sets stopSampleTime; next sample crossing it triggers
    maybeFinish → finishWriting. No watchdog — telegram pattern.
  - cancel() drops pending audio + cancelWriting + deletes file,
    used by CameraInstance.dispose when teardown finds in-flight
    recording.
  - Diagnostic stream → ux/camera/events {event: "diagnostic"}.

Dart surface extensions over Phase 1:
  - UxCameraValue.audioPermissionGranted
  - UxCameraController.refreshAudioPermission()
  - Static UxCameraController.audioPermissionGranted() /
    openSystemSettings()
  - UxCameraDiagnostic event variant
  - FakeUxCameraBackend.{emitDiagnostic, audioPermission,
    openSettingsCalls}

Tests: 32/32 in test/camera (controller + channel) green.
This commit is contained in:
agra
2026-05-13 16:56:49 +03:00
parent 45aac312a8
commit 6d6a871c53
18 changed files with 2337 additions and 22 deletions

View File

@@ -85,6 +85,15 @@ abstract class UxCameraBackend {
/// controller subscribes during [initialize] and unsubscribes on
/// [disposeInstance].
Stream<UxCameraEvent> events(int handle);
/// True iff the user has granted microphone access. Cheap; safe to
/// re-poll on app foregrounding to detect grants made via Settings.
Future<bool> audioPermissionGranted();
/// Deep-link into the system Settings page for this app. Caller is
/// expected to refresh [audioPermissionGranted] on
/// `AppLifecycleState.resumed`.
Future<void> openSettings();
}
/// The tuple returned by [UxCameraBackend.create] — everything the
@@ -128,3 +137,11 @@ class UxCameraSessionInterrupted extends UxCameraEvent {
class UxCameraSessionResumed extends UxCameraEvent {
const UxCameraSessionResumed(super.handle);
}
/// 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`).
class UxCameraDiagnostic extends UxCameraEvent {
const UxCameraDiagnostic(super.handle, this.message);
final String message;
}