Retain call audio owners until worker and device cleanup succeeds
This commit is contained in:
1 parent
a1c77268b8
commit
e857b9de75
2 files changed
+69
-40
No files matched your search
@@ -34,6 +34,8 @@ import com.utun.chat.ChatApplication
|
||||
* оценка по playback head допустима лишь до первого принятого timestamp за этот запуск.
|
||||
* При смене выхода сохраняются native JB и общий счётчик PCM, его старый аппаратный якорь снимается.
|
||||
* Stop ждёт capture/playback и вложенный reporter до освобождения устройств и native-стека.
|
||||
* При незавершённой очистке сохраняет владельцев и бросает ошибку; CallService повторяет stop,
|
||||
* а новый start сначала обязан завершить прежний stop.
|
||||
*/
|
||||
class CallAudioEngine {
|
||||
|
||||
@@ -56,6 +58,7 @@ class CallAudioEngine {
|
||||
private var playThread: Thread? = null
|
||||
private var prevMode: Int = AudioManager.MODE_NORMAL
|
||||
private var prevSpeaker: Boolean = false
|
||||
private var modeOwned = false // Main: прежний AudioManager mode/speaker ещё надо восстановить.
|
||||
|
||||
/** Маршрутизация аудио-выхода (динамик/громкая/гарнитура). */
|
||||
val router = CallAudioRouter()
|
||||
@@ -168,15 +171,16 @@ class CallAudioEngine {
|
||||
callId = id
|
||||
routeGeneration = 0; outputSample = null
|
||||
outputRecovery = CallOutputRecovery()
|
||||
router.bindTelecom(TelecomCallManager.connectionFor(id))
|
||||
muted = false
|
||||
failed.set(false)
|
||||
captureReady = false; playReady = false; duplexReady = false
|
||||
try {
|
||||
router.bindTelecom(TelecomCallManager.connectionFor(id))
|
||||
val ctx = ChatApplication.instance
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
prevMode = am.mode
|
||||
prevSpeaker = am.isSpeakerphoneOn
|
||||
modeOwned = router.telecom == null
|
||||
var stageAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
if (router.telecom == null) am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
@@ -256,7 +260,6 @@ class CallAudioEngine {
|
||||
val createdAt = SystemClock.elapsedRealtime()
|
||||
wiredActive = wantedWired
|
||||
val rec = createAudioRecord(wiredActive)
|
||||
audioRecord = rec
|
||||
if (!active) return@runWorker
|
||||
if (rec == null) {
|
||||
reportFailure(id, "AudioRecord create failed wired=$wiredActive")
|
||||
@@ -309,54 +312,90 @@ class CallAudioEngine {
|
||||
mainHandler.post { if (callId == id && active) onFailure?.invoke(id, detail) }
|
||||
}
|
||||
|
||||
/** Main: снять active, разблокировать AudioRecord.read через stop и дождаться workers/reporter.
|
||||
* AudioTrack пишется без блокировки; AAudio.write ограничен native timeout. Release выполняется после join. */
|
||||
/** Main: отменить работу, разблокировать read и подтвердить join/release каждой независимой части.
|
||||
* Join ограничен 250 мс на worker; живые потоки и устройства остаются до следующей попытки.
|
||||
* При неполной очистке бросает ошибку владельцу; native останавливается лишь после устройств. */
|
||||
fun stop() {
|
||||
if (callId == 0L) return
|
||||
val id = callId
|
||||
active = false
|
||||
router.onOutputDevice = null
|
||||
router.onInputDevice = null
|
||||
router.onRouteRequested = null
|
||||
mainHandler.removeCallbacks(aecDelayRunnable)
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
communicationListener?.let { listener ->
|
||||
val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||
try {
|
||||
val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||
communicationListener = null
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "communication listener removal failed id=$id: $e")
|
||||
}
|
||||
}
|
||||
communicationListener = null
|
||||
}
|
||||
synchronized(recordLock) {
|
||||
audioRecord?.let { rec ->
|
||||
try { if (rec.recordingState == AudioRecord.RECORDSTATE_RECORDING) rec.stop() }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "record stop failed id=$id: ${e.message}") }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "CallAudio", "record stop failed id=$id: $e") }
|
||||
}
|
||||
}
|
||||
synchronized(trackLock) {
|
||||
audioTrack?.let { track ->
|
||||
try { track.removeOnRoutingChangedListener(trackRouteListener) }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track listener removal failed id=$id: ${e.message}") }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track listener removal failed id=$id: $e") }
|
||||
try { if (track.playState == AudioTrack.PLAYSTATE_PLAYING) track.pause() }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track pause failed id=$id: ${e.message}") }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track pause failed id=$id: $e") }
|
||||
}
|
||||
}
|
||||
playThread?.interrupt()
|
||||
captureThread?.join(); captureThread = null
|
||||
playThread?.join(); playThread = null
|
||||
synchronized(recordLock) {
|
||||
try { audioRecord?.release() }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "record release failed id=$id: ${e.message}") }
|
||||
audioRecord = null
|
||||
var interrupted = false
|
||||
for (worker in listOfNotNull(captureThread, playThread)) {
|
||||
try { worker.join(250) }
|
||||
catch (e: InterruptedException) {
|
||||
interrupted = true
|
||||
LogManager.addLog("WARN", "CallAudio", "worker join interrupted id=$id worker=${worker.name}: $e")
|
||||
}
|
||||
if (worker.isAlive) LogManager.addLog("ERROR", "CallAudio", "worker join pending id=$id worker=${worker.name} state=${worker.state}")
|
||||
}
|
||||
if (captureThread?.isAlive != true) captureThread = null
|
||||
if (playThread?.isAlive != true) playThread = null
|
||||
if (captureThread == null && playThread == null) {
|
||||
synchronized(recordLock) {
|
||||
try { audioRecord?.release(); audioRecord = null }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "CallAudio", "record release failed id=$id: $e") }
|
||||
}
|
||||
synchronized(trackLock) {
|
||||
try { audioTrack?.release(); audioTrack = null }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "CallAudio", "track release failed id=$id: $e") }
|
||||
}
|
||||
}
|
||||
try { audioTrack?.release() }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track release failed id=$id: ${e.message}") }
|
||||
audioTrack = null
|
||||
captureReady = false; playReady = false; duplexReady = false
|
||||
NativeLib.callAudioStop()
|
||||
router.onOutputDevice = null
|
||||
router.onInputDevice = null
|
||||
router.onRouteRequested = null
|
||||
playbackBuffer = null
|
||||
val restoreMode = router.telecom == null
|
||||
router.stop()
|
||||
if (restoreMode) restoreAudioMode()
|
||||
if (modeOwned) {
|
||||
try {
|
||||
val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.isSpeakerphoneOn = prevSpeaker
|
||||
am.mode = prevMode
|
||||
modeOwned = false
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "restore audio mode failed id=$id: $e")
|
||||
}
|
||||
}
|
||||
if (interrupted) Thread.currentThread().interrupt()
|
||||
check(captureThread == null && playThread == null && audioRecord == null && audioTrack == null &&
|
||||
communicationListener == null && !modeOwned) {
|
||||
val detail = "cleanup pending id=$id workers=${captureThread?.name}/${playThread?.name} " +
|
||||
"record=${audioRecord != null} track=${audioTrack != null} listener=${communicationListener != null} mode=$modeOwned"
|
||||
LogManager.addLog("ERROR", "CallAudio", detail)
|
||||
detail
|
||||
}
|
||||
try { NativeLib.callAudioStop() }
|
||||
catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "native stop failed id=$id: $e")
|
||||
throw e
|
||||
}
|
||||
callId = 0L
|
||||
muted = false
|
||||
LogManager.addLog("INFO", "CallAudio", "stopped id=$id capture=$capFramesRead play=$playFramesWritten; workers joined")
|
||||
@@ -385,7 +424,6 @@ class CallAudioEngine {
|
||||
audioRecord = null
|
||||
wiredActive = wantedWired
|
||||
rec = createAudioRecord(wiredActive)
|
||||
audioRecord = rec
|
||||
if (rec == null) return
|
||||
rec?.startRecording()
|
||||
}
|
||||
@@ -483,9 +521,9 @@ class CallAudioEngine {
|
||||
return null
|
||||
}
|
||||
val constructorMs = SystemClock.elapsedRealtime() - constructorAt
|
||||
audioRecord = rec // Под recordLock: владение до любых запросов состояния/регистрации listeners.
|
||||
if (rec.state != AudioRecord.STATE_INITIALIZED) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "AudioRecord not initialized (wired=$wired)")
|
||||
rec.release()
|
||||
return null
|
||||
}
|
||||
val routeAt = SystemClock.elapsedRealtime()
|
||||
@@ -514,6 +552,7 @@ class CallAudioEngine {
|
||||
val begin = SystemClock.elapsedRealtime()
|
||||
val track = CallAudioOutput.open(am, outputRecovery.deviceId, outputRecovery.tryFast)
|
||||
audioTrack = track
|
||||
check(track.track == null || track.track.state == AudioTrack.STATE_INITIALIZED) { "AudioTrack not initialized" }
|
||||
applyTrackOutputDevice(router.currentOutputDevice)
|
||||
configurePlaybackBuffer(track, reason)
|
||||
track.addOnRoutingChangedListener(trackRouteListener, mainHandler)
|
||||
@@ -841,16 +880,6 @@ class CallAudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreAudioMode() {
|
||||
try {
|
||||
val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.isSpeakerphoneOn = prevSpeaker
|
||||
am.mode = prevMode
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "restore audio mode failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyTrackOutputDevice(dev: AudioDeviceInfo?) {
|
||||
synchronized(trackLock) {
|
||||
val track = audioTrack ?: return
|
||||
|
||||
@@ -79,12 +79,13 @@ internal class CallAudioOutput private constructor(
|
||||
fun removeOnRoutingChangedListener(listener: AudioRouting.OnRoutingChangedListener) {
|
||||
track?.removeOnRoutingChangedListener(listener)
|
||||
}
|
||||
/** Идемпотентно закрыть выход; после этого write/play/настройка буфера недопустимы. */
|
||||
/** Идемпотентно закрыть выход; ошибка release сохраняет владельца для повторной попытки.
|
||||
* После подтверждённого освобождения write/play/настройка буфера недопустимы. */
|
||||
@Synchronized fun release() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
if (handle != 0L) { CallOutputNative.close(handle); handle = 0 }
|
||||
track?.release()
|
||||
closed = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -109,7 +110,6 @@ internal class CallAudioOutput private constructor(
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build())
|
||||
.setBufferSizeInBytes((min * 2).coerceAtLeast(3840)).setTransferMode(AudioTrack.MODE_STREAM)
|
||||
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY).build()
|
||||
if (track.state != AudioTrack.STATE_INITIALIZED) { track.release(); error("AudioTrack not initialized") }
|
||||
LogManager.addLog("INFO", "CallAudio", "output fallback=AudioTrack device=$deviceId type=$type tryFast=$tryFast fastRoute=$fastRoute")
|
||||
return CallAudioOutput(track, 0, manager)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user