You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MediaCodec.dequeueOutputBuffer在Pixel返回INFO_TRY_AGAIN_LATER问题排查

问题:Pixel 6 Pro上Bitmap转视频时编码器无限循环卡住

我使用bitmap2video库将Bitmap转换为视频,并通过FrameBuilder.kt编码为AVC/H264格式。但在流结束时,Google Pixel 6 Pro上的dequeueOutputBuffer持续返回-1/INFO_TRY_AGAIN_LATER,导致无限循环;而三星S21/S22设备运行完全正常。

设备日志

Pixel 6 Pro日志

15:00:30.299  I  event=androidx.camera.video.VideoRecordEvent$Status@3b2c59b
15:00:30.327  I  event=androidx.camera.video.VideoRecordEvent$Status@ee2077
15:00:30.588  I  event=androidx.camera.video.VideoRecordEvent$Finalize@d1a4d13
// 获取录制视频的URI并解析到MediaRetriever,提取Bitmap帧用于编码
15:00:30.589  I  uri=content://media/external/video/media/1000000258
15:00:35.046  D  drainCodec(false, encoderStatus=-1
15:00:35.058  D  drainCodec(false), encoderStatus=-1
12:35:03.435  D  drainCodec(false), encoderStatus=-1
12:35:03.457  D  drainCodec(false), encoderStatus=-1
12:35:03.480  D  drainCodec(false), encoderStatus=-1
12:35:03.493  D  drainCodec(false), encoderStatus=-1
12:35:03.506  D  drainCodec(false), encoderStatus=-1
12:35:03.516  D  releasing encoder objects
12:35:03.516  D  drainCodec(true), encoderStatus=-1
12:35:03.516  D  sending EOS to encoder
12:35:03.527  D  no output available, spinning to await EOS
12:35:03.537  D  no output available, spinning to await EOS
12:35:03.549  D  no output available, spinning to await EOS
12:35:03.560  D  no output available, spinning to await EOS
12:35:03.571  D  no output available, spinning to await EOS
12:35:03.583  D  no output available, spinning to await EOS
~ 无限循环持续

三星S21日志

12:49:05.493  I  qqq uri=content://media/external/video/media/1000026049
12:49:14.362  D  drainCodec(false)
12:49:14.396  D  drainCodec(false)
12:49:14.442  D  drainCodec(false)
12:49:14.492  D  drainCodec(false)
12:49:14.494  D  encoder output format changed: {max-bitrate=1500000, crop-right=2159, level=32768, mime=video/avc, profile=8, bitrate=1500000, priority=0, intra-refresh-period=0, color-standard=1, csd-1=java.nio.HeapByteBuffer[pos=0 lim=8 cap=8], color-transfer=3, crop-bottom=3839, prepend-sps-pps-to-idr-frames=0, video-qp-average=0, crop-left=0, width=2160, bitrate-mode=1, color-range=2, crop-top=0, frame-rate=30, height=3840, csd-0=java.nio.HeapByteBuffer[pos=0 lim=22 cap=22]}
12:49:14.498  D  {max-bitrate=1500000, crop-right=2159, level=32768, mime=video/avc, profile=8, bitrate=1500000, priority=0, intra-refresh-period=0, color-standard=1, csd-1=java.nio.HeapByteBuffer[pos=0 lim=8 cap=8], color-transfer=3, crop-bottom=3839, prepend-sps-pps-to-idr-frames=0, video-qp-average=0, crop-left=0, width=2160, bitrate-mode=1, color-range=2, crop-top=0, frame-rate=30, height=3840, csd-0=java.nio.HeapByteBuffer[pos=0 lim=22 cap=22]}
12:49:14.507  D  ignoring BUFFER_FLAG_CODEC_CONFIG
12:49:14.509  D  sent 6605 bytes to muxer
12:49:14.510  D  sent 1255 bytes to muxer
12:49:14.517  D  sent 2901 bytes to muxer
12:49:14.548  D  drainCodec(false)
12:49:14.578  D  drainCodec(false)
12:49:14.579  D  sent 7208 bytes to muxer
12:49:14.610  D  drainCodec(false)
12:49:14.611  D  sent 10116 bytes to muxer
12:49:14.646  D  drainCodec(false)
12:49:14.648  D  sent 18212 bytes to muxer
12:49:14.678  D  drainCodec(false)
12:49:14.679  D  sent 14077 bytes to muxer
12:49:14.711  D  drainCodec(false)
12:49:14.712  D  sent 13230 bytes to muxer
12:49:14.745  D  drainCodec(false)
12:49:14.746  D  sent 13348 bytes to muxer
12:49:14.778  D  drainCodec(false)
12:49:14.778  D  sent 14504 bytes to muxer
12:49:14.796  D  drainCodec(false)
12:49:14.804  D  sent 14219 bytes to muxer
12:49:14.814  D  releasing encoder objects
12:49:14.814  D  drainCodec(true)
12:49:14.814  D  sending EOS to encoder
12:49:14.825  D  no output available, spinning to await EOS
12:49:14.828  D  sent 12867 bytes to muxer
12:49:14.838  D  no output available, spinning to await EOS
12:49:14.843  D  end of stream reached

相关代码(drainCodec函数)

private fun drainCodec(endOfStream: Boolean) {
    if (VERBOSE) Log.d(TAG, "drainCodec($endOfStream)")
    if (endOfStream) {
        if (VERBOSE) Log.d(TAG, "sending EOS to encoder")
        mediaCodec.signalEndOfInputStream()
    }
    while (true) {
        val encoderStatus: Int = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC.toLong())
        if (VERBOSE) Log.d(TAG, "encoderStatus=$encoderStatus")

        if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
            // no output available yet, stuck here on Google Pixel
            if (!endOfStream) {
                break // out of while
            } else {
                if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS")
            }
        } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // should happen before receiving buffers, and should only happen once
            if (frameMuxer.isStarted())  throw RuntimeException("format changed twice")
            val newFormat: MediaFormat = mediaCodec.outputFormat
            Log.d(TAG, "encoder output format changed: $newFormat")

            // now that we have the Magic Goodies, start the muxer
            frameMuxer.start(newFormat, audioExtractor)
        } else {
            val encodedData = mediaCodec.getOutputBuffer(encoderStatus) ?: throw RuntimeException("encoderOutputBuffer  $encoderStatus was null")
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                // The codec config data was pulled out and fed to the muxer when we got
                // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG")
                bufferInfo.size = 0
            } else {
                if (VERBOSE) Log.d(TAG, "Flags not configured")
            }
            if (bufferInfo.size != 0) {
                if (!frameMuxer.isStarted()) throw RuntimeException("muxer hasn't started")
                frameMuxer.muxVideoFrame(encodedData, bufferInfo)
                if (VERBOSE) Log.d(TAG, "sent " + bufferInfo.size + " bytes to muxer")
            } else {
                if (VERBOSE) Log.d(TAG, "No bytes sent")

            }
            mediaCodec.releaseOutputBuffer(encoderStatus, false)
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                if (!endOfStream) {
                    Log.w(TAG, "reached end of stream unexpectedly")
                } else {
                    if (VERBOSE) Log.d(TAG, "end of stream reached")
                }
                break // out of while
            }
        }
    }
}

