Fix RX audio for subsequent transmissions (recreate decoder on flush)

This commit is contained in:
ua1zbe
2026-08-17 13:18:50 +03:00
parent 9d7b659231
commit 884678d36d

View File

@@ -75,18 +75,7 @@ class OpusEngine(
// delay + seek preroll) ТОЛЬКО из первых входных буферов // delay + seek preroll) ТОЛЬКО из первых входных буферов
// (см. queueDecoderConfig); csd-ключи MediaFormat он игнорирует, и без // (см. queueDecoderConfig); csd-ключи MediaFormat он игнорирует, и без
// конфига первый же аудио-кадр ломает декодер (C2_CORRUPTED). // конфига первый же аудио-кадр ломает декодер (C2_CORRUPTED).
decoder = try { if (!startDecoderCodec(format)) return
MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_OPUS).also {
it.setCallback(decoderCallback)
it.configure(format, null, null, 0)
queueDecoderConfig() // готовим конфиг ДО start: колбэки уйдут после
it.start()
}
} catch (e: Exception) {
Log.e(TAG, "createDecoder failed: ${e.message}")
null
}
if (decoder == null) return
Log.d(TAG, "decoder started, config=${configBuffers.size}B") Log.d(TAG, "decoder started, config=${configBuffers.size}B")
val minBuf = AudioTrack.getMinBufferSize( val minBuf = AudioTrack.getMinBufferSize(
OUT_SAMPLE_RATE, OUT_SAMPLE_RATE,
@@ -112,13 +101,51 @@ class OpusEngine(
Log.d(TAG, "decoder started") Log.d(TAG, "decoder started")
} }
/** Создать и запустить только декодер (плеер не трогаем). */
private fun startDecoderCodec(format: MediaFormat): Boolean {
val dec = try {
MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_OPUS).also {
it.setCallback(decoderCallback)
it.configure(format, null, null, 0)
queueDecoderConfig() // готовим конфиг ДО start: колбэки уйдут после
}
} catch (e: Exception) {
Log.e(TAG, "createDecoder failed: ${e.message}")
null
}
if (dec == null) return false
// decoder назначаем ДО start(): колбэки нового кодека сразу проходят
// проверку codec !== decoder, иначе первый конфиг-колбэк отбрасывается
// и декодер навсегда остаётся без конфига
decoder = dec
freeInputs.clear()
rxQueue.clear()
receivedAny = false
playStarted = false
gateStart = 0L
try {
dec.start()
} catch (e: Exception) {
Log.e(TAG, "decoder start failed: ${e.message}")
decoder = null
try {
dec.release()
} catch (_: Exception) {
}
return false
}
return true
}
/** Асинхронные колбэки кодера: подача входных кадров + слив вывода. */ /** Асинхронные колбэки кодера: подача входных кадров + слив вывода. */
private val decoderCallback = object : MediaCodec.Callback() { private val decoderCallback = object : MediaCodec.Callback() {
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
if (codec !== decoder) return // колбэк от уже пересозданного декодера
Log.e(TAG, "decoder error: ${e.message} code=${e.errorCode} transient=${e.isTransient}") Log.e(TAG, "decoder error: ${e.message} code=${e.errorCode} transient=${e.isTransient}")
} }
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
if (codec !== decoder) return // колбэк от уже пересозданного декодера
// Конфиг (csd) первыми входными буферами — в async-режиме нельзя // Конфиг (csd) первыми входными буферами — в async-режиме нельзя
// использовать dequeueInputBuffer, только колбэки // использовать dequeueInputBuffer, только колбэки
if (configIdx < configBuffers.size) { if (configIdx < configBuffers.size) {
@@ -156,6 +183,7 @@ class OpusEngine(
} }
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
if (codec !== decoder) return // колбэк от уже пересозданного декодера
try { try {
if (info.size > 0) { if (info.size > 0) {
val outBuf = ByteBuffer.allocate(info.size) val outBuf = ByteBuffer.allocate(info.size)
@@ -193,6 +221,7 @@ class OpusEngine(
} }
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
if (codec !== decoder) return // колбэк от уже пересозданного декодера
Log.d(TAG, "dec out format: sr=${format.getInteger(MediaFormat.KEY_SAMPLE_RATE)} " + Log.d(TAG, "dec out format: sr=${format.getInteger(MediaFormat.KEY_SAMPLE_RATE)} " +
"ch=${format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)}") "ch=${format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)}")
} }
@@ -262,17 +291,47 @@ class OpusEngine(
fun onAudioFlush() { fun onAudioFlush() {
try { try {
decoder?.flush() // MediaCodec.flush() возвращает входные буферы, но уже выданные индексы
// остаются у нас в freeInputs и дублируются колбэками «повторной выдачи» —
// queueInputBuffer падает (index not owned by client) и кадры теряются.
// Надёжнее пересоздать декодер: колбэки старого отсекаются проверкой
// codec !== decoder.
player?.flush() player?.flush()
rxQueue.clear() rxQueue.clear()
receivedAny = false receivedAny = false
playStarted = false playStarted = false
gateStart = 0L gateStart = 0L
onLevel(SRC_SPEAKER, 0f) onLevel(SRC_SPEAKER, 0f)
restartDecoder()
} catch (_: Exception) { } catch (_: Exception) {
} }
} }
/** Пересоздать только декодер (плеер и джиттер-буфер сохраняем). */
private fun restartDecoder() {
val dec = decoder
decoder = null
if (dec != null) {
try {
dec.stop()
} catch (_: Exception) {
}
try {
dec.release()
} catch (_: Exception) {
}
}
val format = MediaFormat()
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_OPUS)
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, OUT_SAMPLE_RATE)
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS)
if (startDecoderCodec(format)) {
concealLogs = 0
dbgOut = 0
ptsCounter = 20_000L
}
}
/** /**
* Один кадр тишины (20 мс) для скрытия потерь вместо паузы в звуке. * Один кадр тишины (20 мс) для скрытия потерь вместо паузы в звуке.
* Сгенерирован opuslib: 8 кГц, моно, 20 кбит/с, нули PCM. * Сгенерирован opuslib: 8 кГц, моно, 20 кбит/с, нули PCM.