Commit Graph

3 Commits

Author SHA1 Message Date
agra
a508aca2bb camera: requestAudioPermission — in-app prompt first, settings on permanent denial
Banner-tap entry point that shows the system mic prompt when the OS
will still surface one, and deep-links to Settings only on permanent
denial. Fixes the fresh-install trap where the mic entry isn't in the
Privacy pane until requestAccess has fired at least once.

Android tracks the first-asked state in SharedPreferences because
shouldShowRequestPermissionRationale returns false in two
observationally identical states (never asked vs permanently denied).
The existing initialize() request path writes the flag too, so a
banner tap after a record-then-deny correctly routes to Settings.

Refactored Android pendingPermission into PendingPermission(primary,
kind, cb) so audio-only requests check RECORD_AUDIO results instead
of always checking CAMERA.
2026-05-21 08:50:39 +03:00
agra
8bed5435ad camera: stricter macOS dispose order + plugin diagnostics → ux.Log
Two changes that target the macOS "camera not found" leak after a
few open/close cycles. macOS's `AVCaptureDevice.DiscoverySession`
excludes devices that are still claimed by another session — even
our own zombie session that hasn't fully released its grip on the
hardware. So if dispose leaves the session in a partially torn-down
state, the next `availableCameras` returns empty.

CameraInstance.dispose now:
  - Cancels the recorder (was already there) so the audio
    data output's retain on the recorder drops.
  - Stops the session.
  - **Nils sample-buffer delegates** on the video + audio data
    outputs before removing them. `setSampleBufferDelegate` holds a
    strong reference to the delegate; the macOS reference to our
    `SampleFanout` was transitively keeping the session alive.
  - Removes inputs + outputs inside a single
    `session.configure { … }` block (begin/commitConfiguration) so
    AVFoundation sees the teardown as one atomic transition rather
    than a sequence of partial states. Apple's docs are explicit on
    this; we weren't following.
  - Clears the strong references to the instance vars.

Plugin diagnostics:
  - When availableCameras returns empty, native now emits an event
    `{handle: -1, event: "diagnostic", message: …}` carrying the
    current `devicesInUse`, `audioInUse` and `instances` keys.
    Per-handle diagnostics already flow through a controller's
    `_onEvent`; plugin-level ones (handle == -1) had no path to the
    log_server jsonl.
  - `MethodChannelUxCameraBackend` now subscribes to its raw event
    stream once and pipes any handle=-1 diagnostic through
    `ux.Log.tag('camera').i('plugin: …')`. The subscription kicks in
    when the broadcast stream is first accessed (still lazy —
    matches the prior behavior).

If the macOS "camera not found" reproduces, the jsonl will show
which side leaked: a non-empty `devicesInUse` says our claim
tracking is stale; an empty one says AVFoundation itself is
holding the hardware.
2026-05-13 19:35:36 +03:00
agra
14565ebd7a camera: macOS port via darwin/ split (no shared-file pragmas)
Reuse the AVFoundation Swift files between iOS and macOS without
sprinkling `#if canImport(UIKit)` through them. The split is:

  darwin/Camera/                 platform-shared (AVFoundation only)
    CameraPlugin                 channel + instance map
    CameraInstance               session + outputs + texture
    CameraSession                AVCaptureSession + runtime-error obs
    CaptureDevice                front/back discovery
    PhotoOutput                  AVCapturePhotoOutput
    PreviewSink                  CVPixelBuffer → FlutterTexture
    VideoRecorder                AVAssetWriter
    DeviceOrientation            wire-string enum

  ios/Classes/Camera/            iOS-only impls + extensions
    AudioSession                 AVAudioSession.upgradeForRecording
    DeviceOrientationBridge      UIDevice.orientation listener
    CameraSession+iOS            AVCaptureSessionWasInterrupted obs
                                 + InterruptionReason decode + the
                                 application-audio-session flags
                                 (all iOS-only on AVCaptureSession)
    CameraSettings               UIApplication.openSettingsURLString
    FlutterRegistrar+iOS         method-form of textures/messenger

  macos/Classes/Camera/          macOS no-op stubs (same surface)
    AudioSession                 no-op (no AVAudioSession on macOS)
    DeviceOrientationBridge      no-op (desktops don't rotate)
    CameraSession+macOS          no-op setupPlatform()
    CameraSettings               NSWorkspace → System Settings'
                                 Privacy_Camera pane
    FlutterRegistrar+macOS       property-form of textures/messenger

`CameraSession.init` now calls `setupPlatform()` which each platform
provides via an extension — keeps the iOS-only interruption observer
and the `automaticallyConfiguresApplicationAudioSession` /
`usesApplicationAudioSession` flags (both iOS-only on AVCaptureSession)
out of the shared file. Flash-mode in PhotoOutput uses
`if #available(macOS 11/13, *)` rather than `#if`, since those are
plain version gates not platform splits.

The shared files compile into the iOS pod from `ios/Classes/Camera-shared/`
and into the macOS pod from `macos/Classes/Camera-shared/`, each a
mirror populated by a `prepare_command` in the podspec:

    rm -rf Classes/Camera-shared && cp -R ../darwin/Camera Classes/Camera-shared

Symlinks and `../` source globs both fail — Pathname.glob bails on
symlinks, and CocoaPods silently drops paths that escape the pod
directory. The mirror destinations are .gitignore'd.

macOS UxPlugin now registers CameraPlugin alongside the others.
2026-05-13 18:53:46 +03:00