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 1f95b00..7ad44c3 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/audio/OpusEngine.kt @@ -75,18 +75,7 @@ class OpusEngine( // delay + seek preroll) ТОЛЬКО из первых входных буферов // (см. queueDecoderConfig); csd-ключи MediaFormat он игнорирует, и без // конфига первый же аудио-кадр ломает декодер (C2_CORRUPTED). - decoder = try { - 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 + if (!startDecoderCodec(format)) return Log.d(TAG, "decoder started, config=${configBuffers.size}B") val minBuf = AudioTrack.getMinBufferSize( OUT_SAMPLE_RATE, @@ -112,13 +101,51 @@ class OpusEngine( 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() { 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}") } override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { + if (codec !== decoder) return // колбэк от уже пересозданного декодера // Конфиг (csd) первыми входными буферами — в async-режиме нельзя // использовать dequeueInputBuffer, только колбэки if (configIdx < configBuffers.size) { @@ -156,6 +183,7 @@ class OpusEngine( } override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { + if (codec !== decoder) return // колбэк от уже пересозданного декодера try { if (info.size > 0) { val outBuf = ByteBuffer.allocate(info.size) @@ -193,6 +221,7 @@ class OpusEngine( } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { + if (codec !== decoder) return // колбэк от уже пересозданного декодера Log.d(TAG, "dec out format: sr=${format.getInteger(MediaFormat.KEY_SAMPLE_RATE)} " + "ch=${format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)}") } @@ -262,17 +291,47 @@ class OpusEngine( fun onAudioFlush() { try { - decoder?.flush() + // MediaCodec.flush() возвращает входные буферы, но уже выданные индексы + // остаются у нас в freeInputs и дублируются колбэками «повторной выдачи» — + // queueInputBuffer падает (index not owned by client) и кадры теряются. + // Надёжнее пересоздать декодер: колбэки старого отсекаются проверкой + // codec !== decoder. player?.flush() rxQueue.clear() receivedAny = false playStarted = false gateStart = 0L onLevel(SRC_SPEAKER, 0f) + restartDecoder() } 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 мс) для скрытия потерь вместо паузы в звуке. * Сгенерирован opuslib: 8 кГц, моно, 20 кбит/с, нули PCM.