一、簡介
使用 MediaCodec 對 yuv 資料進行編碼,編碼的格式為 H.264(AVC) 。
使用 MediaMuxer 将視訊track和音頻track混合到 mp4 容器中,通常視訊編碼使用H.264(AVC)編碼,音頻編碼使用AAC編碼。
二、流程分析
(簡要介紹一下流程,具體api的參數說明起來篇幅太大,不清楚的可以自己搜尋一下)
- 建立編碼器并配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight);
// 設定編碼的顔色格式,實則為nv12(不同手機可能會不一樣)
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
// 設定視訊的比特率,比特率太小會影響編碼的視訊品質
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 6);
// 設定視訊的幀率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// 設定I幀(關鍵幀)的間隔時間,機關秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
// 建立編碼器、配置和啟動
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
encoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
關于比特率可以參考:
- 編碼一幀資料
private void encode(byte[] yuv, long presentationTimeUs) {
// 一、給編碼器設定一幀輸入資料
// 1.擷取一個可用的輸入buffer,最大等待時長為DEFAULT_TIMEOUT_US
int inputBufferIndex = mEncoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
// 2.将輸入資料放到buffer中
inputBuffer.put(yuv);
// 3.将buffer壓入解碼隊列中,即編碼線程就會處理隊列中的資料了
mEncoder.queueInputBuffer(inputBufferIndex, 0, yuv.length, presentationTimeUs, 0);
// 二、從編碼器中取出一幀編碼後的輸出資料
// 1.擷取一個可用的輸出buffer,最大等待時長為DEFAULT_TIMEOUT_US
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
// 2.TODO MediaMuxer将編碼資料寫入到mp4中
// 3.用完後釋放這個輸出buffer
mEncoder.releaseOutputBuffer(outputBufferIndex, false);
}
-
MediaMuxer寫入編碼資料
在寫入前,需要配置一些視訊的頭部資訊(csd參數),否則會報錯。csd參數全稱Codec-specific Data。對于H.264來說,"csd-0"和"csd-1"分别對應sps和pps;對于AAC來說,"csd-0"對應ADTS。
// 寫入頭部資訊,并啟動 MediaMuxer
private int writeHeadInfo(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo) {
byte[] csd = new byte[bufferInfo.size];
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
outputBuffer.position(bufferInfo.offset);
outputBuffer.get(csd);
ByteBuffer sps = null;
ByteBuffer pps = null;
for (int i = bufferInfo.size - 1; i > 3; i--) {
if (csd[i] == 1 && csd[i - 1] == 0 && csd[i - 2] == 0 && csd[i - 3] == 0) {
sps = ByteBuffer.allocate(i - 3);
pps = ByteBuffer.allocate(bufferInfo.size - (i - 3));
sps.put(csd, 0, i - 3).position(0);
pps.put(csd, i - 3, bufferInfo.size - (i - 3)).position(0);
}
}
MediaFormat outputFormat = mEncoder.getOutputFormat();
if (sps != null && pps != null) {
outputFormat.setByteBuffer("csd-0", sps);
outputFormat.setByteBuffer("csd-1", pps);
}
int videoTrackIndex = mMediaMuxer.addTrack(outputFormat);
Log.d(TAG, "videoTrackIndex: " + videoTrackIndex);
mMediaMuxer.start();
return videoTrackIndex;
}
// 寫入一幀編碼後的資料
mMediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
- 結束後釋放相應對象
mEncoder.stop();
mEncoder.release();
mMediaMuxer.release();
三、完整代碼
包含一些基本的傳回值檢查、接口回調、以及可以中途停止解碼的方法等。
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 VideoEncoder {
private static final String TAG = "VideoEncoder";
private final static String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
private static final long DEFAULT_TIMEOUT_US = 10000;
private MediaCodec mEncoder;
private MediaMuxer mMediaMuxer;
private int mVideoTrackIndex;
private boolean mStop = false;
public void init(String outPath, int width, int height) {
try {
mStop = false;
mVideoTrackIndex = -1;
mMediaMuxer = new MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
// 編碼器輸入是NV12格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 6);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mEncoder.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void release() {
mStop = true;
if (mEncoder != null) {
mEncoder.stop();
mEncoder.release();
mEncoder = null;
}
if (mMediaMuxer != null) {
mMediaMuxer.release();
mMediaMuxer = null;
}
}
public void encode(byte[] yuv, long presentationTimeUs) {
if (mEncoder == null || mMediaMuxer == null) {
Log.e(TAG, "mEncoder or mMediaMuxer is null");
return;
}
if (yuv == null) {
Log.e(TAG, "input yuv data is null");
return;
}
int inputBufferIndex = mEncoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
Log.d(TAG, "inputBufferIndex: " + inputBufferIndex);
if (inputBufferIndex == -1) {
Log.e(TAG, "no valid buffer available");
return;
}
ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
inputBuffer.put(yuv);
mEncoder.queueInputBuffer(inputBufferIndex, 0, yuv.length, presentationTimeUs, 0);
while (!mStop) {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
Log.d(TAG, "outputBufferIndex: " + outputBufferIndex);
if (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
// write head info
if (mVideoTrackIndex == -1) {
Log.d(TAG, "this is first frame, call writeHeadInfo first");
mVideoTrackIndex = writeHeadInfo(outputBuffer, bufferInfo);
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
Log.d(TAG, "write outputBuffer");
mMediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
}
mEncoder.releaseOutputBuffer(outputBufferIndex, false);
break; // 跳出循環
}
}
}
private int writeHeadInfo(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo) {
byte[] csd = new byte[bufferInfo.size];
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
outputBuffer.position(bufferInfo.offset);
outputBuffer.get(csd);
ByteBuffer sps = null;
ByteBuffer pps = null;
for (int i = bufferInfo.size - 1; i > 3; i--) {
if (csd[i] == 1 && csd[i - 1] == 0 && csd[i - 2] == 0 && csd[i - 3] == 0) {
sps = ByteBuffer.allocate(i - 3);
pps = ByteBuffer.allocate(bufferInfo.size - (i - 3));
sps.put(csd, 0, i - 3).position(0);
pps.put(csd, i - 3, bufferInfo.size - (i - 3)).position(0);
}
}
MediaFormat outputFormat = mEncoder.getOutputFormat();
if (sps != null && pps != null) {
outputFormat.setByteBuffer("csd-0", sps);
outputFormat.setByteBuffer("csd-1", pps);
}
int videoTrackIndex = mMediaMuxer.addTrack(outputFormat);
Log.d(TAG, "videoTrackIndex: " + videoTrackIndex);
mMediaMuxer.start();
return videoTrackIndex;
}
}
四、調用示例
結合上一篇的解碼器,可以做一些解碼再編碼的例子。
連結:Android MediaExtractor+MediaCodec解碼視訊,傳回yuv回調資料
VideoDecoder mVideoDecoder = new VideoDecoder();
mVideoDecoder.setOutputFormat(VideoDecoder.COLOR_FORMAT_NV12); // 設定輸出nv12的資料
VideoEncoder mVideoEncoder = null;
// 某某線程中
mVideoDecoder.decode("/sdcard/test.mp4", new VideoDecoder.DecodeCallback() {
@Override
public void onDecode(byte[] yuv, int width, int height, int frameCount, long presentationTimeUs) {
Log.d(TAG, "frameCount: " + frameCount + ", presentationTimeUs: " + presentationTimeUs);
if (mVideoEncoder == null) {
mVideoEncoder = new VideoEncoder();
mVideoEncoder.init("/sdcard/test_out.mp4", width, height);
}
// yuv資料操作,例如儲存或者再去編碼等
mVideoEncoder.encode(yuv, presentationTimeUs);
}
@Override
public void onFinish() {
Log.d(TAG, "onFinish");
if (mVideoEncoder != null) mVideoEncoder.release();
}
@Override
public void onStop() {
Log.d(TAG, "onStop");
if (mVideoEncoder != null) mVideoEncoder.release();
}
});