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

Android平台MP4视频全帧提取慢且丢帧问题求助

解决Android中MP4视频逐帧提取慢、丢帧的问题

原代码的核心问题

  • 用固定1秒间隔取帧,完全忽略视频实际帧率,必然丢帧(比如25fps的视频,1秒有25帧,你只取1帧,直接丢24帧)
  • MediaMetadataRetriever.getFrameAtTime依赖视频关键帧,非关键帧位置大概率取不到准确帧,只会返回最近的关键帧,导致重复或丢失
  • 原代码里每次取到帧都切换到主线程更新ImageView,现在你不需要显示,这部分完全是额外开销

最优解决方案:用MediaCodec硬件加速逐帧解码

MediaCodec是Android官方推荐的硬解码API,能直接读取视频的所有原始帧,速度快且无丢帧,适合批量处理帧的场景。

以下是适配raw资源的完整实现:

import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer

private fun extractAllFramesFromRawVideo(rawResId: Int) {
    val extractor = MediaExtractor()
    var codec: MediaCodec? = null
    val bufferInfo = MediaCodec.BufferInfo()

    try {
        // 配置MediaExtractor读取raw资源
        val fd = resources.openRawResourceFd(rawResId)
        extractor.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
        fd.close()

        // 找到视频轨道
        var videoTrackIndex = -1
        for (i in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(i)
            val mimeType = format.getString(MediaFormat.KEY_MIME)
            if (mimeType?.startsWith("video/") == true) {
                videoTrackIndex = i
                break
            }
        }
        if (videoTrackIndex == -1) {
            Log.e("FrameExtraction", "No video track found")
            return
        }

        extractor.selectTrack(videoTrackIndex)
        val videoFormat = extractor.getTrackFormat(videoTrackIndex)
        val mimeType = videoFormat.getString(MediaFormat.KEY_MIME) ?: ""
        val frameRate = videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
        Log.d("FrameExtraction", "Video frame rate: $frameRate fps")

        // 初始化MediaCodec硬解码
        codec = MediaCodec.createDecoderByType(mimeType)
        codec.configure(videoFormat, null, null, 0)
        codec.start()

        val inputBuffers = codec.inputBuffers
        var outputBuffers = codec.outputBuffers
        var isExtractorDone = false
        var frameCount = 0

        while (true) {
            // 喂数据给解码器
            if (!isExtractorDone) {
                val inputBufferIndex = codec.dequeueInputBuffer(10000)
                if (inputBufferIndex >= 0) {
                    val inputBuffer = inputBuffers[inputBufferIndex]
                    val sampleSize = extractor.readSampleData(inputBuffer, 0)
                    if (sampleSize < 0) {
                        // 数据喂完,发送结束信号
                        codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                        isExtractorDone = true
                    } else {
                        val presentationTimeUs = extractor.sampleTime
                        codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0)
                        extractor.advance()
                    }
                }
            }

            // 从解码器获取解码后的帧
            val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 10000)
            when (outputBufferIndex) {
                MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                    // 输出缓冲区更新,重新获取
                    outputBuffers = codec.outputBuffers
                }
                MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    // 输出格式变化,这里可以忽略,我们只需要帧数据
                }
                MediaCodec.INFO_TRY_AGAIN_LATER -> {
                    // 暂时没有输出,继续循环
                }
                else -> {
                    val outputBuffer = outputBuffers[outputBufferIndex]
                    val buffer = ByteBuffer.allocate(bufferInfo.size)
                    outputBuffer.position(bufferInfo.offset)
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
                    buffer.put(outputBuffer)
                    buffer.flip()

                    // 这里处理解码后的帧数据,比如转Bitmap或者直接保存为文件
                    // 示例:保存为PNG文件(你可以替换成自己的处理逻辑)
                    val bitmap = android.graphics.BitmapFactory.decodeStream(buffer.inputStream())
                    bitmap?.let {
                        val outputFile = File(
                            getExternalFilesDir(Environment.DIRECTORY_PICTURES),
                            "frame_${frameCount++}.png"
                        )
                        FileOutputStream(outputFile).use { fos ->
                            it.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, fos)
                        }
                        it.recycle() // 及时回收Bitmap避免内存泄漏
                    }

                    // 释放输出缓冲区
                    codec.releaseOutputBuffer(outputBufferIndex, false)

                    // 检查是否解码完成
                    if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                        break
                    }
                }
            }
        }

        Log.d("FrameExtraction", "Extracted total $frameCount frames")

    } catch (e: Exception) {
        Log.e("FrameExtraction", "Error extracting frames: ${e.message}", e)
    } finally {
        codec?.stop()
        codec?.release()
        extractor.release()
    }
}

// 调用方式
extractAllFramesFromRawVideo(R.raw.video_h)

关键优化点

  • 硬解码加速:MediaCodec利用硬件解码能力,比MediaMetadataRetriever的软提取快数倍
  • 逐帧精准提取:完全按照视频的实际帧率和帧顺序提取,不会丢帧或重复
  • 去掉冗余操作:移除了原代码中主线程更新ImageView的逻辑,减少线程切换开销
  • 资源及时释放:在finally块中确保所有媒体资源被正确释放,避免内存泄漏

额外提示

  • 如果不需要转Bitmap,直接处理ByteBuffer中的原始YUV数据,速度会更快(适合图像处理算法直接输入)
  • 可以根据需要调整帧处理的逻辑,比如批量保存、异步处理等,进一步提升效率

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 10:05:56