Retain failed media routing listener removals for acknowledged retry

This commit is contained in:
evgeny committed 2026-10-29 13:34:24 +01:00
1 parent ee20c8b9b7
commit 2516545f9d
2 files changed
+49 -13

No files matched your search

@@ -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++