Retain failed media routing listener removals for acknowledged retry
This commit is contained in:
1 parent
ee20c8b9b7
commit
2516545f9d
2 files changed
+49
-13
No files matched your search
+47
-12
@@ -64,12 +64,15 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
private var forcedPhone = false
|
||||
private var savePending = false
|
||||
private var modeOwned = false
|
||||
private class OutputRegistration {
|
||||
private class OutputRegistration(val session: Int) {
|
||||
lateinit var listener: AudioRouting.OnRoutingChangedListener
|
||||
var reported = false
|
||||
var deviceId: Int? = null
|
||||
}
|
||||
private val outputs = linkedMapOf<AudioTrack, OutputRegistration>()
|
||||
private val pendingOutputs = linkedMapOf<AudioTrack, OutputRegistration>() // Неактивные listeners до SDK ACK, callback не читает track.
|
||||
private var outputCleanup: ScheduledFuture<*>? = null
|
||||
private var outputCleanupDelay = 250L
|
||||
private var confirmationTimeout: ScheduledFuture<*>? = null
|
||||
private var wantedOutput: AudioDeviceInfo? = null
|
||||
private var wantedInputName: String? = null
|
||||
@@ -139,8 +142,8 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
"Media audio session closing/closed"
|
||||
}
|
||||
check(!CallController.blocksMedia()) { "Media output blocked by call" }
|
||||
check(track !in outputs) { "AudioTrack already registered" }
|
||||
val owner = OutputRegistration()
|
||||
check(track !in outputs && track !in pendingOutputs) { "AudioTrack already registered or cleanup pending" }
|
||||
val owner = OutputRegistration(track.audioSessionId)
|
||||
owner.listener = AudioRouting.OnRoutingChangedListener {
|
||||
enqueue {
|
||||
if (outputs[track] !== owner) return@enqueue
|
||||
@@ -161,9 +164,9 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
ensureTimeout()
|
||||
confirm()
|
||||
} catch (e: Exception) {
|
||||
outputs.remove(track)
|
||||
try { track.removeOnRoutingChangedListener(owner.listener) } catch (cleanup: Exception) {
|
||||
LogManager.addLog("WARN", "media_audio", "output listener cleanup failed session=${track.audioSessionId}: $cleanup")
|
||||
try { detachOutput(track, owner) } catch (cleanup: Exception) {
|
||||
e.addSuppressed(cleanup)
|
||||
LogManager.addLog("ERROR", "media_audio", "output listener cleanup failed session=${owner.session}; retained: $cleanup")
|
||||
}
|
||||
LogManager.addLog("ERROR", "media_audio", "output registration failed session=${track.audioSessionId}: $e")
|
||||
recover(e)
|
||||
@@ -176,18 +179,48 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
if (closed) return@AutoCloseable
|
||||
checkBlockingCaller()
|
||||
io.submit {
|
||||
if (outputs[track] !== registration) return@submit
|
||||
outputs.remove(track)
|
||||
try { track.removeOnRoutingChangedListener(registration.listener) } catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "media_audio", "output listener removal failed session=${track.audioSessionId}: $e")
|
||||
}
|
||||
if (outputs[track] !== registration && pendingOutputs[track] !== registration) return@submit
|
||||
detachOutput(track, registration)
|
||||
try { reconcile() }
|
||||
catch (e: Exception) { recover(e) }
|
||||
LogManager.addLog("INFO", "media_audio", "output detached session=${track.audioSessionId} count=${outputs.size}")
|
||||
LogManager.addLog("INFO", "media_audio", "output detached session=${registration.session} count=${outputs.size}")
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun detachOutput(track: AudioTrack, owner: OutputRegistration) {
|
||||
outputs.remove(track); pendingOutputs[track] = owner
|
||||
try { track.removeOnRoutingChangedListener(owner.listener); pendingOutputs.remove(track) } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "media_audio", "output listener removal failed session=${owner.session}; retained: $e")
|
||||
scheduleOutputCleanup(); throw e
|
||||
}
|
||||
if (pendingOutputs.isEmpty()) { outputCleanup?.cancel(false); outputCleanup = null; outputCleanupDelay = 250 }
|
||||
}
|
||||
|
||||
/** Также очищает listener частично неудавшегося attach, у которого caller не получил lease. */
|
||||
private fun cleanupOutputListeners() {
|
||||
var failure: Exception? = null
|
||||
for ((track, owner) in pendingOutputs.toMap()) try { detachOutput(track, owner) } catch (e: Exception) { failure = e }
|
||||
failure?.let { throw it }
|
||||
}
|
||||
|
||||
private fun scheduleOutputCleanup() {
|
||||
if (outputCleanup != null) return
|
||||
val delay = outputCleanupDelay
|
||||
try {
|
||||
outputCleanup = io.schedule({
|
||||
outputCleanup = null
|
||||
try { cleanupOutputListeners() } catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "media_audio", "output listener cleanup pending count=${pendingOutputs.size}: $e")
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS)
|
||||
outputCleanupDelay = (delay * 2).coerceAtMost(30_000)
|
||||
LogManager.addLog("DEBUG", "media_audio", "output listener cleanup scheduled delayMs=$delay count=${pendingOutputs.size}")
|
||||
} catch (e: RejectedExecutionException) {
|
||||
LogManager.addLog("ERROR", "media_audio", "output listener cleanup rejected; owners retained count=${pendingOutputs.size}: $e")
|
||||
}
|
||||
}
|
||||
|
||||
/** Барьер cleanup после остановки всех движков, перед передачей ресурсов Telecom. */
|
||||
fun awaitIdle() {
|
||||
checkBlockingCaller()
|
||||
@@ -197,6 +230,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
"Media audio session cleanup incomplete"
|
||||
}
|
||||
check(outputs.isEmpty() && synchronized(inputLock) { shared?.closed != false }) { "Media audio still owned" }
|
||||
cleanupOutputListeners()
|
||||
invalidateAttempt()
|
||||
releaseHardware()
|
||||
mutable.value = mutable.value.copy(switching = false, inputName = null, outputName = null)
|
||||
@@ -215,6 +249,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
}
|
||||
invalidateAttempt()
|
||||
var failure: Exception? = null
|
||||
try { cleanupOutputListeners() } catch (e: Exception) { failure = e }
|
||||
try { releaseHardware() } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "media_audio", "session hardware cleanup failed; close remains retryable: $e")
|
||||
failure = e
|
||||
|
||||
@@ -96,8 +96,9 @@ class AudioTrack(val id: Int) : AudioRouting {
|
||||
access("add listener after")
|
||||
}
|
||||
fun removeOnRoutingChangedListener(listener: AudioRouting.OnRoutingChangedListener) {
|
||||
access("remove listener")
|
||||
check("remove listener" !in failures) { "track failed: remove listener" }
|
||||
listeners.remove(listener)
|
||||
check("remove listener after" !in failures) { "track failed: remove listener after" }
|
||||
}
|
||||
fun setPreferredDevice(device: AudioDeviceInfo?): Boolean {
|
||||
access("prefer"); selectionAttempts++
|
||||
|
||||
Reference in new issue
Block a user