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

Android视频转MP4(H264)生成无有效流文件问题排查

问题:视频转码后生成MP4文件有正常大小和时长,但视频流为空

我正在实现从相册选择任意格式/编码的视频,将其转换为H264编码MP4文件的逻辑。生成的文件有正常大小和时长,但视频流为空,ffmpeg输出如下:

Duration: 00:00:02.83, bitrate: 4462 kb/s
Stream #0:0(eng): Video: none, none, 90k tbr, 90k tbn, 90k tbc (default)
Unsupported codec with id 0 for input stream 0

当前Kotlin代码实现:

fun processVideo(uri: Uri, filename: String): Result<File> {
    val newFileName = filename.substringBeforeLast(".") + ".mp4"
    val outputFile = File(context.cacheDir, newFileName)
    Timber.e("Output file is $outputFile")

    val extractor = MediaExtractor()
    var muxer: MediaMuxer? = null
    var decoder: MediaCodec? = null
    var encoder: MediaCodec? = null

    try {
        // Initialize MediaExtractor
        extractor.setDataSource(context, uri, null)

        // Initialize MediaMuxer
        muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

        // Loop through tracks to find video track
        Timber.e("Loop through tracks to find video track")
        val oldTrackIndex = findTrackIndex(extractor, "video/")
        if (oldTrackIndex == -1) {
            Timber.e("No video track found in input video")
        }
        extractor.selectTrack(oldTrackIndex)

        extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)

        val inputVideoFormat = extractor.getTrackFormat(oldTrackIndex)

        encoder = createVideoEncoder(inputVideoFormat)
        decoder = createVideoDecoder(inputVideoFormat)

        decoder.start()
        encoder.start()

        var videoTrackIndex = -1

        val retrieverSrc = MediaMetadataRetriever()
        retrieverSrc.setDataSource(context, uri)
        val degreesString = retrieverSrc.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION
        )
        if (degreesString != null) {
            val degrees = degreesString.toInt()
            if (degrees >= 0) {
                muxer.setOrientationHint(degrees)
            }
        }

        var inputBuffer: ByteBuffer
        var outputBuffer: ByteBuffer

        var inputBufferIndex: Int
        var outputBufferIndex: Int

        val bufferInfo = MediaCodec.BufferInfo()

        val startTime = System.currentTimeMillis()

        var isDecoderEOS = false
        var isEncoderEOS = false

        while (!isEncoderEOS) {
            if (!isDecoderEOS) {
                // Step 1: Extract data from MediaExtractor to MediaCodec (decoder)
                inputBufferIndex = decoder.dequeueInputBuffer(10000)
                if (inputBufferIndex >= 0) {
                    inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
                    val sampleSize = extractor.readSampleData(inputBuffer, 0)
                    if (sampleSize < 0) {
                        // End of stream
                        Timber.e("End of stream reached.")
                        decoder.queueInputBuffer(
                            inputBufferIndex,
                            0,
                            0,
                            0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        )
                        isDecoderEOS = true
                    } else {
                        Timber.e("Queueing input buffer for decoding.")
                        decoder.queueInputBuffer(
                            inputBufferIndex,
                            0,
                            sampleSize,
                            extractor.sampleTime,
                            extractor.sampleFlags
                        )
                        extractor.advance()
                    }
                }
            }

            // Step 2: Decode and re-encode (from decoder to encoder)
            outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000)
            if (outputBufferIndex >= 0) {
                outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
                val outBufferIndex = encoder.dequeueInputBuffer(10000)
                if (outBufferIndex >= 0) {
                    Timber.e("Re-encoding video frame.")
                    val outBuffer = encoder.getInputBuffer(outBufferIndex)!!
                    outBuffer.put(outputBuffer)
                    encoder.queueInputBuffer(
                        outBufferIndex,
                        0,
                        bufferInfo.size,
                        bufferInfo.presentationTimeUs,
                        bufferInfo.flags
                    )

                    // Release the decoder's output buffer after successfully filling encoder's input buffer
                    decoder.releaseOutputBuffer(outputBufferIndex, false)
                }
            }

            // Step 3: Write re-encoded data to MediaMuxer
            val encoderOutputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000)
            if (encoderOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // The output format has changed, add the new format to the MediaMuxer
                if (videoTrackIndex == -1) {
                    videoTrackIndex = muxer.addTrack(encoder.outputFormat)
                    Timber.e("Encoder format: ${encoder.outputFormat}")
                    muxer.start() // Start the muxer here after adding the track
                }
            } else if (encoderOutputBufferIndex >= 0) {
                if (videoTrackIndex != -1) {
                    Timber.e("Writing re-encoded data to MediaMuxer.")
                    outputBuffer = encoder.getOutputBuffer(encoderOutputBufferIndex)!!
                    muxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo)
                    encoder.releaseOutputBuffer(encoderOutputBufferIndex, false)
                }
            }

            // Check for end of stream
            if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                Timber.e("Received end of stream flag.")
                isEncoderEOS = true
            }
        }
        val endTime = System.currentTimeMillis()
        val timeTaken = endTime - startTime
        val timeTakenSeconds = timeTaken / 1000
        val timeTakenRemainingMillis = timeTaken % 1000

        Timber.e("Time taken for main loop: ${timeTakenSeconds}s ${timeTakenRemainingMillis}ms")
        Timber.e("Main processing loop completed")
    } catch (e: Exception) {
        Timber.e("Error during video conversion", e)
        Timber.e(e)
    } finally {
        extractor.safeRelease()
        decoder?.safeStop()
        decoder?.safeRelease()
        encoder?.safeStop()
        encoder?.safeRelease()
        muxer?.safeStop()
        muxer?.safeRelease()
    }

    return Result.success(outputFile)
}

