Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9bb48f7e | ||
|
|
c8daa276fc | ||
|
|
1bfac034cd | ||
|
|
7b8e897e8a | ||
|
|
c279982778 | ||
|
|
3b2a31e133 |
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "ru.ua1zbe.svxremote"
|
applicationId = "ru.ua1zbe.svxremote"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 11
|
versionCode = 13
|
||||||
versionName = "1.0.9"
|
versionName = "1.0.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package ru.ua1zbe.svxremote
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.CheckBox
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import ru.ua1zbe.svxremote.prefs.Prefs
|
import ru.ua1zbe.svxremote.prefs.Prefs
|
||||||
@@ -25,6 +28,23 @@ class AudioSettingsActivity : Activity() {
|
|||||||
val lblRxBuffer = findViewById<TextView>(R.id.lbl_rx_buffer_value)
|
val lblRxBuffer = findViewById<TextView>(R.id.lbl_rx_buffer_value)
|
||||||
val seekJitter = findViewById<SeekBar>(R.id.seek_jitter)
|
val seekJitter = findViewById<SeekBar>(R.id.seek_jitter)
|
||||||
val lblJitter = findViewById<TextView>(R.id.lbl_jitter_value)
|
val lblJitter = findViewById<TextView>(R.id.lbl_jitter_value)
|
||||||
|
val spinBitrate = findViewById<Spinner>(R.id.spin_tx_bitrate)
|
||||||
|
|
||||||
|
// Скорость передачи Opus: Авто + фиксированные 6..32 кбит/с.
|
||||||
|
// SvxLink: Opus 8 кГц, кадр 20 мс; декодер принимает любой битрейт,
|
||||||
|
// поэтому выбор — на стороне клиента. Авто = адаптация под интернет.
|
||||||
|
val bitrates = intArrayOf(0, 6, 8, 10, 12, 16, 20, 24, 32)
|
||||||
|
val bitrateLabels = bitrates.map {
|
||||||
|
if (it == 0) getString(R.string.audio_tx_bitrate_auto)
|
||||||
|
else getString(R.string.audio_tx_bitrate_fmt, it)
|
||||||
|
}
|
||||||
|
spinBitrate.adapter = ArrayAdapter(
|
||||||
|
this, android.R.layout.simple_spinner_item, bitrateLabels
|
||||||
|
).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
val savedBitrate = Prefs.txBitrateKbps(this)
|
||||||
|
spinBitrate.setSelection(bitrates.indexOfFirst { it == savedBitrate }.coerceAtLeast(0))
|
||||||
|
|
||||||
fun updateMicGainLabel() {
|
fun updateMicGainLabel() {
|
||||||
lblMicGain.text = "${100 + seekMicGain.progress * 10}%"
|
lblMicGain.text = "${100 + seekMicGain.progress * 10}%"
|
||||||
@@ -59,10 +79,15 @@ class AudioSettingsActivity : Activity() {
|
|||||||
seekRxBuffer.setOnSeekBarChangeListener(listener)
|
seekRxBuffer.setOnSeekBarChangeListener(listener)
|
||||||
seekJitter.setOnSeekBarChangeListener(listener)
|
seekJitter.setOnSeekBarChangeListener(listener)
|
||||||
|
|
||||||
|
val chkRxStats = findViewById<CheckBox>(R.id.chk_rx_stats)
|
||||||
|
chkRxStats.isChecked = Prefs.rxStatsEnabled(this)
|
||||||
|
|
||||||
findViewById<Button>(R.id.btn_audio_save).setOnClickListener {
|
findViewById<Button>(R.id.btn_audio_save).setOnClickListener {
|
||||||
Prefs.setMicGain(this, 1.0f + seekMicGain.progress * 0.1f)
|
Prefs.setMicGain(this, 1.0f + seekMicGain.progress * 0.1f)
|
||||||
Prefs.setRxBufferMs(this, 100 + seekRxBuffer.progress * 20)
|
Prefs.setRxBufferMs(this, 100 + seekRxBuffer.progress * 20)
|
||||||
Prefs.setRxJitterMs(this, seekJitter.progress * 20)
|
Prefs.setRxJitterMs(this, seekJitter.progress * 20)
|
||||||
|
Prefs.setTxBitrateKbps(this, bitrates[spinBitrate.selectedItemPosition])
|
||||||
|
Prefs.setRxStatsEnabled(this, chkRxStats.isChecked)
|
||||||
Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
private lateinit var connectZone: View
|
private lateinit var connectZone: View
|
||||||
private lateinit var rxMeter: SMeterView
|
private lateinit var rxMeter: SMeterView
|
||||||
private lateinit var micMeter: SMeterView
|
private lateinit var micMeter: SMeterView
|
||||||
|
private lateinit var lblRxStats: TextView
|
||||||
private lateinit var pttButton: PttButton
|
private lateinit var pttButton: PttButton
|
||||||
private lateinit var btnSettings: ImageButton
|
private lateinit var btnSettings: ImageButton
|
||||||
private lateinit var btnMonitor: ImageButton
|
private lateinit var btnMonitor: ImageButton
|
||||||
@@ -142,6 +143,8 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
rxMeter = findViewById(R.id.rx_meter)
|
rxMeter = findViewById(R.id.rx_meter)
|
||||||
micMeter = findViewById(R.id.mic_meter)
|
micMeter = findViewById(R.id.mic_meter)
|
||||||
micMeter.setColor(0xFFF85149.toInt())
|
micMeter.setColor(0xFFF85149.toInt())
|
||||||
|
lblRxStats = findViewById(R.id.lbl_rx_stats)
|
||||||
|
lblRxStats.visibility = if (Prefs.rxStatsEnabled(this)) View.VISIBLE else View.GONE
|
||||||
pttButton = findViewById(R.id.ptt_button)
|
pttButton = findViewById(R.id.ptt_button)
|
||||||
btnSettings = findViewById(R.id.btn_settings)
|
btnSettings = findViewById(R.id.btn_settings)
|
||||||
btnMonitor = findViewById(R.id.btn_monitor)
|
btnMonitor = findViewById(R.id.btn_monitor)
|
||||||
@@ -200,7 +203,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
val reset = Runnable { titleTaps = 0 }
|
val reset = Runnable { titleTaps = 0 }
|
||||||
titleResetRunnable = reset
|
titleResetRunnable = reset
|
||||||
mainHandler.postDelayed(reset, 1200)
|
mainHandler.postDelayed(reset, 2000)
|
||||||
if (titleTaps >= 3) {
|
if (titleTaps >= 3) {
|
||||||
titleTaps = 0
|
titleTaps = 0
|
||||||
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
@@ -498,6 +501,14 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRxStats(lossPct: Double, jitterMs: Double) {
|
||||||
|
mainHandler.post {
|
||||||
|
if (!Prefs.rxStatsEnabled(this)) return@post
|
||||||
|
lblRxStats.visibility = View.VISIBLE
|
||||||
|
lblRxStats.text = getString(R.string.rx_stats_fmt, lossPct, jitterMs.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Сбросить таймер молчания: группа жива — продолжаем её слушать. */
|
/** Сбросить таймер молчания: группа жива — продолжаем её слушать. */
|
||||||
private fun resetMonitorIdle() {
|
private fun resetMonitorIdle() {
|
||||||
monitorIdleTask?.let { mainHandler.removeCallbacks(it) }
|
monitorIdleTask?.let { mainHandler.removeCallbacks(it) }
|
||||||
|
|||||||
@@ -31,12 +31,16 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
|||||||
fun onAudioFlush()
|
fun onAudioFlush()
|
||||||
fun onError(detail: String)
|
fun onError(detail: String)
|
||||||
fun onLevel(source: Int, level: Float)
|
fun onLevel(source: Int, level: Float)
|
||||||
|
fun onRxStats(lossPct: Double, jitterMs: Double)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
private val engine by lazy {
|
private val engine by lazy {
|
||||||
OpusEngine { source, level -> listener?.onLevel(source, level) }
|
OpusEngine(
|
||||||
|
onLevel = { source, level -> listener?.onLevel(source, level) },
|
||||||
|
onRxStats = { lossPct, jitterMs -> listener?.onRxStats(lossPct, jitterMs) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val client: ReflectorClient by lazy { ReflectorClient(this) }
|
val client: ReflectorClient by lazy { ReflectorClient(this) }
|
||||||
|
|
||||||
@@ -121,6 +125,7 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
|||||||
updateNotification(getString(R.string.notif_connected))
|
updateNotification(getString(R.string.notif_connected))
|
||||||
engine.rxBufferMs = Prefs.rxBufferMs(this)
|
engine.rxBufferMs = Prefs.rxBufferMs(this)
|
||||||
engine.jitterFrames = (Prefs.rxJitterMs(this) / 20).coerceAtLeast(1)
|
engine.jitterFrames = (Prefs.rxJitterMs(this) / 20).coerceAtLeast(1)
|
||||||
|
engine.txBitrateKbps = Prefs.txBitrateKbps(this)
|
||||||
engine.startDecoder()
|
engine.startDecoder()
|
||||||
}
|
}
|
||||||
ReflectorClient.STATE_DISCONNECTED -> {
|
ReflectorClient.STATE_DISCONNECTED -> {
|
||||||
@@ -174,7 +179,7 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioPacket(packet: ByteArray, seq: Int) {
|
override fun onAudioPacket(packet: ByteArray, seq: Int) {
|
||||||
engine.onAudioPacket(packet)
|
engine.onAudioPacket(packet, seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioFlush() {
|
override fun onAudioFlush() {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import kotlin.math.log10
|
|||||||
* SVXLink: моно, 8 кГц, 16 бит PCM, кадры 20 мс. Поток уровня сигнала 0..1.
|
* SVXLink: моно, 8 кГц, 16 бит PCM, кадры 20 мс. Поток уровня сигнала 0..1.
|
||||||
*/
|
*/
|
||||||
class OpusEngine(
|
class OpusEngine(
|
||||||
private val onLevel: (source: Int, level: Float) -> Unit
|
private val onLevel: (source: Int, level: Float) -> Unit,
|
||||||
|
private val onRxStats: ((lossPct: Double, jitterMs: Double) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -30,6 +31,10 @@ class OpusEngine(
|
|||||||
const val CHANNELS = 1
|
const val CHANNELS = 1
|
||||||
const val FRAME_SIZE = 160 // 20 мс в 8 кГц
|
const val FRAME_SIZE = 160 // 20 мс в 8 кГц
|
||||||
const val BIT_RATE = 20000
|
const val BIT_RATE = 20000
|
||||||
|
// Авто-адаптация битрейта TX (0 = авто включено в настройках)
|
||||||
|
const val AUTO_BITRATE_MIN = 8
|
||||||
|
const val AUTO_BITRATE_MAX = 24
|
||||||
|
const val AUTO_BITRATE_STEP = 4
|
||||||
|
|
||||||
const val SRC_MIC = 0
|
const val SRC_MIC = 0
|
||||||
const val SRC_SPEAKER = 1
|
const val SRC_SPEAKER = 1
|
||||||
@@ -64,9 +69,34 @@ class OpusEngine(
|
|||||||
var rxBufferMs = 417 // буфер AudioTrack, мс (40000 байт ≈ 417 мс при 48 кГц)
|
var rxBufferMs = 417 // буфер AudioTrack, мс (40000 байт ≈ 417 мс при 48 кГц)
|
||||||
@Volatile
|
@Volatile
|
||||||
var jitterFrames = 10 // сколько кадров накопить до первого слива (кадр = 20 мс)
|
var jitterFrames = 10 // сколько кадров накопить до первого слива (кадр = 20 мс)
|
||||||
|
// Скорость передачи Opus, кбит/с. 0 = Авто (адаптация под интернет).
|
||||||
|
@Volatile
|
||||||
|
var txBitrateKbps = 0
|
||||||
|
// Текущий битрейт в режиме Авто (пересчитывается по статистике приёма)
|
||||||
|
@Volatile
|
||||||
|
private var autoKbps = 16
|
||||||
private val configBuffers = ArrayList<ByteArray>()
|
private val configBuffers = ArrayList<ByteArray>()
|
||||||
private var configIdx = 0
|
private var configIdx = 0
|
||||||
|
|
||||||
|
// ---------- Статистика приёма для авто-битрейта ----------
|
||||||
|
private var lastSeq = -1
|
||||||
|
private var lastPktTime = 0L
|
||||||
|
private var lossCount = 0L
|
||||||
|
private var pktCount = 0L
|
||||||
|
private var jitterAccum = 0.0
|
||||||
|
private var jitterN = 0
|
||||||
|
private var lastEvalTime = 0L
|
||||||
|
private var goodWindows = 0
|
||||||
|
|
||||||
|
/** Размер пакета при 20 мс кадре: bitrate * 0.02 / 8 байт. */
|
||||||
|
fun bitrateBytes(kbps: Int): Int = (kbps * 20) / 8
|
||||||
|
|
||||||
|
/** Эффективный битрейт для следующей передачи, бит/с. */
|
||||||
|
fun effectiveBitrate(): Int {
|
||||||
|
val kbps = if (txBitrateKbps > 0) txBitrateKbps else autoKbps
|
||||||
|
return kbps * 1000
|
||||||
|
}
|
||||||
|
|
||||||
fun startDecoder() {
|
fun startDecoder() {
|
||||||
if (decoder != null) return
|
if (decoder != null) return
|
||||||
val format = MediaFormat()
|
val format = MediaFormat()
|
||||||
@@ -283,7 +313,8 @@ class OpusEngine(
|
|||||||
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(v).array()
|
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(v).array()
|
||||||
|
|
||||||
/** Подача одного Opus-пакета на декодер (с UDP-потока). */
|
/** Подача одного Opus-пакета на декодер (с UDP-потока). */
|
||||||
fun onAudioPacket(packet: ByteArray) {
|
fun onAudioPacket(packet: ByteArray, seq: Int) {
|
||||||
|
updateRxStats(seq)
|
||||||
lastRealPacketTime = System.currentTimeMillis()
|
lastRealPacketTime = System.currentTimeMillis()
|
||||||
rxQueue.add(packet)
|
rxQueue.add(packet)
|
||||||
receivedAny = true
|
receivedAny = true
|
||||||
@@ -293,11 +324,67 @@ class OpusEngine(
|
|||||||
decoder?.let { feedDecoder(it, idx, rxQueue.poll() ?: packet) }
|
decoder?.let { feedDecoder(it, idx, rxQueue.poll() ?: packet) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Джиттер-гейт открыт: накопили кадры или вышло 150 мс с первого пакета. */
|
/**
|
||||||
|
* Статистика приёма (потери по seq, джиттер по интервалам пакетов).
|
||||||
|
* Раз в 5 секунд корректирует autoKbps: плохо — ниже, стабильно — выше.
|
||||||
|
*/
|
||||||
|
private fun updateRxStats(seq: Int) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (lastSeq >= 0) {
|
||||||
|
val dt = now - lastPktTime
|
||||||
|
// seq — счётчик ГОВОРЯЩЕГО: при повторах кадров он может
|
||||||
|
// повторяться или идти назад — это не потери. Пауза > 200 мс
|
||||||
|
// между пакетами — смена реплики/говорящего, тоже не потери.
|
||||||
|
if (dt <= 200) {
|
||||||
|
val gap = (seq - lastSeq - 1).toLong()
|
||||||
|
if (gap in 1..50000) lossCount += gap
|
||||||
|
}
|
||||||
|
pktCount++
|
||||||
|
val j = abs(dt - 20).toDouble()
|
||||||
|
if (j < 100) {
|
||||||
|
jitterAccum += j
|
||||||
|
jitterN++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastSeq = seq
|
||||||
|
lastPktTime = now
|
||||||
|
if (lastEvalTime == 0L) {
|
||||||
|
lastEvalTime = now
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (now - lastEvalTime < 5000 || pktCount < 30) return
|
||||||
|
val lossPct = lossCount * 100.0 / pktCount
|
||||||
|
val jitterMs = if (jitterN > 0) jitterAccum / jitterN else 0.0
|
||||||
|
Log.d(TAG, "stats window: pkt=$pktCount loss=$lossCount pct=$lossPct j=$jitterMs lastSeq=$lastSeq")
|
||||||
|
if (lossPct > 3.0 || jitterMs > 60.0) {
|
||||||
|
if (autoKbps > AUTO_BITRATE_MIN) {
|
||||||
|
autoKbps -= AUTO_BITRATE_STEP
|
||||||
|
Log.d(TAG, "auto bitrate down: ${autoKbps}kbps (loss=${"%.1f".format(lossPct)}% jitter=${"%.0f".format(jitterMs)}ms)")
|
||||||
|
}
|
||||||
|
goodWindows = 0
|
||||||
|
} else if (lossPct < 0.5 && jitterMs < 25.0) {
|
||||||
|
goodWindows++
|
||||||
|
if (goodWindows >= 2 && autoKbps < AUTO_BITRATE_MAX) {
|
||||||
|
autoKbps += AUTO_BITRATE_STEP
|
||||||
|
Log.d(TAG, "auto bitrate up: ${autoKbps}kbps")
|
||||||
|
goodWindows = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
goodWindows = 0
|
||||||
|
}
|
||||||
|
lossCount = 0
|
||||||
|
pktCount = 0
|
||||||
|
jitterAccum = 0.0
|
||||||
|
jitterN = 0
|
||||||
|
lastEvalTime = now
|
||||||
|
onRxStats?.invoke(lossPct, jitterMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Джиттер-гейт открыт: накопили кадры или вышло 300 мс с первого пакета. */
|
||||||
private fun gateOpen(): Boolean {
|
private fun gateOpen(): Boolean {
|
||||||
if (playStarted) return true
|
if (playStarted) return true
|
||||||
if (rxQueue.size >= jitterFrames) return true
|
if (rxQueue.size >= jitterFrames) return true
|
||||||
return gateStart != 0L && System.currentTimeMillis() - gateStart >= 150
|
return gateStart != 0L && System.currentTimeMillis() - gateStart >= 300
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAudioFlush() {
|
fun onAudioFlush() {
|
||||||
@@ -441,11 +528,12 @@ class OpusEngine(
|
|||||||
fmt.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_OPUS)
|
fmt.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_OPUS)
|
||||||
fmt.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE)
|
fmt.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE)
|
||||||
fmt.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS)
|
fmt.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS)
|
||||||
fmt.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
|
fmt.setInteger(MediaFormat.KEY_BIT_RATE, effectiveBitrate())
|
||||||
fmt.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, FRAME_SIZE * 2)
|
fmt.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, FRAME_SIZE * 2)
|
||||||
val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_OPUS)
|
val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_OPUS)
|
||||||
codec.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
codec.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
codec.start()
|
codec.start()
|
||||||
|
Log.d(TAG, "encoder started, bitrate=${effectiveBitrate() / 1000}kbps")
|
||||||
codec
|
codec
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -162,4 +162,20 @@ object Prefs {
|
|||||||
fun setRxJitterMs(context: Context, ms: Int) {
|
fun setRxJitterMs(context: Context, ms: Int) {
|
||||||
sp(context).edit().putInt("rx_jitter_ms", ms.coerceIn(0, 400)).apply()
|
sp(context).edit().putInt("rx_jitter_ms", ms.coerceIn(0, 400)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Скорость передачи Opus, кбит/с. 0 = Авто (адаптация под интернет). */
|
||||||
|
fun txBitrateKbps(context: Context): Int =
|
||||||
|
sp(context).getInt("tx_bitrate_kbps", 0)
|
||||||
|
|
||||||
|
fun setTxBitrateKbps(context: Context, kbps: Int) {
|
||||||
|
sp(context).edit().putInt("tx_bitrate_kbps", kbps.coerceIn(0, 32)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Показывать строку статистики приёма (потери/джиттер) на главном экране. */
|
||||||
|
fun rxStatsEnabled(context: Context): Boolean =
|
||||||
|
sp(context).getBoolean("rx_stats_enabled", true)
|
||||||
|
|
||||||
|
fun setRxStatsEnabled(context: Context, enabled: Boolean) {
|
||||||
|
sp(context).edit().putBoolean("rx_stats_enabled", enabled).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class ReflectorClient(
|
|||||||
if (data.size < 6) return
|
if (data.size < 6) return
|
||||||
val r = BinReader(data)
|
val r = BinReader(data)
|
||||||
val type = r.u16()
|
val type = r.u16()
|
||||||
r.u16() // client id
|
val cid = r.u16() // client id
|
||||||
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) {
|
||||||
@@ -549,7 +549,7 @@ class ReflectorClient(
|
|||||||
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()) {
|
||||||
Log.d(TAG, "UDP RX audio seq=$seq len=${audio.size}")
|
Log.d(TAG, "UDP RX audio seq=$seq cid=$cid len=${audio.size}")
|
||||||
callbacks.onAudioPacket(audio, seq)
|
callbacks.onAudioPacket(audio, seq)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "UDP RX audio empty/null len=${payload.size}")
|
Log.w(TAG, "UDP RX audio empty/null len=${payload.size}")
|
||||||
|
|||||||
@@ -112,8 +112,38 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:max="20"
|
android:max="20"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/audio_tx_bitrate"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spin_tx_bitrate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/chk_rx_stats"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:text="@string/audio_rx_stats"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_audio_save"
|
android:id="@+id/btn_audio_save"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -181,6 +181,17 @@
|
|||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Статистика приёма (скрытые настройки, по умолчанию выключено) -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/lbl_rx_stats"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:visibility="gone" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Последний говоривший -->
|
<!-- Последний говоривший -->
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
<string name="audio_mic_gain">Усиление микрофона</string>
|
<string name="audio_mic_gain">Усиление микрофона</string>
|
||||||
<string name="audio_rx_buffer">Буфер воспроизведения</string>
|
<string name="audio_rx_buffer">Буфер воспроизведения</string>
|
||||||
<string name="audio_jitter">Накопление перед стартом</string>
|
<string name="audio_jitter">Накопление перед стартом</string>
|
||||||
|
<string name="audio_tx_bitrate">Скорость передачи (Opus)</string>
|
||||||
|
<string name="audio_tx_bitrate_auto">Авто</string>
|
||||||
|
<string name="audio_tx_bitrate_fmt">%1$d кбит/с</string>
|
||||||
|
<string name="audio_rx_stats">Показывать статистику приёма (потери/джиттер)</string>
|
||||||
|
<string name="rx_stats_fmt">RX: потери %1$.1f%%, джиттер %2$d мс</string>
|
||||||
<string name="toast_node_joined">Подключился: %1$s%2$s</string>
|
<string name="toast_node_joined">Подключился: %1$s%2$s</string>
|
||||||
<string name="toast_node_left">Отключился: %1$s%2$s</string>
|
<string name="toast_node_left">Отключился: %1$s%2$s</string>
|
||||||
<string name="node_tg_suffix"> (группа %1$d)</string>
|
<string name="node_tg_suffix"> (группа %1$d)</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user