Fix flush on PTT release; monitoring priority tweaks (v1.0.5)
This commit is contained in:
@@ -65,6 +65,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
|
|
||||||
private var connected = false
|
private var connected = false
|
||||||
private var monitoring = false
|
private var monitoring = false
|
||||||
|
private var priorityActive = false
|
||||||
private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11)
|
private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11)
|
||||||
|
|
||||||
private var svc: SvxService? = null
|
private var svc: SvxService? = null
|
||||||
@@ -159,6 +160,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPttUp() {
|
override fun onPttUp() {
|
||||||
|
Log.d("SVXRemote", "PTT up")
|
||||||
svc?.stopTx()
|
svc?.stopTx()
|
||||||
lblPttHint.text = getString(R.string.hold_to_talk)
|
lblPttHint.text = getString(R.string.hold_to_talk)
|
||||||
lblTxState.text =
|
lblTxState.text =
|
||||||
@@ -291,17 +293,32 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
|
|
||||||
override fun onTalkerStart(tg: Long, callsign: String) {
|
override fun onTalkerStart(tg: Long, callsign: String) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
lblTalkerTitle.text = if (monitoring) {
|
val priority = monitoring && settings.groups.firstOrNull() == tg
|
||||||
getString(R.string.talker_mon, callsign, tg)
|
if (priority) {
|
||||||
} else {
|
priorityActive = true
|
||||||
getString(R.string.talker_now, callsign)
|
}
|
||||||
|
if (priority || !priorityActive) {
|
||||||
|
svc?.selectGroup(tg)
|
||||||
|
lblTalkerTitle.text = when {
|
||||||
|
priority -> getString(R.string.talker_mon_priority, callsign, tg)
|
||||||
|
monitoring -> getString(R.string.talker_mon, callsign, tg)
|
||||||
|
else -> getString(R.string.talker_now, callsign)
|
||||||
|
}
|
||||||
|
lblTalkerTitle.setTextColor(
|
||||||
|
if (priority) getColor(R.color.danger) else getColor(R.color.text_secondary)
|
||||||
|
)
|
||||||
|
lblTalker.text = callsign
|
||||||
}
|
}
|
||||||
lblTalker.text = callsign
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTalkerStop(tg: Long, callsign: String) {
|
override fun onTalkerStop(tg: Long, callsign: String) {
|
||||||
// последний говоривший остаётся на экране
|
// последний говоривший остаётся на экране
|
||||||
|
mainHandler.post {
|
||||||
|
if (monitoring && settings.groups.firstOrNull() == tg) {
|
||||||
|
priorityActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeJoined(callsign: String, tg: Long?) {
|
override fun onNodeJoined(callsign: String, tg: Long?) {
|
||||||
@@ -416,6 +433,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
/** Включить/выключить режим мониторинга групп с переподключением. */
|
/** Включить/выключить режим мониторинга групп с переподключением. */
|
||||||
private fun toggleMonitoring() {
|
private fun toggleMonitoring() {
|
||||||
monitoring = !monitoring
|
monitoring = !monitoring
|
||||||
|
priorityActive = false
|
||||||
updateMonitorButton()
|
updateMonitorButton()
|
||||||
updatePttEnabled()
|
updatePttEnabled()
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
import ru.ua1zbe.svxremote.audio.OpusEngine
|
import ru.ua1zbe.svxremote.audio.OpusEngine
|
||||||
import ru.ua1zbe.svxremote.proto.MsgParser
|
import ru.ua1zbe.svxremote.proto.MsgParser
|
||||||
import ru.ua1zbe.svxremote.proto.ReflectorClient
|
import ru.ua1zbe.svxremote.proto.ReflectorClient
|
||||||
@@ -87,13 +88,22 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
|||||||
|
|
||||||
fun isRunning(): Boolean = client.isRunning()
|
fun isRunning(): Boolean = client.isRunning()
|
||||||
|
|
||||||
|
fun selectGroup(tg: Long) {
|
||||||
|
client.selectGroup(tg)
|
||||||
|
}
|
||||||
|
|
||||||
fun startTx() {
|
fun startTx() {
|
||||||
engine.startCapture { packet -> client.sendAudio(packet) }
|
engine.startCapture { packet -> client.sendAudio(packet) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopTx() {
|
fun stopTx() {
|
||||||
engine.stopCapture()
|
Log.d("SVXRemote", "stopTx")
|
||||||
client.sendAudioFlush()
|
// В фоне: сеть запрещена в главном потоке (NetworkOnMainThreadException),
|
||||||
|
// а join потока захвата блокировал бы UI
|
||||||
|
Thread {
|
||||||
|
engine.stopCapture()
|
||||||
|
client.sendAudioFlush()
|
||||||
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Callbacks (фоновые потоки) ----------
|
// ---------- Callbacks (фоновые потоки) ----------
|
||||||
|
|||||||
@@ -308,14 +308,43 @@ class OpusEngine(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Дренаж: отдаём пакеты, оставшиеся в кодере после остановки записи
|
||||||
|
try {
|
||||||
|
var outIdx = enc.dequeueOutputBuffer(encodeBufferInfo, 0)
|
||||||
|
while (outIdx >= 0) {
|
||||||
|
if (encodeBufferInfo.size > 0) {
|
||||||
|
val outBuf = ByteBuffer.allocate(encodeBufferInfo.size)
|
||||||
|
enc.getOutputBuffer(outIdx)?.also { src ->
|
||||||
|
src.position(encodeBufferInfo.offset)
|
||||||
|
src.limit(encodeBufferInfo.offset + encodeBufferInfo.size)
|
||||||
|
outBuf.put(src)
|
||||||
|
}
|
||||||
|
onPacket(outBuf.array())
|
||||||
|
}
|
||||||
|
enc.releaseOutputBuffer(outIdx, false)
|
||||||
|
outIdx = enc.dequeueOutputBuffer(encodeBufferInfo, 0)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
|
if (!encoding && recThread == null) return
|
||||||
encoding = false
|
encoding = false
|
||||||
try {
|
try {
|
||||||
recorder?.stop()
|
recorder?.stop()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
|
// Ждём завершения потока захвата: он должен отправить последние пакеты,
|
||||||
|
// чтобы flush (окончание передачи) ушёл после всего аудио
|
||||||
|
val t = recThread
|
||||||
|
recThread = null
|
||||||
|
if (t != null && t.isAlive) {
|
||||||
|
try {
|
||||||
|
t.join(1000)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
recorder?.release()
|
recorder?.release()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -330,8 +359,6 @@ class OpusEngine(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
encoder = null
|
encoder = null
|
||||||
recThread?.interrupt()
|
|
||||||
recThread = null
|
|
||||||
onLevel(SRC_MIC, 0f)
|
onLevel(SRC_MIC, 0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,12 +153,30 @@ class ReflectorClient(
|
|||||||
udpSend(sock, s, data)
|
udpSend(sock, s, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Переключить принимаемую группу на живом соединении. */
|
||||||
|
fun selectGroup(tg: Long) {
|
||||||
|
val sock = tcpSocket
|
||||||
|
if (sock == null || !running.get()) {
|
||||||
|
Log.d(TAG, "selectGroup tg=$tg skipped (sock=${sock != null} running=${running.get()})")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "selectGroup tg=$tg")
|
||||||
|
sendTcp(sock, MsgBuilder.selectTg(tg))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Конец передачи. */
|
/** Конец передачи. */
|
||||||
fun sendAudioFlush() {
|
fun sendAudioFlush() {
|
||||||
val sock = udpSocket
|
val sock = udpSocket
|
||||||
val s = settings
|
val s = settings
|
||||||
val id = clientId
|
val id = clientId
|
||||||
if (sock == null || s == null || id == 0) return
|
if (sock == null || s == null || id == 0) {
|
||||||
|
Log.w(TAG, "sendAudioFlush skipped (sock=${sock != null} s=${s != null} id=$id)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(TAG, "sendAudioFlush")
|
||||||
udpSend(sock, s, MsgBuilder.udpFlush(id, nextSeq()))
|
udpSend(sock, s, MsgBuilder.udpFlush(id, nextSeq()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +386,7 @@ class ReflectorClient(
|
|||||||
sendTcp(socket, MsgBuilder.selectTg(s.talkGroup))
|
sendTcp(socket, MsgBuilder.selectTg(s.talkGroup))
|
||||||
sendTcp(socket, MsgBuilder.tgMonitor(listOf(s.talkGroup)))
|
sendTcp(socket, MsgBuilder.tgMonitor(listOf(s.talkGroup)))
|
||||||
} else {
|
} else {
|
||||||
sendTcp(socket, MsgBuilder.selectTg(0))
|
sendTcp(socket, MsgBuilder.selectTg(monitor.first()))
|
||||||
sendTcp(socket, MsgBuilder.tgMonitor(monitor))
|
sendTcp(socket, MsgBuilder.tgMonitor(monitor))
|
||||||
}
|
}
|
||||||
openUdp(s)
|
openUdp(s)
|
||||||
@@ -396,6 +414,7 @@ class ReflectorClient(
|
|||||||
|
|
||||||
ReflectorProtocol.MSG_ERROR -> {
|
ReflectorProtocol.MSG_ERROR -> {
|
||||||
val err = MsgParser.errorMessage(msg.fields)
|
val err = MsgParser.errorMessage(msg.fields)
|
||||||
|
Log.w(TAG, "MSG_ERROR: $err")
|
||||||
if (state < STATE_READY) {
|
if (state < STATE_READY) {
|
||||||
stopFatal(err.ifEmpty { "Access denied" })
|
stopFatal(err.ifEmpty { "Access denied" })
|
||||||
return false
|
return false
|
||||||
@@ -498,7 +517,7 @@ class ReflectorClient(
|
|||||||
val seq = r.u16()
|
val seq = r.u16()
|
||||||
val payload = data.copyOfRange(6, data.size)
|
val payload = data.copyOfRange(6, data.size)
|
||||||
when (type) {
|
when (type) {
|
||||||
ReflectorProtocol.MSG_UDP_HEARTBEAT -> Unit
|
ReflectorProtocol.MSG_UDP_HEARTBEAT -> Log.d(TAG, "UDP RX heartbeat")
|
||||||
ReflectorProtocol.MSG_UDP_AUDIO -> {
|
ReflectorProtocol.MSG_UDP_AUDIO -> {
|
||||||
val audio = MsgParser.udpAudio(payload)
|
val audio = MsgParser.udpAudio(payload)
|
||||||
if (audio != null && audio.isNotEmpty()) {
|
if (audio != null && audio.isNotEmpty()) {
|
||||||
@@ -512,7 +531,7 @@ class ReflectorClient(
|
|||||||
Log.d(TAG, "UDP RX flush")
|
Log.d(TAG, "UDP RX flush")
|
||||||
callbacks.onAudioFlush()
|
callbacks.onAudioFlush()
|
||||||
}
|
}
|
||||||
ReflectorProtocol.MSG_UDP_ALL_SAMPLES_FLUSHED -> Unit
|
ReflectorProtocol.MSG_UDP_ALL_SAMPLES_FLUSHED -> Log.d(TAG, "UDP RX all-flushed seq=$seq")
|
||||||
else -> Log.d(TAG, "UDP RX type=$type len=${data.size}")
|
else -> Log.d(TAG, "UDP RX type=$type len=${data.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,10 +555,16 @@ class ReflectorClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendRawUdp(sock: DatagramSocket, addr: InetAddress, port: Int, data: ByteArray) {
|
private fun sendRawUdp(sock: DatagramSocket, addr: InetAddress, port: Int, data: ByteArray) {
|
||||||
if (sock.isClosed) return
|
if (sock.isClosed) {
|
||||||
|
Log.w(TAG, "UDP TX skipped: socket closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
sock.send(DatagramPacket(data, data.size, addr, port))
|
sock.send(DatagramPacket(data, data.size, addr, port))
|
||||||
|
val typ = if (data.size >= 2) ((data[0].toInt() shl 8) or (data[1].toInt() and 0xFF)) else -1
|
||||||
|
Log.d(TAG, "UDP TX type=$typ len=${data.size}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "UDP send error: ${e.message}", e)
|
||||||
callbacks.onError("UDP send: ${e.message}")
|
callbacks.onError("UDP send: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<string name="btn_monitor_on">Мониторинг групп включен (нажмите, чтобы выключить)</string>
|
<string name="btn_monitor_on">Мониторинг групп включен (нажмите, чтобы выключить)</string>
|
||||||
<string name="status_monitoring">Мониторинг: %1$s</string>
|
<string name="status_monitoring">Мониторинг: %1$s</string>
|
||||||
<string name="talker_mon">Говорит: %1$s · TG%2$d</string>
|
<string name="talker_mon">Говорит: %1$s · TG%2$d</string>
|
||||||
|
<string name="talker_mon_priority">ПРИОРИТЕТ: %1$s · TG%2$d</string>
|
||||||
<string name="status_big_disconnected">DISCONNECTED</string>
|
<string name="status_big_disconnected">DISCONNECTED</string>
|
||||||
<string name="status_big_connected">CONNECTED</string>
|
<string name="status_big_connected">CONNECTED</string>
|
||||||
<string name="status_big_talking">ПЕРЕДАЧА</string>
|
<string name="status_big_talking">ПЕРЕДАЧА</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user