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:
@@ -109,10 +109,29 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
withInstance(args, result: result) { instance in
|
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 {
|
instance.sessionQueueAsync {
|
||||||
do {
|
do {
|
||||||
let size = try instance.setDescription(cameraId: cameraId)
|
let size = try instance.setDescription(cameraId: cameraId)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if let oldId, oldId != cameraId {
|
||||||
|
self.devicesInUse.remove(oldId)
|
||||||
|
}
|
||||||
result([
|
result([
|
||||||
"previewSize": [
|
"previewSize": [
|
||||||
"width": size.width,
|
"width": size.width,
|
||||||
@@ -122,6 +141,9 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
|
|||||||
}
|
}
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if oldId != cameraId {
|
||||||
|
self.devicesInUse.remove(cameraId)
|
||||||
|
}
|
||||||
result(FlutterError(
|
result(FlutterError(
|
||||||
code: "init_failed",
|
code: "init_failed",
|
||||||
message: error.localizedDescription,
|
message: error.localizedDescription,
|
||||||
@@ -326,7 +348,13 @@ public class CameraPlugin: NSObject, NativePlugin, FlutterStreamHandler {
|
|||||||
}
|
}
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
DispatchQueue.main.async {
|
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)
|
self.instances.removeValue(forKey: handle)
|
||||||
result(FlutterError(
|
result(FlutterError(
|
||||||
code: "init_failed",
|
code: "init_failed",
|
||||||
|
|||||||
Reference in New Issue
Block a user