private fun createVideoEncoder(inputFormat: MediaFormat): MediaCodec {
    Timber.e("Create video encoder")
    val codecInfo = selectCodec("video/avc")
    val videoEncoder = MediaCodec.createByCodecName(codecInfo!!.name)

    val width = inputFormat.getInteger(MediaFormat.KEY_WIDTH)
    val height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT)
    val frameRate = inputFormat.getSafeInteger(MediaFormat.KEY_FRAME_RATE, 30)
    val iFrameInterval = inputFormat.getSafeInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
    val colorFormat = inputFormat.getSafeInteger(
        MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
    )
    val bitrateMode = inputFormat.getSafeInteger(
        MediaFormat.KEY_BITRATE_MODE,
        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
    )
    val quality = 0.1f
    val estimatedBitRate = estimateBitRate(width, height, frameRate, quality)

    val outputFormat = MediaFormat.createVideoFormat(
        "video/avc",
        width,
        height
    )

    // Set the codec to H.264
    outputFormat.setString(MediaFormat.KEY_MIME, "video/avc")

    // Copy properties from input format to output format
    outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, estimatedBitRate)
    outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
    outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
    outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
    outputFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, bitrateMode)

    videoEncoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    return videoEncoder
}

private fun createVideoDecoder(inputFormat: MediaFormat): MediaCodec {
    Timber.e("Create video decoder")
    val codecInfo = selectCodec(inputFormat.getString(MediaFormat.KEY_MIME)!!, shouldUseEncode = false)
    val decoder = MediaCodec.createByCodecName(codecInfo!!.name)
    decoder.configure(inputFormat, null, null, 0)
    return decoder
}

问题根源分析

核心问题出在两个关键点:

  1. 缓冲区数据拷贝逻辑错误:从解码器获取的outputBuffer包含解码后的原始帧数据,但直接调用outBuffer.put(outputBuffer)时,未重置缓冲区的position和limit,导致实际写入的数据为空或不完整。
  2. 颜色格式不兼容:编码器直接复用输入格式的颜色格式,部分设备解码器输出的格式与编码器支持的格式不匹配,导致编码器无法处理数据。

修复方案

1. 修正缓冲区数据拷贝逻辑

在将解码器输出写入编码器输入时,需要正确设置缓冲区的读写范围:

// 原代码
outBuffer.put(outputBuffer)

// 修改为
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
outBuffer.put(outputBuffer)
outputBuffer.clear() // 重置缓冲区状态,方便后续复用

2. 确保编码器使用兼容的颜色格式

新增辅助函数查找编码器支持的YUV420格式,替换原代码中直接复用输入颜色格式的逻辑:

private fun createVideoEncoder(inputFormat: MediaFormat): MediaCodec {
    Timber.e("Create video encoder")
    val codecInfo = selectCodec("video/avc")
    val videoEncoder = MediaCodec.createByCodecName(codecInfo!!.name)

    val width = inputFormat.getInteger(MediaFormat.KEY_WIDTH)
    val height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT)
    val frameRate = inputFormat.getSafeInteger(MediaFormat.KEY_FRAME_RATE, 30)
    val iFrameInterval = inputFormat.getSafeInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
    val bitrateMode = inputFormat.getSafeInteger(
        MediaFormat.KEY_BITRATE_MODE,
        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
    )
    val quality = 0.1f
    val estimatedBitRate = estimateBitRate(width, height, frameRate, quality)

    // 获取编码器兼容的YUV420颜色格式
    val colorFormat = findEncoderCompatibleColorFormat(codecInfo)

    val outputFormat = MediaFormat.createVideoFormat(
        "video/avc",
        width,
        height
    )

    outputFormat.setString(MediaFormat.KEY_MIME, "video/avc")
    outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, estimatedBitRate)
    outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
    outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
    outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
    outputFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, bitrateMode)

    videoEncoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    return videoEncoder
}

// 新增:查找编码器支持的YUV420格式
private fun findEncoderCompatibleColorFormat(codecInfo: MediaCodecInfo): Int {
    val capabilities = codecInfo.getCapabilitiesForType("video/avc")
    for (format in capabilities.colorFormats) {
        when (format) {
            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible -> {
                return format
            }
        }
    }
    return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
}

3. 完善EOF判断逻辑

将结束流的判断移至编码器输出处理分支中,避免提前终止循环:

// 原代码:循环末尾判断EOF
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    Timber.e("Received end of stream flag.")
    isEncoderEOS = true
}

// 修改为:在编码器输出处理时判断
else if (encoderOutputBufferIndex >= 0) {
    if (videoTrackIndex != -1) {
        Timber.e("Writing re-encoded data to MediaMuxer.")
        outputBuffer = encoder.getOutputBuffer(encoderOutputBufferIndex)!!
        muxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo)
        encoder.releaseOutputBuffer(encoderOutputBufferIndex, false)
        
        // 在这里判断结束流
        if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            Timber.e("Received end of stream flag.")
            isEncoderEOS = true
        }
    }
}

修复验证(编辑1)

按上述方案修正后,ffmpeg输出正常的视频流信息:

Duration: 00:00:30.03, start: 0.000000, bitrate: 684 kb/s
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, unknown/bt470bg/unknown), 640x360, 682 kb/s, 30 fps, 30 tbr, 90k tbn, 180k tbc (default)
    Metadata:
      creation_time   : 2023-11-03T07:02:10.000000Z
      handler_name    : VideoHandle

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 01:34:50