аудио: убрать потери кадров (Concentus не успевал); экран — POWER

- Opus complexity 8->3, отключён inband FEC: кодер выдавал ~60 кадров/с при 100
  захваченных -> 40% кадров терялось в очереди (DROP_OLDEST) = «хрип».
  Стало enc_us~3мс на 10-мс кадр, dropped перестал расти.
- Прогрев кодера тишиной на старте (JIT Concentus) — убирает стартовый всплеск потерь.
- Диагностика в логе: enc_us / snd_us.
- ScreenControl идемпотентен (POWER — переключатель): сверяемся с реальным isInteractive.
- Зависимость shizuku-vd 0.1.1 -> 0.1.2 (POWER-инъекция).
This commit is contained in:
2026-10-01 12:10:30 +03:00
parent 4743427756
commit 20d0d05fb4
4 changed files with 44 additions and 13 deletions
@@ -246,12 +246,19 @@ class VmService : LifecycleService() {
applyAudioMode(msg.mode) applyAudioMode(msg.mode)
} }
is ScreenControl -> { is ScreenControl -> {
Log.i(TAG, "ScreenControl: sleep=${msg.sleep}") val wantOn = !msg.sleep
controller.setScreen(msg.sleep) val isOn = currentScreenOn()
// Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние. Log.i(TAG, "ScreenControl: sleep=${msg.sleep} (сейчас on=$isOn)")
lifecycleScope.launch { if (isOn == wantOn) {
kotlinx.coroutines.delay(600) // POWER — переключатель; если цель уже достигнута, инжектить нельзя (моргнём).
pushScreenState() lifecycleScope.launch { pushScreenState() }
} else {
controller.setScreen(msg.sleep)
// Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние.
lifecycleScope.launch {
kotlinx.coroutines.delay(600)
pushScreenState()
}
} }
} }
is SetText -> { is SetText -> {
@@ -319,15 +326,26 @@ class VmService : LifecycleService() {
private fun startAudioPump() { private fun startAudioPump() {
lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
val packet = ByteArray(AudioConfig.MAX_DATAGRAM) val packet = ByteArray(AudioConfig.MAX_DATAGRAM)
// Прогреваем JIT и состояние кодера тишиной: без этого первые кадры кодировались
// в разы дольше 10 мс (Concentus — JVM, холодный профиль), очередь успевала
// переполниться, и стартовые ~секунды звука терялись («хрип при включении»).
runCatching {
val silence = ShortArray(AudioConfig.FRAME_SHORTS)
repeat(30) { audioEncoder.encode(silence, 0, AudioConfig.FRAME_SAMPLES) }
}
var encNanos = 0L
var sndNanos = 0L
for (pcm in audioFrames) { for (pcm in audioFrames) {
val server = audioServer ?: continue val server = audioServer ?: continue
if (audioSessions.isEmpty()) continue if (audioSessions.isEmpty()) continue
val t0 = System.nanoTime()
val payload = try { val payload = try {
audioEncoder.encode(pcm, 0, AudioConfig.FRAME_SAMPLES) audioEncoder.encode(pcm, 0, AudioConfig.FRAME_SAMPLES)
} catch (t: Throwable) { } catch (t: Throwable) {
Log.w(TAG, "opus encode failed: ${t.message}") Log.w(TAG, "opus encode failed: ${t.message}")
continue continue
} }
val t1 = System.nanoTime()
val len = AudioDatagram.pack(audioSeq++, payload, payload.size, packet) val len = AudioDatagram.pack(audioSeq++, payload, payload.size, packet)
val datagram = packet.copyOf(len) val datagram = packet.copyOf(len)
for (connId in audioSessions) { for (connId in audioSessions) {
@@ -337,10 +355,19 @@ class VmService : LifecycleService() {
Log.w(TAG, "audio send failed: ${t.message}") Log.w(TAG, "audio send failed: ${t.message}")
} }
} }
val t2 = System.nanoTime()
encNanos += t1 - t0
sndNanos += t2 - t1
val encoded = audioEncoded.incrementAndGet() val encoded = audioEncoded.incrementAndGet()
if (encoded % 200 == 0) { if (encoded % 200 == 0) {
val captured = audioCaptured.get() val captured = audioCaptured.get()
Log.i(TAG, "audio: captured=$captured encoded=$encoded dropped=${captured - encoded} sessions=${audioSessions.size}") Log.i(
TAG,
"audio: captured=$captured encoded=$encoded dropped=${captured - encoded} " +
"sessions=${audioSessions.size} enc_us=${encNanos / 200 / 1000} snd_us=${sndNanos / 200 / 1000}",
)
encNanos = 0
sndNanos = 0
} }
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ navCompose = "2.10.2"
mercury = "0.1.1-SNAPSHOT" mercury = "0.1.1-SNAPSHOT"
mercuryTransport = "0.1.2-SNAPSHOT" mercuryTransport = "0.1.2-SNAPSHOT"
mercuryBinocular = "0.1.0-SNAPSHOT" mercuryBinocular = "0.1.0-SNAPSHOT"
mercuryShizuku = "0.1.1-SNAPSHOT" mercuryShizuku = "0.1.2-SNAPSHOT"
[libraries] [libraries]
# kotlinx # kotlinx
@@ -26,11 +26,12 @@ object AudioConfig {
const val BITRATE = 64_000 const val BITRATE = 64_000
/** /**
* Сложность Opus-кодера. Чуть ниже максимума (10): Concentus — JVM-порт, а кодирует * Сложность Opus-кодера. Concentus — JVM-порт, кодирует на очках в реальном времени;
* он на очках в реальном времени; при 8 качество на 64 кбит/с практически то же, * при complexity 8 один 10-мс stereo-кадр не укладывался в 10 мс, кодер не успевал
* зато есть запас по CPU. * (~60 кадров/с при 100 захваченных → 40% кадров терялось в очереди = «хрип»). На 3
* запас по CPU большой, а на 64 кбит/с разница в качестве незначительна.
*/ */
const val COMPLEXITY = 8 const val COMPLEXITY = 3
/** Ожидаемый процент потерь — для настройки inband FEC/PLC Opus. */ /** Ожидаемый процент потерь — для настройки inband FEC/PLC Opus. */
const val PACKET_LOSS_PERCENT = 10 const val PACKET_LOSS_PERCENT = 10
@@ -20,7 +20,10 @@ class OpusAudioEncoder(
this.bitrate = bitrate this.bitrate = bitrate
this.complexity = complexity this.complexity = complexity
useVBR = true useVBR = true
useInbandFEC = true // InbandFEC в Concentus удваивает стоимость кодирования (кодер считает ещё и
// резервную копию предыдущего кадра). Потери по UDP у нас ~0, а пропуски seq и так
// прячем на декодере (PLC, см. [OpusAudioDecoder.decodeLost]) — FEC только жжёт CPU.
useInbandFEC = false
packetLossPercent = AudioConfig.PACKET_LOSS_PERCENT packetLossPercent = AudioConfig.PACKET_LOSS_PERCENT
} }