Track Android radio speakers and actual transmission by group
This commit is contained in:
1 parent
d5174262d8
commit
bcda9b82f4
3 files changed
+64
-11
No files matched your search
@@ -90,6 +90,9 @@ class RadioAudioEngine {
|
||||
|
||||
private val _talkingFlow = MutableStateFlow(false)
|
||||
val talkingFlow: StateFlow<Boolean> = _talkingFlow
|
||||
/** Фактический TX (PTT/VAD), привязанный к каналу. Capture публикует, io сбрасывает после join. */
|
||||
private val _transmittingGroup = MutableStateFlow<Long?>(null)
|
||||
val transmittingGroup: StateFlow<Long?> = _transmittingGroup
|
||||
|
||||
/* Сериализованный поток для setup/teardown аудио (не блокирует main). */
|
||||
private val io = Executors.newSingleThreadExecutor { r ->
|
||||
@@ -510,6 +513,10 @@ class RadioAudioEngine {
|
||||
}
|
||||
vadMode = 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
|
||||
_vadActiveFlow.value = false
|
||||
}
|
||||
@@ -654,6 +661,11 @@ class RadioAudioEngine {
|
||||
}
|
||||
if (captureRunning) _talkingFlow.value = requestedTalk || NativeLib.radioTransmitting()
|
||||
} else skippedFrames++
|
||||
val transmitting = if (captureRunning && active && groupId == group && NativeLib.radioTransmitting()) group else null
|
||||
if (_transmittingGroup.value != transmitting) {
|
||||
_transmittingGroup.value = transmitting
|
||||
LogManager.addLog("DEBUG", "radio", "TX state group=$group on=${transmitting != null} manual=$requestedTalk vad=$vadMode")
|
||||
}
|
||||
val nowMs = SystemClock.elapsedRealtime()
|
||||
if (nowMs - statsMs >= 1000L) {
|
||||
LogManager.addLog("DEBUG", "debug", "VAD capture summary group=$group spanMs=${nowMs - statsMs} " +
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/** Эфемерные говорящие одной активной рации; снимки участников и локальный mute не меняют этот набор. */
|
||||
data class RadioTalkState(val groupId: Long = 0, val speakers: Set<Long> = emptySet()) {
|
||||
fun onTalk(event: RadioTalkEvent): RadioTalkState {
|
||||
if (groupId == 0L || event.groupId != groupId || speakers.contains(event.nodeId) == event.on) return this
|
||||
return copy(speakers = if (event.on) speakers + event.nodeId else speakers - event.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
data class RadioTalkEvent(val groupId: Long, val nodeId: Long, val stream: Int, val on: Boolean)
|
||||
|
||||
/** Строгий формат CHAT_EVT_RADIO_TALK: group(8), source(8), stream(2), on(1), little endian. */
|
||||
fun parseRadioTalkEvent(data: ByteArray?): RadioTalkEvent? {
|
||||
if (data == null || data.size != 19 || data[18].toInt() !in 0..1) return null
|
||||
val bytes = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val group = bytes.long
|
||||
val node = bytes.long
|
||||
if (group == 0L || node == 0L) return null
|
||||
return RadioTalkEvent(group, node, bytes.short.toInt() and 0xffff, data[18].toInt() == 1)
|
||||
}
|
||||
@@ -75,6 +75,8 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
private val _radioTalking = MutableStateFlow<Long?>(null)
|
||||
val radioTalking: StateFlow<Long?> = _radioTalking
|
||||
private val _radioTalkState = MutableStateFlow(RadioTalkState())
|
||||
val radioTalkState: StateFlow<RadioTalkState> = _radioTalkState
|
||||
|
||||
private val _radioRoute = MutableStateFlow(AudioRoute.SPEAKER)
|
||||
val radioRoute: StateFlow<AudioRoute> = _radioRoute
|
||||
@@ -402,6 +404,11 @@ class ChatViewModel : ViewModel() {
|
||||
4 -> { if (repo != null) { LogManager.addLog("INFO", "VM", "CHANNEL_UPDATED — refreshChannels"); refreshChannels() } else { LogManager.addLog("WARN", "VM", "CHANNEL_UPDATED skipped — repo is NULL") } }
|
||||
12 -> LogManager.addLog("INFO", "VM", "service started")
|
||||
13 -> {
|
||||
_radioTalkState.value = RadioTalkState()
|
||||
_radioTalking.value = null
|
||||
_radioActive.value = false
|
||||
radioChannelId = null
|
||||
radioAudio.stop()
|
||||
memberRadios.clear()
|
||||
memberRadioMutes.clear()
|
||||
_members.value = _members.value.map { it.copy(radioActive = false) }
|
||||
@@ -567,6 +574,10 @@ class ChatViewModel : ViewModel() {
|
||||
val chId = String(data, 1, cl)
|
||||
memberRadios[chId] = memberRadios[chId].orEmpty() - nodeId
|
||||
memberRadioMutes[chId] = memberRadioMutes[chId].orEmpty() - nodeId
|
||||
if (_radioTalkState.value.groupId == chId.toULongOrNull()?.toLong()) {
|
||||
_radioTalkState.value = _radioTalkState.value.copy(speakers = _radioTalkState.value.speakers - nodeId)
|
||||
_radioTalking.value = if (_radioActive.value) _radioTalkState.value.speakers.firstOrNull() else null
|
||||
}
|
||||
val cur = _currentChannel.value
|
||||
if (cur != null && cur.id == String(data, 1, cl))
|
||||
_members.value = _members.value.filter { it.nodeId != nodeId }
|
||||
@@ -736,17 +747,19 @@ class ChatViewModel : ViewModel() {
|
||||
preparationState(target, job, state)
|
||||
}
|
||||
43 -> { /* RADIO_TALK: [group_id:8][src:8][stream:2][on:1] */
|
||||
if (data == null || data.size < 19) return
|
||||
val bb = java.nio.ByteBuffer.wrap(data, 0, 19).order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
val groupId = bb.getLong()
|
||||
val src = bb.getLong()
|
||||
bb.getShort() /* stream */
|
||||
val on = bb.get()
|
||||
val cur = _currentChannel.value
|
||||
if (cur != null && cur.id.toULongOrNull()?.toLong() == groupId) {
|
||||
_radioTalking.value = if (on.toInt() != 0) src else null
|
||||
LogManager.addLog("INFO", "VM", "RADIO_TALK ${if (on.toInt() != 0) "start" else "stop"} src=0x%016x".format(src))
|
||||
val event = parseRadioTalkEvent(data)
|
||||
if (event == null) { LogManager.addLog("ERROR", "radio", "invalid RADIO_TALK bytes=${data?.size ?: 0}"); return }
|
||||
val previous = _radioTalkState.value
|
||||
val next = previous.onTalk(event)
|
||||
if (event.groupId != previous.groupId) {
|
||||
LogManager.addLog("DEBUG", "radio", "ignored talk group=${event.groupId} active=${previous.groupId} node=${event.nodeId}")
|
||||
return
|
||||
}
|
||||
if (next == previous) return
|
||||
_radioTalkState.value = next
|
||||
_radioTalking.value = if (_radioActive.value) next.speakers.firstOrNull() else null
|
||||
LogManager.addLog("DEBUG", "radio", "talk group=${event.groupId} node=${event.nodeId} stream=${event.stream} " +
|
||||
"on=${event.on} speakers=${next.speakers.size}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -776,6 +789,8 @@ class ChatViewModel : ViewModel() {
|
||||
radioChannelId?.takeIf { it != ch.id }?.toULongOrNull()?.toLong()
|
||||
?.let { NativeLib.radioSetActive(it, false) } /* выключаем старый канал */
|
||||
radioChannelId = ch.id
|
||||
_radioTalkState.value = RadioTalkState(groupId)
|
||||
_radioTalking.value = null
|
||||
NativeLib.radioSetActive(groupId, true)
|
||||
radioAudio.start(groupId)
|
||||
radioAudio.router.onChanged = { r, headset ->
|
||||
@@ -797,6 +812,7 @@ class ChatViewModel : ViewModel() {
|
||||
NativeLib.radioSetActive(groupId, false)
|
||||
radioAudio.stop()
|
||||
_radioTalking.value = null
|
||||
_radioTalkState.value = RadioTalkState()
|
||||
_radioRoute.value = AudioRoute.SPEAKER
|
||||
_radioHeadsetAvailable.value = false
|
||||
_wiredInputAvailable.value = false
|
||||
@@ -908,6 +924,7 @@ class ChatViewModel : ViewModel() {
|
||||
radioChannelId = null
|
||||
_radioActive.value = false
|
||||
_radioTalking.value = null
|
||||
_radioTalkState.value = RadioTalkState()
|
||||
_radioRoute.value = AudioRoute.SPEAKER
|
||||
_radioHeadsetAvailable.value = false
|
||||
_wiredInputAvailable.value = false
|
||||
@@ -1031,7 +1048,7 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
_currentChannel.value = channel
|
||||
_radioActive.value = (radioChannelId == channel.id)
|
||||
if (radioChannelId != channel.id) _radioTalking.value = null
|
||||
_radioTalking.value = if (_radioActive.value) _radioTalkState.value.speakers.firstOrNull() else null
|
||||
_soundOn.value = NativeLib.getSoundOnMessage(channel.id)
|
||||
NotificationSound.setMuted(channel.id, !_soundOn.value)
|
||||
r.connectChannel(channel.id)
|
||||
|
||||
Reference in new issue
Block a user