Android MediaMuxer分块录制AAC音频帧乱序崩溃修复
问题背景
- 需求:录制麦克风音频(后续将适配蓝牙耳机麦克风),转换为
MPEG4 AAC格式。按照项目与后端的通信规范,音频必须切分为0.5~2秒长度的短分片,现有示例仅将分片保存到缓存目录,未实现向后端发送的逻辑。 - 技术选型:使用
AudioRecord采集PCM-16格式音频,通过MediaCodec编码为AAC格式,最终使用MediaMuxer封装保存为MPEG4文件。 - 示例代码基于公开的硬件音频编码示例实现,完整代码如下:
private const val TAG = "RECORDING" private const val AUDIO_CHUNK_LENGTH_MS = 500L private const val RECORDER_SAMPLERATE = 44100 private const val RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_MONO private const val RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT private val BUFFER_SIZE = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING) class RecordingUtil2( val context: Context, private val dispatcher: CoroutineDispatcher = Dispatchers.Default ) { lateinit var audioRecord: AudioRecord lateinit var encoder: MediaCodec lateinit var mediaMuxer: MediaMuxer var trackId: Int = 0 var chunkCuttingJob: Job? = null var recordingJob: Job? = null var audioStartTimeNs: Long = 0 var currentFile: File? = null var chunkEnd = false private fun prepareRecorder() { audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, RECORDER_SAMPLERATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE * 10 ) } private suspend fun startRecording() { Timber.tag(TAG).i("started recording, buffer size $BUFFER_SIZE") createTempFile() prepareRecorder() try { encoder = createMediaCodec(BUFFER_SIZE) encoder.start() createMuxer(encoder.outputFormat, currentFile!!) mediaMuxer.start() } catch (exception: Exception) { Timber.tag(TAG).w(exception) } audioStartTimeNs = System.nanoTime() audioRecord.startRecording() var bufferInfo = MediaCodec.BufferInfo() chunkCuttingJob = CoroutineScope(dispatcher).launch { while (isActive) { delay(AUDIO_CHUNK_LENGTH_MS) cutChunk() } } recordingJob = CoroutineScope(dispatcher).launch { val buffer2 = ByteArray(BUFFER_SIZE) do { val bytes = audioRecord.read(buffer2, 0, BUFFER_SIZE) if (bytes != BUFFER_SIZE) { Timber.tag(TAG).w("read less bytes than full buffer ($bytes/$BUFFER_SIZE)") } encodeRawAudio(encoder, mediaMuxer, buffer2, bytes, bufferInfo, !isActive || chunkEnd) if (chunkEnd) { recreateEncoderAndMuxer() bufferInfo = MediaCodec.BufferInfo() } } while(isActive) } } private fun recreateEncoderAndMuxer() { createTempFile() chunkEnd = false audioStartTimeNs = System.nanoTime() encoder.stop() encoder.release() encoder = createMediaCodec(BUFFER_SIZE) encoder.start() mediaMuxer.stop() mediaMuxer.release() createMuxer(encoder.outputFormat, currentFile!!) mediaMuxer.start() } private fun encodeRawAudio(encoder: MediaCodec, muxer: MediaMuxer, bytes: ByteArray, byteCount: Int, bufferInfo: MediaCodec.BufferInfo, last: Boolean = false) { with(encoder) { val infputBufferIndex = dequeueInputBuffer(10_000) val inputBuffer = getInputBuffer(infputBufferIndex) inputBuffer?.clear() inputBuffer?.put(bytes) val presentationTimeUs: Long = (System.nanoTime() - audioStartTimeNs) / 1000 queueInputBuffer(infputBufferIndex, 0, byteCount, presentationTimeUs, if (last) BUFFER_FLAG_END_OF_STREAM else 0) var outputBufferIndex = dequeueOutputBuffer(bufferInfo, 0) Timber.tag(TAG).d("encoding $byteCount bytes, last = $last, time: $presentationTimeUs, buffer time: ${bufferInfo.presentationTimeUs}") while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { if (outputBufferIndex >= 0) { val outputBuffer = getOutputBuffer(outputBufferIndex) outputBuffer?.position(bufferInfo.offset) outputBuffer?.limit(bufferInfo.offset + bufferInfo.size) if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { val data = ByteArray(outputBuffer!!.remaining()) outputBuffer.get(data) muxer.writeSampleData(trackId, outputBuffer, bufferInfo) } outputBuffer?.clear() releaseOutputBuffer(outputBufferIndex, false) } outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0) } } } private fun cutChunk() { Timber.tag(TAG).i("cutting chunk") chunkEnd = true } private fun stopRecording() { Timber.tag(TAG).i("stopped recording") chunkCuttingJob?.cancel() chunkCuttingJob = null recordingJob?.cancel() recordingJob = null audioRecord.stop() encoder.release() mediaMuxer.stop() mediaMuxer.release() } suspend fun record(isRecording: Boolean) { if (isRecording) { startRecording() } else { stopRecording() } } private fun createMediaCodec(bufferSize: Int, existing: MediaCodec? = null): MediaCodec { val mediaFormat = MediaFormat().apply { setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC) setInteger(MediaFormat.KEY_BIT_RATE, 32000) setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1) setInteger(MediaFormat.KEY_SAMPLE_RATE, RECORDER_SAMPLERATE) setInteger(MediaFormat.KEY_AAC_PROFILE, CodecProfileLevel.AACObjectLC) setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize) } val encoderString = MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(mediaFormat) Timber.tag(TAG).d("chosen codec: $encoderString") val mediaCodec = existing ?: MediaCodec.createByCodecName(encoderString) try { mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) } catch (e: Exception) { Timber.tag(TAG).w(e) mediaCodec.release() } return mediaCodec } private fun createMuxer(format: MediaFormat, file: File) { try { file.createNewFile() mediaMuxer = MediaMuxer(file.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) trackId = mediaMuxer.addTrack(format) } catch (e: java.lang.Exception) { Timber.tag(TAG).e(e) } } private var currentIndex: Int = 0 private fun createTempFile() { currentFile = File(context.cacheDir, "$currentIndex.m4a").also { it.createNewFile() } currentIndex++ } }
调用方式
代码在协程中运行,调用示例如下:
class MyViewModel : ViewModel() { fun startRecording() { val recordingUtil = RecordingUtil2(...) viewModelScope.launch(Dispatchers.Default) { recordingUtil.record(true) } } }
故障现象
连续保存若干音频分片到对应文件后,MediaMuxer因MPEG4Writer抛出异常崩溃,核心报错信息:
E/MPEG4Writer: do not support out of order frames (timestamp: 13220 < last: 23219 for Audio track
该问题存在两个反常点:
- 传入
MediaCodec.queueInputBuffer(...)的时间戳为严格递增、按顺序传入,不存在乱序情况。 - 报错信息中的last时间戳固定为23219,根据Android原生MPEG4Writer的实现逻辑,此处应该展示上一帧的动态时间戳,几乎不可能出现远大于0的固定值。
日志对比
崩溃场景上下文日志
I/MPEG4Writer: Normal stop process D/MPEG4Writer: Audio track stopping. Stop source I/MPEG4Writer: Received total/0-length (22/1) buffers and encoded 22 frames. - Audio D/MPEG4Writer: Audio track source stopping D/MPEG4Writer: Audio track source stopped I/MPEG4Writer: Audio track drift time: 0 us D/MPEG4Writer: Audio track stopped. Stop source D/MPEG4Writer: Stopping writer thread D/MPEG4Writer: 0 chunks are written in the last batch D/MPEG4Writer: Writer thread stopped I/MPEG4Writer: Ajust the moov start time from 44099 us -> 44099 us I/MPEG4Writer: The mp4 file will not be streamable. D/MPEG4Writer: Audio track stopping. Stop source D/RECORDING: encoding 3528 bytes, last = false, time: 79102, buffer time: 0 D/RECORDING: encoding 3528 bytes, last = false, time: 85883, buffer time: 0 D/RECORDING: encoding 3528 bytes, last = false, time: 89383, buffer time: 79102 I/MPEG4Writer: setStartTimestampUs: 79102 from Audio track I/MPEG4Writer: Earliest track starting time: 79102 E/MPEG4Writer: do not support out of order frames (timestamp: 13220 < last: 23219 for Audio track E/MPEG4Writer: 0 frames to dump timeStamps in Audio track I/MPEG4Writer: Received total/0-length (3/0) buffers and encoded 2 frames. - Audio I/MPEG4Writer: Audio track drift time: 0 us E/MediaAdapter: pushBuffer called before start E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1 E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1 Process: com.example, PID: 23499 java.lang.IllegalStateException: writeSampleData returned an error
正常分片保存日志
I/MPEG4Writer: Normal stop process D/MPEG4Writer: Audio track stopping. Stop source D/MPEG4Writer: Audio track source stopping I/MPEG4Writer: Received total/0-length (18/0) buffers and encoded 18 frames. - Audio D/MPEG4Writer: Audio track source stopped I/MPEG4Writer: Audio track drift time: 0 us D/MPEG4Writer: Audio track stopped. Stop source D/MPEG4Writer: Stopping writer thread D/MPEG4Writer: 0 chunks are written in the last batch D/MPEG4Writer: Writer thread stopped I/MPEG4Writer: Ajust the moov start time from 45890 us -> 45890 us I/MPEG4Writer: The mp4 file will not be streamable. D/MPEG4Writer: Audio track stopping. Stop source D/RECORDING: encoding 3528 bytes, last = false, time: 44099, buffer time: 0 D/RECORDING: encoding 3528 bytes, last = false, time: 74366, buffer time: 44099 I/MPEG4Writer: setStartTimestampUs: 44099 from Audio track I/MPEG4Writer: Earliest track starting time: 44099 D/RECORDING: encoding 3528 bytes, last = false, time: 116122, buffer time: 80805 D/RECORDING: encoding 3528 bytes, last = false, time: 156789, buffer time: 104025 D/RECORDING: encoding 3528 bytes, last = false, time: 196940, buffer time: 152221 D/RECORDING: encoding 3528 bytes, last = false, time: 235010, buffer time: 176108 D/RECORDING: encoding 3528 bytes, last = false, time: 275232, buffer time: 243989 D/RECORDING: encoding 3528 bytes, last = false, time: 316400, buffer time: 267209 D/RECORDING: encoding 3528 bytes, last = false, time: 361290, buffer time: 313871 D/RECORDING: encoding 3528 bytes, last = false, time: 401305, buffer time: 338259 D/RECORDING: encoding 3528 bytes, last = false, time: 441019, buffer time: 412824 D/RECORDING: encoding 3528 bytes, last = false, time: 481193, buffer time: 436044 I/RECORDING: cutting chunk D/RECORDING: encoding 3528 bytes, last = true, time: 518624, buffer time: 458978 I/MediaCodec: Codec shutdown complete
临时规避现象
崩溃场景日志初始会出现2个时间戳为0的BufferInfo帧,正常场景仅出现1个,但实际观测到仅出现1个0时间戳帧时仍会偶现崩溃,因此该现象并非根本原因。在分片切换逻辑中添加100ms延时可以在大部分场景下规避崩溃,但属于临时方案,需要找到根本修复方法。
内容的提问来源于stack exchange,提问作者Piotr Śmietana
相关产品推荐
相关产品推荐

