camera: keep devicesInUse aligned across flip + create failure

Two leak paths surfaced after a flip-then-record-then-pop session
left the front camera claim stranded:

1. setDescription swapped instance.device without telling the plugin —
   devicesInUse still held the original cameraId. After dispose,
   releaseClaim only removed the *current* id, leaving the original
   stuck. Next push of the page hit device_busy on the original cam.
   Fix: setDescription handler now does a contention check, inserts
   the new id and drops the old (or rolls back on swap failure).

2. create's catch path called releaseClaim(for: instance), but if
   configureSession threw before instance.device was set,
   instance.currentCameraId is nil — and the cameraId we inserted on
   line above leaked. Fix: drop the known cameraId + audio claim
   explicitly in the catch.
This commit is contained in:
agra
2026-05-13 17:04:32 +03:00
parent 6d6a871c53
commit 73a69b6374

View File

@@ -109,10 +109,29 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
return
}
withInstance(args, result: result) { instance in
// Contention check against other instances. The instance's
// current cameraId is held by us in `devicesInUse`; only a
// foreign holder should block. (A no-op flip same id
// also passes.)
let oldId = instance.currentCameraId
if oldId != cameraId, self.devicesInUse.contains(cameraId) {
result(FlutterError(
code: "device_busy",
message: cameraId,
details: nil
))
return
}
// Tentatively claim the new id before the async swap so a
// concurrent create can't race us. Roll back on failure.
self.devicesInUse.insert(cameraId)
instance.sessionQueueAsync {
do {
let size = try instance.setDescription(cameraId: cameraId)
DispatchQueue.main.async {
if let oldId, oldId != cameraId {
self.devicesInUse.remove(oldId)
}
result([
"previewSize": [
"width": size.width,
@@ -122,6 +141,9 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
}
} catch let error as NSError {
DispatchQueue.main.async {
if oldId != cameraId {
self.devicesInUse.remove(cameraId)
}
result(FlutterError(
code: "init_failed",
message: error.localizedDescription,
@@ -326,7 +348,13 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
}
} catch let error as NSError {
DispatchQueue.main.async {
self.releaseClaim(for: instance)
// Can't rely on `releaseClaim(for: instance)` here
// if `configureSession` threw before `instance.device`
// was set, `instance.currentCameraId` is nil and the
// claim we inserted on line above would leak. Drop the
// ids we know we inserted explicitly.
self.devicesInUse.remove(cameraId)
if enableAudio { self.audioInUse = false }
self.instances.removeValue(forKey: handle)
result(FlutterError(
code: "init_failed",