Android 使用MediaMuxer+MediaCodec编码MP4视频

以下是一个基于 MediaMuxer + MediaCodec 的 YUV420 转 MP4 工具类实现,支持开始录制、送帧、结束录制接口:

java 复制代码
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;

public class YuvToMp4Encoder {
    private static final String TAG = "YuvToMp4Encoder";
    private static final String MIME_TYPE = "video/avc"; // H.264编码
    private static final int TIMEOUT_USEC = 10000; // 10ms超时

    // 编码器状态
    public enum EncoderState {
        IDLE, PREPARED, ENCODING, STOPPED
    }

    private MediaCodec mMediaCodec;
    private MediaMuxer mMediaMuxer;
    private int mTrackIndex;
    private boolean mMuxerStarted;
    private EncoderState mState = EncoderState.IDLE;
    private int mWidth;
    private int mHeight;
    private int mFrameRate;
    private int mBitRate;
    private long mPresentationTimeUs;
    private long mFirstFrameTime = -1;

    /**
     * 初始化编码器
     * @param width 视频宽度
     * @param height 视频高度
     * @param frameRate 帧率
     * @param bitRate 比特率 (bps)
     */
    public YuvToMp4Encoder(int width, int height, int frameRate, int bitRate) {
        this.mWidth = width;
        this.mHeight = height;
        this.mFrameRate = frameRate;
        this.mBitRate = bitRate;
    }

    /**
     * 准备编码器
     * @param outputPath 输出文件路径
     * @throws IOException 初始化异常
     */
    public void prepare(String outputPath) throws IOException {
        if (mState != EncoderState.IDLE) {
            throw new IllegalStateException("Encoder must be in IDLE state");
        }

        try {
            // 1. 创建并配置MediaCodec
            mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
            format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔(秒)

            mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mMediaCodec.start();

            // 2. 创建MediaMuxer
            mMediaMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            mTrackIndex = -1;
            mMuxerStarted = false;

            mState = EncoderState.PREPARED;
            mPresentationTimeUs = 0;
            Log.i(TAG, "Encoder prepared successfully");
        } catch (Exception e) {
            e.printStackTrace();
            release();
        }
    }

    /**
     * 开始编码
     */
    public void start() {
        if (mState != EncoderState.PREPARED) {
            throw new IllegalStateException("Encoder must be prepared before starting");
        }
        mState = EncoderState.ENCODING;
        mFirstFrameTime = -1;
        Log.i(TAG, "Encoder started");
    }

    /**
     * 输入一帧YUV420数据
     * @param yuvData YUV420字节数组
     * @param timestamp 时间戳(ns)
     */
    public void feedFrame(byte[] yuvData, long timestamp) {
        if (mState != EncoderState.ENCODING) {
            Log.w(TAG, "Ignoring frame - encoder not in encoding state");
            return;
        }

        try {
            // 1. 获取输入缓冲区
            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
            if (inputBufferIndex < 0) {
                Log.w(TAG, "No available input buffer, skipping frame");
                return;
            }

            // 2. 填充YUV数据
            ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
            if (inputBuffer != null) {
                inputBuffer.clear();
                inputBuffer.put(yuvData);

                // 3. 计算PTS (呈现时间戳)
                long presentationTimeUs = timestamp / 1000; // ns -> μs
                if (mFirstFrameTime < 0) {
                    mFirstFrameTime = presentationTimeUs;
                }
                presentationTimeUs -= mFirstFrameTime;

                // 4. 提交到编码器
                mMediaCodec.queueInputBuffer(
                        inputBufferIndex,
                        0,
                        yuvData.length,
                        presentationTimeUs,
                        0
                );
            }

            // 5. 处理编码输出
            drainEncoder(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 停止编码并释放资源
     */
    public void stop() {
        if (mState == EncoderState.STOPPED || mState == EncoderState.IDLE) {
            return;
        }

        try {
            if (mState == EncoderState.ENCODING) {
                // 发送结束信号
                drainEncoder(true);

                // 停止编码器
                if (mMediaCodec != null) {
                    mMediaCodec.stop();
                }
            }

            // 停止混合器
            if (mMediaMuxer != null && mMuxerStarted) {
                mMediaMuxer.stop();
            }

            Log.i(TAG, "Encoder stopped successfully");
        } catch (Exception e) {
           e.printStackTrace();
        } finally {
            release();
            mState = EncoderState.STOPPED;
        }
    }

    // 释放所有资源
    private void release() {
        try {
            if (mMediaCodec != null) {
                mMediaCodec.release();
                mMediaCodec = null;
            }
            if (mMediaMuxer != null) {
                mMediaMuxer.release();
                mMediaMuxer = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        mMuxerStarted = false;
        mTrackIndex = -1;
    }

    // 处理编码器输出
    private void drainEncoder(boolean endOfStream) {
        if (endOfStream) {
            // 发送结束信号
            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
            if (inputBufferIndex >= 0) {
                mMediaCodec.queueInputBuffer(
                        inputBufferIndex, 0, 0, 0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                );
            }
        }

        // 处理所有可用输出
        while (true) {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);

            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                if (!endOfStream) break;
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 格式改变时设置混合器
                if (mMuxerStarted) {
                    throw new RuntimeException("Format changed after muxer started");
                }
                MediaFormat newFormat = mMediaCodec.getOutputFormat();
                mTrackIndex = mMediaMuxer.addTrack(newFormat);
                mMediaMuxer.start();
                mMuxerStarted = true;
            } else if (outputBufferIndex < 0) {
//                Log.w(TAG, "Unexpected result from dequeueOutputBuffer: " + outputBufferIndex);
            } else {
                ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
                if (outputBuffer == null) {
                    Log.e(TAG, "Output buffer was null");
                } else {
                    // 写入混合器
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0 && bufferInfo.size > 0) {
                        if (mMuxerStarted) {
                            outputBuffer.position(bufferInfo.offset);
                            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                            mMediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
                        }
                    }

                    // 检查结束标志
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        break;
                    }
                }
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            }
        }
    }

    /**
     * 获取当前编码器状态
     */
    public EncoderState getState() {
        return mState;
    }
}
相关推荐
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android