From 54b196108d330d220c7e42bce594acdf65c415b8 Mon Sep 17 00:00:00 2001 From: Roman Herbstmann Date: Sun, 20 Sep 2026 09:26:17 +0200 Subject: [PATCH] audio: do not forward the codec config buffer as an encoded frame MediaCodec emits the audio codec config (for AAC the AudioSpecificConfig) as its first output buffer, flagged with BUFFER_FLAG_CODEC_CONFIG. AudioEncoder.checkBuffer only validated the timestamp, so that buffer was forwarded like a regular frame: senders transmitted it as a tiny bogus AAC frame and AndroidMuxerRecordController wrote it into the recording as a sample. The resulting MP4 files start with a 2-byte audio sample that is identical to the track extradata. FFmpeg and VLC skip it, but Chrome aborts playback of the whole file with PIPELINE_ERROR_DECODE. Dropping the buffer is safe because the configuration is derived independently on every path: RTMP builds the AudioSpecificConfig in AacPacket.sendAudioInfo from sample rate and channel count, RTSP signals it through the SDP body, and the muxer takes it from the MediaFormat passed to setAudioFormat, which already carries csd-0. This mirrors how VideoEncoder keeps SPS/PPS out of the frame path. --- .../com/pedro/encoder/audio/AudioEncoder.java | 5 ++ .../encoder/AudioEncoderCheckBufferTest.kt | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 encoder/src/test/java/com/pedro/encoder/AudioEncoderCheckBufferTest.kt diff --git a/encoder/src/main/java/com/pedro/encoder/audio/AudioEncoder.java b/encoder/src/main/java/com/pedro/encoder/audio/AudioEncoder.java index 58eff9db3..78d2fb3f5 100644 --- a/encoder/src/main/java/com/pedro/encoder/audio/AudioEncoder.java +++ b/encoder/src/main/java/com/pedro/encoder/audio/AudioEncoder.java @@ -167,6 +167,11 @@ protected long calculatePts(Frame frame, long presentTimeUs) { @Override protected boolean checkBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) { + // Codec config (e.g. AAC AudioSpecificConfig) is signalled per protocol and via + // MediaFormat csd-0 for the muxer; do not forward it as an encoded audio frame. + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + return false; + } return checkValidTimeStamp(bufferInfo); } diff --git a/encoder/src/test/java/com/pedro/encoder/AudioEncoderCheckBufferTest.kt b/encoder/src/test/java/com/pedro/encoder/AudioEncoderCheckBufferTest.kt new file mode 100644 index 000000000..6ce35c9d2 --- /dev/null +++ b/encoder/src/test/java/com/pedro/encoder/AudioEncoderCheckBufferTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 pedroSG94. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pedro.encoder + +import android.media.MediaCodec +import android.media.MediaFormat +import com.pedro.encoder.audio.AudioEncoder +import com.pedro.encoder.audio.GetAudioData +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.ByteBuffer + +class AudioEncoderCheckBufferTest { + + private val encoder = object : AudioEncoder(NO_OP_AUDIO_DATA) { + fun acceptsBuffer(bufferInfo: MediaCodec.BufferInfo): Boolean { + return checkBuffer(ByteBuffer.allocate(bufferInfo.size), bufferInfo) + } + } + + @Test + fun `WHEN buffer has codec config flag THEN checkBuffer rejects it`() { + val bufferInfo = MediaCodec.BufferInfo().apply { + offset = 0 + size = 2 + presentationTimeUs = 0 + flags = MediaCodec.BUFFER_FLAG_CODEC_CONFIG + } + assertFalse(encoder.acceptsBuffer(bufferInfo)) + } + + @Test + fun `WHEN buffer is a normal encoded frame THEN checkBuffer accepts valid timestamp`() { + val bufferInfo = MediaCodec.BufferInfo().apply { + offset = 0 + size = 256 + presentationTimeUs = 1_000 + flags = 0 + } + assertTrue(encoder.acceptsBuffer(bufferInfo)) + } + + private companion object { + private val NO_OP_AUDIO_DATA = object : GetAudioData { + override fun getAudioData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {} + override fun onAudioFormat(mediaFormat: MediaFormat) {} + } + } +}