以下是一个基于 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;
}
}