аудио: убрать потери кадров (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:
@@ -246,12 +246,19 @@ class VmService : LifecycleService() {
|
||||
applyAudioMode(msg.mode)
|
||||
}
|
||||
is ScreenControl -> {
|
||||
Log.i(TAG, "ScreenControl: sleep=${msg.sleep}")
|
||||
controller.setScreen(msg.sleep)
|
||||
// Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние.
|
||||
lifecycleScope.launch {
|
||||
kotlinx.coroutines.delay(600)
|
||||
pushScreenState()
|
||||
val wantOn = !msg.sleep
|
||||
val isOn = currentScreenOn()
|
||||
Log.i(TAG, "ScreenControl: sleep=${msg.sleep} (сейчас on=$isOn)")
|
||||
if (isOn == wantOn) {
|
||||
// POWER — переключатель; если цель уже достигнута, инжектить нельзя (моргнём).
|
||||
lifecycleScope.launch { pushScreenState() }
|
||||
} else {
|
||||
controller.setScreen(msg.sleep)
|
||||
// Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние.
|
||||
lifecycleScope.launch {
|
||||
kotlinx.coroutines.delay(600)
|
||||
pushScreenState()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SetText -> {
|
||||
@@ -319,15 +326,26 @@ class VmService : LifecycleService() {
|
||||
private fun startAudioPump() {
|
||||
lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
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) {
|
||||
val server = audioServer ?: continue
|
||||
if (audioSessions.isEmpty()) continue
|
||||
val t0 = System.nanoTime()
|
||||
val payload = try {
|
||||
audioEncoder.encode(pcm, 0, AudioConfig.FRAME_SAMPLES)
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "opus encode failed: ${t.message}")
|
||||
continue
|
||||
}
|
||||
val t1 = System.nanoTime()
|
||||
val len = AudioDatagram.pack(audioSeq++, payload, payload.size, packet)
|
||||
val datagram = packet.copyOf(len)
|
||||
for (connId in audioSessions) {
|
||||
@@ -337,10 +355,19 @@ class VmService : LifecycleService() {
|
||||
Log.w(TAG, "audio send failed: ${t.message}")
|
||||
}
|
||||
}
|
||||
val t2 = System.nanoTime()
|
||||
encNanos += t1 - t0
|
||||
sndNanos += t2 - t1
|
||||
val encoded = audioEncoded.incrementAndGet()
|
||||
if (encoded % 200 == 0) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ navCompose = "2.10.2"
|
||||
mercury = "0.1.1-SNAPSHOT"
|
||||
mercuryTransport = "0.1.2-SNAPSHOT"
|
||||
mercuryBinocular = "0.1.0-SNAPSHOT"
|
||||
mercuryShizuku = "0.1.1-SNAPSHOT"
|
||||
mercuryShizuku = "0.1.2-SNAPSHOT"
|
||||
|
||||
[libraries]
|
||||
# kotlinx
|
||||
|
||||
@@ -26,11 +26,12 @@ object AudioConfig {
|
||||
const val BITRATE = 64_000
|
||||
|
||||
/**
|
||||
* Сложность Opus-кодера. Чуть ниже максимума (10): Concentus — JVM-порт, а кодирует
|
||||
* он на очках в реальном времени; при 8 качество на 64 кбит/с практически то же,
|
||||
* зато есть запас по CPU.
|
||||
* Сложность Opus-кодера. Concentus — JVM-порт, кодирует на очках в реальном времени;
|
||||
* при complexity 8 один 10-мс stereo-кадр не укладывался в 10 мс, кодер не успевал
|
||||
* (~60 кадров/с при 100 захваченных → 40% кадров терялось в очереди = «хрип»). На 3
|
||||
* запас по CPU большой, а на 64 кбит/с разница в качестве незначительна.
|
||||
*/
|
||||
const val COMPLEXITY = 8
|
||||
const val COMPLEXITY = 3
|
||||
|
||||
/** Ожидаемый процент потерь — для настройки inband FEC/PLC Opus. */
|
||||
const val PACKET_LOSS_PERCENT = 10
|
||||
|
||||
@@ -20,7 +20,10 @@ class OpusAudioEncoder(
|
||||
this.bitrate = bitrate
|
||||
this.complexity = complexity
|
||||
useVBR = true
|
||||
useInbandFEC = true
|
||||
// InbandFEC в Concentus удваивает стоимость кодирования (кодер считает ещё и
|
||||
// резервную копию предыдущего кадра). Потери по UDP у нас ~0, а пропуски seq и так
|
||||
// прячем на декодере (PLC, см. [OpusAudioDecoder.decodeLost]) — FEC только жжёт CPU.
|
||||
useInbandFEC = false
|
||||
packetLossPercent = AudioConfig.PACKET_LOSS_PERCENT
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user