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.
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../file.dart' show UxFile;
|
||||
import '../log.dart' show Log;
|
||||
import 'camera.dart'
|
||||
show
|
||||
UxCameraDescription,
|
||||
@@ -12,6 +13,8 @@ import 'camera.dart'
|
||||
UxResolutionPreset;
|
||||
import 'camera_backend.dart';
|
||||
|
||||
final _log = Log.tag('camera');
|
||||
|
||||
/// Production [UxCameraBackend]. Hand-rolled MethodChannel +
|
||||
/// EventChannel — matches the rest of `package:ux`, no pigeon.
|
||||
class MethodChannelUxCameraBackend implements UxCameraBackend {
|
||||
@@ -23,8 +26,21 @@ class MethodChannelUxCameraBackend implements UxCameraBackend {
|
||||
/// Single broadcast stream over the native event channel. We
|
||||
/// demultiplex per-handle inside [events] so a Stream subscription
|
||||
/// is cheap (no per-instance EventChannel hop).
|
||||
late final Stream<Object?> _rawEvents =
|
||||
_eventsChannel.receiveBroadcastStream();
|
||||
late final Stream<Object?> _rawEvents = () {
|
||||
final stream = _eventsChannel.receiveBroadcastStream();
|
||||
// Plugin-level diagnostics (handle == -1) flow over the same
|
||||
// channel but aren't claimed by any controller's per-handle
|
||||
// filter. Subscribe once globally so they reach `ux.Log` (and
|
||||
// therefore the log_server jsonl) even before any controller
|
||||
// is alive.
|
||||
stream.listen((e) {
|
||||
final m = (e as Map).cast<Object?, Object?>();
|
||||
if (m['event'] != 'diagnostic') return;
|
||||
if ((m['handle'] as num?)?.toInt() != -1) return;
|
||||
_log.i('plugin: ${m['message'] ?? ''}');
|
||||
});
|
||||
return stream;
|
||||
}();
|
||||
|
||||
@override
|
||||
Future<List<UxCameraDescription>> availableCameras() async {
|
||||
|
||||
Reference in New Issue
Block a user