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
ПЕРЕДАЧА