Keep radio capture owned through startup faults and deferred native cleanup
This commit is contained in:
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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user