From 268c9d76af5760af8771d394e77b1e1291d74263 Mon Sep 17 00:00:00 2001 From: ua1zbe Date: Mon, 17 Aug 2026 08:22:47 +0300 Subject: [PATCH] Fix flush on PTT release; monitoring priority tweaks (v1.0.5) --- .../java/ru/ua1zbe/svxremote/MainActivity.kt | 28 ++++++++++++--- .../java/ru/ua1zbe/svxremote/SvxService.kt | 16 +++++++-- .../ru/ua1zbe/svxremote/audio/OpusEngine.kt | 31 ++++++++++++++-- .../ua1zbe/svxremote/proto/ReflectorClient.kt | 35 ++++++++++++++++--- app/src/main/res/values/strings.xml | 1 + 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt index 05795f4..a7d7af8 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt @@ -65,6 +65,7 @@ class MainActivity : Activity(), SvxService.Listener { private var connected = false private var monitoring = false + private var priorityActive = false private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11) private var svc: SvxService? = null @@ -159,6 +160,7 @@ class MainActivity : Activity(), SvxService.Listener { } override fun onPttUp() { + Log.d("SVXRemote", "PTT up") svc?.stopTx() lblPttHint.text = getString(R.string.hold_to_talk) lblTxState.text = @@ -291,17 +293,32 @@ class MainActivity : Activity(), SvxService.Listener { override fun onTalkerStart(tg: Long, callsign: String) { mainHandler.post { - lblTalkerTitle.text = if (monitoring) { - getString(R.string.talker_mon, callsign, tg) - } else { - getString(R.string.talker_now, callsign) + val priority = monitoring && settings.groups.firstOrNull() == tg + if (priority) { + priorityActive = true + } + 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) { // последний говоривший остаётся на экране + mainHandler.post { + if (monitoring && settings.groups.firstOrNull() == tg) { + priorityActive = false + } + } } override fun onNodeJoined(callsign: String, tg: Long?) { @@ -416,6 +433,7 @@ class MainActivity : Activity(), SvxService.Listener { /** Включить/выключить режим мониторинга групп с переподключением. */ private fun toggleMonitoring() { monitoring = !monitoring + priorityActive = false updateMonitorButton() updatePttEnabled() if (connected) { diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt b/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt index e8d55fd..d326b0c 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt @@ -8,6 +8,7 @@ import android.app.Service import android.content.Intent import android.os.Binder import android.os.IBinder +import android.util.Log import ru.ua1zbe.svxremote.audio.OpusEngine import ru.ua1zbe.svxremote.proto.MsgParser import ru.ua1zbe.svxremote.proto.ReflectorClient @@ -87,13 +88,22 @@ class SvxService : Service(), ReflectorClient.Callbacks { fun isRunning(): Boolean = client.isRunning() + fun selectGroup(tg: Long) { + client.selectGroup(tg) + } + fun startTx() { engine.startCapture { packet -> client.sendAudio(packet) } } fun stopTx() { - engine.stopCapture() - client.sendAudioFlush() + Log.d("SVXRemote", "stopTx") + // В фоне: сеть запрещена в главном потоке (NetworkOnMainThreadException), + // а join потока захвата блокировал бы UI + Thread { + engine.stopCapture() + client.sendAudioFlush() + }.start() } // ---------- Callbacks (фоновые потоки) ---------- @@ -165,4 +175,4 @@ class SvxService : Service(), ReflectorClient.Callbacks { override fun onError(detail: String) { listener?.onError(detail) } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt b/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt index e233df0..1cca3a7 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt @@ -308,14 +308,43 @@ class OpusEngine( } 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() { + if (!encoding && recThread == null) return encoding = false try { recorder?.stop() } catch (_: Exception) { } + // Ждём завершения потока захвата: он должен отправить последние пакеты, + // чтобы flush (окончание передачи) ушёл после всего аудио + val t = recThread + recThread = null + if (t != null && t.isAlive) { + try { + t.join(1000) + } catch (_: InterruptedException) { + } + } try { recorder?.release() } catch (_: Exception) { @@ -330,8 +359,6 @@ class OpusEngine( } catch (_: Exception) { } encoder = null - recThread?.interrupt() - recThread = null onLevel(SRC_MIC, 0f) } diff --git a/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt b/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt index f74c9cd..0448c81 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt @@ -153,12 +153,30 @@ class ReflectorClient( 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() { val sock = udpSocket val s = settings 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())) } @@ -368,7 +386,7 @@ class ReflectorClient( sendTcp(socket, MsgBuilder.selectTg(s.talkGroup)) sendTcp(socket, MsgBuilder.tgMonitor(listOf(s.talkGroup))) } else { - sendTcp(socket, MsgBuilder.selectTg(0)) + sendTcp(socket, MsgBuilder.selectTg(monitor.first())) sendTcp(socket, MsgBuilder.tgMonitor(monitor)) } openUdp(s) @@ -396,6 +414,7 @@ class ReflectorClient( ReflectorProtocol.MSG_ERROR -> { val err = MsgParser.errorMessage(msg.fields) + Log.w(TAG, "MSG_ERROR: $err") if (state < STATE_READY) { stopFatal(err.ifEmpty { "Access denied" }) return false @@ -498,7 +517,7 @@ class ReflectorClient( val seq = r.u16() val payload = data.copyOfRange(6, data.size) when (type) { - ReflectorProtocol.MSG_UDP_HEARTBEAT -> Unit + ReflectorProtocol.MSG_UDP_HEARTBEAT -> Log.d(TAG, "UDP RX heartbeat") ReflectorProtocol.MSG_UDP_AUDIO -> { val audio = MsgParser.udpAudio(payload) if (audio != null && audio.isNotEmpty()) { @@ -512,7 +531,7 @@ class ReflectorClient( Log.d(TAG, "UDP RX flush") 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}") } } @@ -536,10 +555,16 @@ class ReflectorClient( } 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 { 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) { + Log.e(TAG, "UDP send error: ${e.message}", e) callbacks.onError("UDP send: ${e.message}") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dececf..c0f4761 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Мониторинг групп включен (нажмите, чтобы выключить) Мониторинг: %1$s Говорит: %1$s · TG%2$d + ПРИОРИТЕТ: %1$s · TG%2$d DISCONNECTED CONNECTED ПЕРЕДАЧА