问题原因

  1. 硬件编码器差异:Pixel 6 Pro的AVC编码器对EOS(流结束)信号的处理逻辑与三星设备不同,可能编码器内部未正确生成结束帧,导致持续无输出返回。
  2. 无超时退出机制:当前代码在发送EOS后无限循环等待,没有设置退出阈值,一旦编码器无法响应结束信号就会卡死。
  3. 帧处理不完整:从日志看,Pixel在发送EOS前的drainCodec(false)阶段已多次返回INFO_TRY_AGAIN_LATER,说明编码器已无输出,但后续发送EOS后,编码器无法生成结束信号。

解决方案

1. 给EOS等待添加超时机制

修改drainCodec函数,限制等待EOS的次数,避免无限循环:

private fun drainCodec(endOfStream: Boolean) {
    if (VERBOSE) Log.d(TAG, "drainCodec($endOfStream)")
    if (endOfStream) {
        if (VERBOSE) Log.d(TAG, "sending EOS to encoder")
        mediaCodec.signalEndOfInputStream()
    }
    var waitCount = 0
    val maxWaitAttempts = 50 // 最多等待50次,对应约500ms
    while (true) {
        val encoderStatus: Int = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC.toLong())
        if (VERBOSE) Log.d(TAG, "encoderStatus=$encoderStatus")

        if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
            if (!endOfStream) {
                break
            } else {
                if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS")
                waitCount++
                // 超过等待次数强制退出
                if (waitCount >= maxWaitAttempts) {
                    Log.w(TAG, "Timeout waiting for EOS, exiting loop")
                    break
                }
                // 短暂休眠,降低CPU占用并给编码器处理时间
                Thread.sleep(10)
            }
        } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            if (frameMuxer.isStarted()) throw RuntimeException("format changed twice")
            val newFormat: MediaFormat = mediaCodec.outputFormat
            Log.d(TAG, "encoder output format changed: $newFormat")
            frameMuxer.start(newFormat, audioExtractor)
        } else {
            val encodedData = mediaCodec.getOutputBuffer(encoderStatus) ?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG")
                bufferInfo.size = 0
            }
            if (bufferInfo.size != 0) {
                if (!frameMuxer.isStarted()) throw RuntimeException("muxer hasn't started")
                frameMuxer.muxVideoFrame(encodedData, bufferInfo)
                if (VERBOSE) Log.d(TAG, "sent " + bufferInfo.size + " bytes to muxer")
            }
            mediaCodec.releaseOutputBuffer(encoderStatus, false)
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                if (VERBOSE) Log.d(TAG, "end of stream reached")
                break
            }
        }
    }
}

2. 确保所有输入帧处理完成

在调用drainCodec(true)发送EOS前,先排空编码器的所有输出,确保已提交的帧都处理完毕:

// 先排空所有已处理的输出
while (true) {
    val status = mediaCodec.dequeueOutputBuffer(bufferInfo, 0)
    if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
        break
    } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 处理格式变更(如果需要)
    } else {
        mediaCodec.releaseOutputBuffer(status, false)
    }
}
// 再发送EOS并处理结束逻辑
drainCodec(true)

3. 明确编码器配置参数

初始化编码器时,设置明确的关键参数,避免自动适配的兼容性问题:

val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height)
format.setInteger(MediaFormat.KEY_BIT_RATE, 1500000)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)

内容的提问来源于stack exchange,提问作者MikkelT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:57:01