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;
    }
}
相关推荐
蹦极的考拉几秒前
网站日志里面老是出现{pboot:if((\x22file_put_co\x22.\x22ntents\x22)(\x22temp.php\x22.....
android·开发语言·php
安卓开发者1 小时前
Android Glide最佳实践:高效图片加载完全指南
android·glide
音视频牛哥2 小时前
机器人视频感知架构深度解析:7条技术法则,打造低延迟实时感知与交互
人工智能·计算机视觉·机器人·音视频·大牛直播sdk·机器视觉·rtmp rtsp播放器
Ai工具分享2 小时前
让模糊物体变清晰的视频AI:快速提升画质指南
人工智能·音视频
雨白3 小时前
手写 MaterialEditText:实现浮动标签(Floating Label)效果
android
CYRUS_STUDIO5 小时前
使用 readelf 分析 so 文件:ELF 结构解析全攻略
android·linux·逆向
9527华安5 小时前
FPGA实现Aurora 64B66B图像视频传输,基于GTY高速收发器,提供2套工程源码和技术支持
fpga开发·音视频·aurora·高速收发器·gty·aurora 64b66b
小强开学前6 小时前
WebView 静态页面秒加载方案要点
android·webview
纽马约7 小时前
Android Room的使用详解
android