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 }
问题根源分析
核心问题出在两个关键点:
- 缓冲区数据拷贝逻辑错误:从解码器获取的
outputBuffer包含解码后的原始帧数据,但直接调用outBuffer.put(outputBuffer)时,未重置缓冲区的position和limit,导致实际写入的数据为空或不完整。 - 颜色格式不兼容:编码器直接复用输入格式的颜色格式,部分设备解码器输出的格式与编码器支持的格式不匹配,导致编码器无法处理数据。
修复方案
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
相关产品推荐
相关产品推荐

