Keep radio capture owned through startup faults and deferred native cleanup

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent 1ddd75e134
commit a04cfe4441
1 file changed
+86 -52
@@ -62,6 +62,7 @@ class RadioAudioEngine {
private var playThread: Thread? = null
private var outputLease: AutoCloseable? = null
private var capturePttRequested = false
private var nativeCaptureOwned = false // Только io: capture start мог иметь эффект; сбрасывается после stop API.
@Volatile private var active = false
@Volatile private var groupId = 0L
@@ -312,9 +313,10 @@ class RadioAudioEngine {
outputLease = MediaAudioController.attachOutput(track)
track.play()
playbackRunning = true
playThread = Thread { playLoop() }.apply {
priority = Thread.MAX_PRIORITY; start()
}
val worker = Thread({ playLoop(track, group, version) }, "radio-playback")
playThread = worker
worker.priority = Thread.MAX_PRIORITY
worker.start()
registerScreenReceiver(version)
updateCapture()
@@ -412,8 +414,10 @@ class RadioAudioEngine {
/** Оба детектора используют одно окно по экрану/фону; измеритель не продлевает это окно. */
private fun updateCapture() {
try {
mainHandler.removeCallbacks(vadTimeout)
if (!settingsReady) return
if (!active || groupId != audioGroup) { stopCapture(); return }
val now = SystemClock.elapsedRealtime()
vadWindow.setEnabled(autoSettings.enabled, now)
var mode = radioCaptureMode(active, screenInteractive, vadWindow.isAllowed(now), autoSettings, requestedTalk, levelMonitoring)
@@ -431,6 +435,7 @@ class RadioAudioEngine {
val detectorFailed = mode != RadioAutoSettings.OFF && autoMode == RadioAutoSettings.OFF
if (captureSubscription != null && (captureRequestedMode != mode || (detectorFailed && !requestedTalk && !levelMonitoring))) stopCapture()
if (audioTrack == null) return
if (captureSubscription == null && nativeCaptureOwned) stopCapture()
if (captureSubscription == null && !startCapture(mode)) {
requestedTalk = false
_talkingFlow.value = false
@@ -439,10 +444,17 @@ class RadioAudioEngine {
if (requestedTalk != capturePttRequested) {
if (captureSubscription?.ptt(requestedTalk) != true) {
LogManager.addLog("ERROR", "radio", "PTT command rejected; stopping capture group=$audioGroup")
requestedTalk = false
stopCapture(); return
}
capturePttRequested = requestedTalk
}
} catch (e: Exception) {
requestedTalk = false
_talkingFlow.value = nativeCaptureOwned && _transmittingGroup.value != null
LogManager.addLog("ERROR", "RadioAudio", "capture update failed group=$audioGroup generation=$generation " +
"nativeOwned=$nativeCaptureOwned; retained for retry: $e")
}
}
private fun startCapture(mode: Int): Boolean {
@@ -450,53 +462,64 @@ class RadioAudioEngine {
LogManager.addLog("WARN", "radio", "Auto/PTT capture blocked: RECORD_AUDIO not granted group=$audioGroup mode=$mode")
return false
}
val subscription = try {
MediaAudioController.subscribe("radio")
} catch (e: Exception) {
LogManager.addLog("ERROR", "radio", "shared capture subscription failed: ${e.message}")
return false
}
val channels = subscription.channels
if (!NativeLib.radioCaptureStart(audioGroup, channels, mode)) {
LogManager.addLog("ERROR", "RadioAudio", "native capture start failed group=$audioGroup ch=$channels mode=$mode")
subscription.close()
return false
}
autoMode = NativeLib.radioAutoMode()
captureRequestedMode = mode
if (mode != RadioAutoSettings.OFF && autoMode == RadioAutoSettings.OFF && !requestedTalk && !levelMonitoring) {
LogManager.addLog("ERROR", "radio", "Automatic transmission unavailable: capture cancelled group=$audioGroup")
NativeLib.radioCaptureStop(); subscription.close()
return false
}
captureSubscription = subscription
captureRunning = true
_autoActiveFlow.value = autoMode != RadioAutoSettings.OFF
// BEGIN должен попасть в uasync раньше первого кадра ручной передачи.
if (requestedTalk) {
NativeLib.radioTalkBegin(audioGroup)
talking = true
_talkingFlow.value = true
}
capturePttRequested = requestedTalk
val captureGroup = audioGroup
captureThread = Thread {
try { captureLoop(subscription, captureGroup, channels) }
catch (e: Exception) { LogManager.addLog("ERROR", "RadioAudio", "capture failed: ${e.message}") }
finally {
if (captureRunning) io.execute {
if (captureSubscription === subscription) {
val version = generation
var operation = "subscribe"
try {
val subscription = MediaAudioController.subscribe("radio")
captureSubscription = subscription // Сохраняется до любых native-вызовов с возможным частичным эффектом.
val channels = subscription.channels
check(channels in 1..2) { "invalid capture channels=$channels" }
operation = "native start"
nativeCaptureOwned = true
check(NativeLib.radioCaptureStart(captureGroup, channels, mode)) { "native capture start rejected" }
operation = "mode"
autoMode = NativeLib.radioAutoMode()
check(autoMode == mode || autoMode == RadioAutoSettings.OFF) { "native capture mode mismatch requested=$mode actual=$autoMode" }
captureRequestedMode = mode
if (mode != RadioAutoSettings.OFF && autoMode == RadioAutoSettings.OFF) {
check(requestedTalk || levelMonitoring) { "Automatic transmission unavailable: capture cancelled" }
LogManager.addLog("WARN", "radio", "Automatic transmission unavailable: keeping manual/monitor capture group=$captureGroup")
}
captureRunning = true
_autoActiveFlow.value = autoMode != RadioAutoSettings.OFF
// BEGIN должен попасть в uasync раньше первого кадра ручной передачи.
operation = "begin"
if (requestedTalk) {
NativeLib.radioTalkBegin(captureGroup)
talking = true
_talkingFlow.value = true
}
capturePttRequested = requestedTalk
operation = "worker start"
val worker = Thread({
try { captureLoop(subscription, captureGroup, channels) } finally {
if (captureRunning && active && generation == version && groupId == captureGroup) io.execute {
if (captureSubscription !== subscription || generation != version) return@execute
requestedTalk = false
stopCapture()
LogManager.addLog("WARN", "RadioAudio", "capture lost: resources released")
try {
stopCapture()
LogManager.addLog("WARN", "RadioAudio", "capture lost: resources released group=$captureGroup generation=$version")
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "capture loss cleanup failed group=$captureGroup; retained for retry: $e")
}
}
}
}, "radio-capture")
captureThread = worker
worker.priority = Thread.MAX_PRIORITY
worker.start()
LogManager.addLog("INFO", "radio", "capture started group=$captureGroup ch=$channels mode=$autoMode shared=true")
return true
} catch (e: Exception) {
requestedTalk = false
LogManager.addLog("ERROR", "RadioAudio", "capture startup failed operation=$operation group=$captureGroup " +
"generation=$version mode=$mode: $e")
try { stopCapture() } catch (cleanup: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "capture startup rollback failed group=$captureGroup; retained for retry: $cleanup")
}
}.apply {
name = "radio-capture"; priority = Thread.MAX_PRIORITY; start()
return false
}
LogManager.addLog("INFO", "radio", "capture started group=$audioGroup ch=$channels mode=$autoMode shared=true")
return true
}
private fun stopCapture() {
@@ -514,28 +537,39 @@ class RadioAudioEngine {
}
interrupted = joinWorker(captureThread, "capture")
captureThread = null
if (subscription != null) try {
if (nativeCaptureOwned) try {
NativeLib.radioCaptureStop()
nativeCaptureOwned = false
LogManager.addLog("INFO", "radio", "Auto/PTT capture released group=$audioGroup")
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "native capture stop failed group=$audioGroup: $e")
if (failure == null) failure = e
}
autoMode = RadioAutoSettings.OFF
capturePttRequested = false
talking = false
if (_transmittingGroup.value != null) {
LogManager.addLog("DEBUG", "radio", "TX cleared after capture stop group=${_transmittingGroup.value}")
_transmittingGroup.value = null
}
_talkingFlow.value = requestedTalk
_autoActiveFlow.value = false
if (!nativeCaptureOwned) clearCaptureState()
else _talkingFlow.value = requestedTalk || _transmittingGroup.value != null
failure?.let { throw it }
} finally {
if (interrupted) Thread.currentThread().interrupt()
}
}
/** Только после подтверждённого native capture/audio stop: снять наблюдаемый TX и режим детектора. */
private fun clearCaptureState() {
nativeCaptureOwned = false
autoMode = RadioAutoSettings.OFF
capturePttRequested = false
talking = false
if (_transmittingGroup.value != null) {
LogManager.addLog("DEBUG", "radio", "TX cleared after capture stop group=${_transmittingGroup.value}")
_transmittingGroup.value = null
}
_talkingFlow.value = requestedTalk
_autoActiveFlow.value = false
}
/** Дождаться своего worker до остановки DSP; interruption не разрешает освободить используемые им ресурсы. */
private fun joinWorker(worker: Thread?, kind: String): Boolean {
var interrupted = Thread.interrupted()
@@ -597,7 +631,7 @@ class RadioAudioEngine {
"track=${System.identityHashCode(track)}; retained for retry: $e")
if (failure == null) failure = e
}
try { NativeLib.radioAudioStop() } catch (e: Exception) {
try { NativeLib.radioAudioStop(); clearCaptureState() } catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "native audio stop failed group=$audioGroup: $e")
if (failure == null) failure = e
}