Android 使用MediaMuxer+MediaCodec编码MP4视频异步方案
Android 使用MediaMuxer+MediaCodec编码MP4视频同步方案
异步编码实现方案
-
核心改进
- 异步架构设计:
- 使用两个独立的线程池:mEncoderExecutor 处理帧输入,mDrainExecutor 处理编码输出
- 使用 LinkedBlockingQueue<byte[]> 作为帧数据队列,实现生产者-消费者模式
- 使用 AtomicBoolean 控制异步任务的启停
- 回调机制:
- 新增 AsyncEncoderCallback 接口,提供 onFrameEncoded()、onEncodingError()、onEncodingCompleted() 回调
- 支持异步通知编码状态和错误处理
- 非阻塞操作:
- feedFrame() 方法现在只是将帧数据加入队列,立即返回,不阻塞主线程
- 编码和输出处理都在后台线程中异步执行
- 异步架构设计:
-
性能优势
- 主线程不阻塞:feedFrame() 调用立即返回,不会因为编码处理而卡顿
- 并发处理:帧输入和编码输出可以并行进行,提高整体吞吐量
- 内存管理:队列机制可以平滑处理帧率波动,避免内存溢出
完整方案:
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;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}// 异步编码回调接口public interface AsyncEncoderCallback {void onFrameEncoded();void onEncodingError(Exception error);void onEncodingCompleted();}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;private int duration = 0;private int conunt;// 异步编码相关private ExecutorService mEncoderExecutor;private ExecutorService mDrainExecutor;private LinkedBlockingQueue<byte[]> mFrameQueue;private AtomicBoolean mIsEncoding = new AtomicBoolean(false);private AsyncEncoderCallback mCallback;/*** 初始化编码器* @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;duration = (int) (1000*1000 * 1.0f / frameRate);// 初始化异步编码组件mEncoderExecutor = Executors.newSingleThreadExecutor();mDrainExecutor = Executors.newSingleThreadExecutor();mFrameQueue = new LinkedBlockingQueue<>();mIsEncoding.set(false);}/*** 准备编码器* @param outputPath 输出文件路径* @throws IOException 初始化异常*/public void prepare(String outputPath) throws IOException {if (mState != EncoderState.IDLE && mState != EncoderState.STOPPED) {throw new IllegalStateException("Encoder must be in IDLE state");}try {// 1. 创建并配置MediaCodecmMediaCodec = 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. 创建MediaMuxermMediaMuxer = 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();}}long startTime = 0;/*** 开始编码*/public void start() {start(null);}/*** 开始异步编码* @param callback 编码回调*/public void start(AsyncEncoderCallback callback) {if (mState != EncoderState.PREPARED) {throw new IllegalStateException("Encoder must be prepared before starting");}mState = EncoderState.ENCODING;mFirstFrameTime = -1;startTime = System.nanoTime();conunt = 0;mCallback = callback;mIsEncoding.set(true);// 启动异步编码任务startAsyncEncoding();Log.i(TAG, "Async encoder started");}/*** 异步输入一帧YUV420数据* @param yuvData YUV420字节数组*/public void feedFrame(byte[] yuvData) {if (mState != EncoderState.ENCODING || !mIsEncoding.get()) {Log.w(TAG, "Ignoring frame - encoder not in encoding state");return;}// 将帧数据加入队列,由后台线程处理try {mFrameQueue.offer(yuvData);} catch (Exception e) {Log.e(TAG, "Failed to queue frame", e);if (mCallback != null) {mCallback.onEncodingError(e);}}}/*** 启动异步编码任务*/private void startAsyncEncoding() {// 启动帧处理任务mEncoderExecutor.execute(new Runnable() {@Overridepublic void run() {processFrames();}});// 启动输出处理任务mDrainExecutor.execute(new Runnable() {@Overridepublic void run() {processOutput();}});}/*** 处理帧队列中的帧数据*/private void processFrames() {while (mIsEncoding.get() || !mFrameQueue.isEmpty()) {try {byte[] yuvData = mFrameQueue.poll();if (yuvData == null) {Thread.sleep(1); // 短暂休眠避免CPU占用过高continue;}conunt++;encodeFrame(yuvData);} catch (Exception e) {Log.e(TAG, "Error processing frame", e);if (mCallback != null) {mCallback.onEncodingError(e);}}}}/*** 编码单帧数据*/private void encodeFrame(byte[] yuvData) {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 = conunt * duration;// 4. 提交到编码器mMediaCodec.queueInputBuffer(inputBufferIndex,0,yuvData.length,presentationTimeUs,0);}} catch (Exception e) {Log.e(TAG, "Error encoding frame", e);if (mCallback != null) {mCallback.onEncodingError(e);}}}/*** 处理编码器输出*/private void processOutput() {while (mIsEncoding.get() || mState == EncoderState.ENCODING) {try {drainEncoder(false);Thread.sleep(1); // 短暂休眠避免CPU占用过高} catch (Exception e) {Log.e(TAG, "Error processing output", e);if (mCallback != null) {mCallback.onEncodingError(e);}}}}/*** 停止编码并释放资源*/public void stop() {if (mState == EncoderState.STOPPED || mState == EncoderState.IDLE) {return;}try {// 停止异步编码mIsEncoding.set(false);if (mState == EncoderState.ENCODING) {// 发送结束信号drainEncoder(true);// 停止编码器if (mMediaCodec != null) {mMediaCodec.stop();}}// 停止混合器if (mMediaMuxer != null && mMuxerStarted) {mMediaMuxer.stop();}Log.i(TAG, "Async encoder stopped successfully");// 通知回调if (mCallback != null) {mCallback.onEncodingCompleted();}} catch (Exception e) {e.printStackTrace();if (mCallback != null) {mCallback.onEncodingError(e);}} finally {release();mState = EncoderState.STOPPED;}}// 释放所有资源private void release() {try {// 停止异步任务mIsEncoding.set(false);// 关闭线程池if (mEncoderExecutor != null) {mEncoderExecutor.shutdown();mEncoderExecutor = null;}if (mDrainExecutor != null) {mDrainExecutor.shutdown();mDrainExecutor = null;}// 清空队列if (mFrameQueue != null) {mFrameQueue.clear();}// 释放编码器资源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);// 通知帧编码完成if (mCallback != null && !endOfStream) {mCallback.onFrameEncoded();}}}}/*** 获取当前编码器状态*/public EncoderState getState() {return mState;}
}