Keep media audio session close retryable until router cleanup succeeds
This commit is contained in:
1 parent
1970f895da
commit
3d2e98b02e
2 files changed
+26
-9
No files matched your search
@@ -35,7 +35,8 @@ internal object MediaAudioController : MediaAudioSession()
|
||||
* refresh/select асинхронны. attachOutput/close lease/awaitIdle/close блокируют вызывающего worker,
|
||||
* поэтому main и io для них запрещены. Track регистрируется до play и отсоединяется до release.
|
||||
* Ошибки инвалидируют поколение; следующий refresh повторяет попытку, сохраняя действующие PCM-подписки.
|
||||
* Настройка становится committed только после успешной записи. close требует отсутствия потребителей.
|
||||
* Настройка становится committed только после успешной записи. close требует отсутствия потребителей,
|
||||
* запрещает новых и закрывает executor лишь после подтверждения hardware/router cleanup; отказ допускает повтор close.
|
||||
*/
|
||||
internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor { Thread(it, "media-audio-control").apply { isDaemon = true } }) : AutoCloseable {
|
||||
@@ -44,6 +45,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
@Volatile private var controlThread: Thread? = null
|
||||
@Volatile private var closed = false
|
||||
@Volatile private var closing = false
|
||||
private val router = CallAudioRouter()
|
||||
private val mutable = MutableStateFlow(MediaAudioState())
|
||||
val state: StateFlow<MediaAudioState> = mutable
|
||||
@@ -110,7 +112,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
|
||||
/** Не открывает микрофон из UI. Вызывается реальным владельцем захвата с RECORD_AUDIO. */
|
||||
fun subscribe(name: String): MicrophoneCapture.Subscription = synchronized(inputLock) {
|
||||
check(!closed) { "Media audio session closed" }
|
||||
check(!closed && !closing) { "Media audio session closing/closed" }
|
||||
check(!CallController.blocksMedia()) { "Media capture blocked by call" }
|
||||
if (shared?.closed != false) {
|
||||
val device = InputBridge()
|
||||
@@ -124,9 +126,9 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
/** Регистрация завершается до play, удаление — до release. Не вызывается из main или io. */
|
||||
fun attachOutput(track: AudioTrack): AutoCloseable {
|
||||
checkBlockingCaller()
|
||||
check(!closed) { "Media audio session closed" }
|
||||
check(!closed && !closing) { "Media audio session closing/closed" }
|
||||
val registration = io.submit<OutputRegistration> {
|
||||
check(!closed) { "Media audio session closed" }
|
||||
check(!closed && !closing) { "Media audio session closing/closed" }
|
||||
check(!CallController.blocksMedia()) { "Media output blocked by call" }
|
||||
check(track !in outputs) { "AudioTrack already registered" }
|
||||
val owner = OutputRegistration()
|
||||
@@ -181,6 +183,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
fun awaitIdle() {
|
||||
checkBlockingCaller()
|
||||
io.submit {
|
||||
check(!closing) { "Media audio session cleanup incomplete" }
|
||||
check(outputs.isEmpty() && synchronized(inputLock) { shared?.closed != false }) { "Media audio still owned" }
|
||||
invalidateAttempt()
|
||||
releaseHardware()
|
||||
@@ -196,10 +199,20 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
io.submit {
|
||||
synchronized(inputLock) {
|
||||
check(outputs.isEmpty() && shared?.closed != false) { "Media audio still owned" }
|
||||
closed = true
|
||||
closing = true
|
||||
}
|
||||
invalidateAttempt()
|
||||
try { releaseHardware() } finally { router.stop() }
|
||||
var failure: Exception? = null
|
||||
try { releaseHardware() } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "media_audio", "session hardware cleanup failed; close remains retryable: $e")
|
||||
failure = e
|
||||
}
|
||||
try { router.stop() } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "media_audio", "session router cleanup failed; close remains retryable: $e")
|
||||
if (failure == null) failure = e
|
||||
}
|
||||
failure?.let { throw it }
|
||||
closed = true
|
||||
LogManager.addLog("INFO", "media_audio", "session closed")
|
||||
}.get()
|
||||
} finally { if (closed) io.shutdown() }
|
||||
@@ -211,9 +224,9 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
}
|
||||
|
||||
private fun enqueue(action: () -> Unit) {
|
||||
if (closed) return
|
||||
if (closed || closing) return
|
||||
try {
|
||||
io.execute { if (!closed) try { action() } catch (e: Exception) { recover(e) } }
|
||||
io.execute { if (!closed && !closing) try { action() } catch (e: Exception) { recover(e) } }
|
||||
} catch (e: RejectedExecutionException) {
|
||||
if (!closed) LogManager.addLog("ERROR", "media_audio", "control task rejected: $e")
|
||||
}
|
||||
@@ -359,7 +372,7 @@ internal open class MediaAudioSession(private val io: ScheduledExecutorService =
|
||||
val epoch = generation
|
||||
lateinit var task: ScheduledFuture<*>
|
||||
task = io.schedule({
|
||||
if (closed || generation != epoch || confirmationTimeout !== task) return@schedule
|
||||
if (closed || closing || generation != epoch || confirmationTimeout !== task) return@schedule
|
||||
confirmationTimeout = null
|
||||
try {
|
||||
check(inputReady && routeAvailable && outputsReady(outputs.keys)) {
|
||||
|
||||
@@ -30,6 +30,8 @@ class BluetoothAdapter {
|
||||
var closeAttempts = 0
|
||||
var rejectRequest = false
|
||||
var throwRequest = false
|
||||
var closeFailures = emptySet<BluetoothProfile>()
|
||||
var closeFailuresAfter = emptySet<BluetoothProfile>()
|
||||
var requestEntered: CountDownLatch? = null
|
||||
var requestReturn: CountDownLatch? = null
|
||||
|
||||
@@ -61,9 +63,11 @@ class BluetoothAdapter {
|
||||
check(profile == BluetoothProfile.HEADSET)
|
||||
if (proxy == null) return
|
||||
++closeAttempts
|
||||
check(proxy !in closeFailures) { "BT close failed before effect proxy=$proxy" }
|
||||
if (!live.remove(proxy)) return // SDK close идемпотентен; запись удаляется только один раз.
|
||||
++closes
|
||||
(proxy as BluetoothHeadset).connected = false
|
||||
LogManager.addLog("INFO", "HostBluetooth", "closed proxy=${proxy.id} live=${live.size}")
|
||||
check(proxy !in closeFailuresAfter) { "BT close failed after effect proxy=$proxy" }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user