Android-->MediaMuxer,MediaCodec,AudioRecord及Camera實現音訊視訊混合MP4檔案
本文相當長,讀者請注意…
閱讀之前,我喜歡你已經瞭解了以下內容:
這個開源庫介紹了, 音訊和視訊的錄製, 其實已經夠了~~~,不過視訊的錄製採用的是GLSurfaceView中的Surface方法, 並沒有直接採用TextureView和Camera的PreviewCallback方法.
這個是谷歌的開源專案,裡面介紹了很多關於GLSurfaceView和TextureView的操作,當然也有MediaCodec的使用.
能量補充完了,就該到我登場了…
本文的目的是通過Camera的PreviewCallback拿到幀資料,用MediaCodec編碼成H264,新增到MediaMuxer混合器打包成MP4檔案,並且使用TextureView預覽攝像頭. 當然使用AudioRecord錄製音訊,也是通過MediaCodec編碼,一樣是新增到MediaMuxer混合器和視訊一起打包, 這個難度係數很低.
在使用MediaMuxer混合的時候,主要的難點就是控制視訊資料和音訊資料的同步新增,和狀態的判斷;
本文所有程式碼,採用片段式講解,文章結尾會有原始碼下載:
1:視訊錄製和H264的資料獲取
Camera mCamera = Camera.open();
mCamera.addCallbackBuffer(mImageCallbackBuffer);//必須的呼叫1
mCamera.setPreviewCallbackWithBuffer(mCameraPreviewCallback);
...
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//通過回撥,拿到的data資料是原始資料
videoRunnable.add(data);//丟給videoRunnable執行緒,使用MediaCodec進行h264編碼操作
camera.addCallbackBuffer(data);//必須的呼叫2
}
1.1:H264的編碼操作
編碼器的配置:
private static final String MIME_TYPE = "video/avc"; // H.264的mime型別
MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);//選擇系統用於編碼H264的編碼器資訊,固定的呼叫
mColorFormat = selectColorFormat(codecInfo, MIME_TYPE);//根據MIME格式,選擇顏色格式,固定的呼叫
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE,
this.mWidth, this.mHeight);//根據MIME建立MediaFormat,固定
//以下引數的設定,儘量固定.當然,如果你非常瞭解,也可以自行修改
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);//設定位元率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);//設定幀率
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);//設定顏色格式
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);//設定關鍵幀的時間
try {
mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());//這裡就是根據上面拿到的編碼器建立一個MediaCodec了;//MediaCodec還有一個方法可以直接用MIME型別,建立
} catch (IOException e) {
e.printStackTrace();
}
//第二個引數用於播放MP4檔案,顯示影象的Surface;
//第四個引數,編碼H264的時候,固定CONFIGURE_FLAG_ENCODE, 播放的時候傳入0即可;API文件有解釋
mMediaCodec.configure(mediaFormat, null, null,
MediaCodec.CONFIGURE_FLAG_ENCODE);//關鍵方法
mMediaCodec.start();//必須
開始H264的編碼:
private void encodeFrame(byte[] input) {//這個引數就是上面回撥拿到的原始資料
NV21toI420SemiPlanar(input, mFrameData, this.mWidth, this.mHeight);//固定的方法,用於顏色轉換
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();//拿到輸入緩衝區,用於傳送資料進行編碼
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();//拿到輸出緩衝區,用於取到編碼後的資料
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);//得到當前有效的輸入緩衝區的索引
if (inputBufferIndex >= 0) {//當輸入緩衝區有效時,就是>=0
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(mFrameData);//往輸入緩衝區寫入資料,關鍵點
mMediaCodec.queueInputBuffer(inputBufferIndex, 0,
mFrameData.length, System.nanoTime() / 1000, 0);//將緩衝區入隊
} else {
}
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);//拿到輸出緩衝區的索引
do {
if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = mMediaCodec.getOutputBuffers();
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//特別注意此處的呼叫
MediaFormat newFormat = mMediaCodec.getOutputFormat();
MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
if (mediaMuxerRunnable != null) {
//如果要合成視訊和音訊,需要處理混合器的音軌和視軌的新增.因為只有新增音軌和視軌之後,寫入資料才有效
mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
}
} else if (outputBufferIndex < 0) {
} else {
//走到這裡的時候,說明資料已經編碼成H264格式了
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];//outputBuffer儲存的就是H264資料了
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
//因為上面的addTrackIndex方法不一定會被呼叫,所以要在此處再判斷並新增一次,這也是混合的難點之一
if (mediaMuxerRunnable.isAudioAdd()) {
MediaFormat newFormat = mMediaCodec.getOutputFormat();
mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
}
// adjust the ByteBuffer values to match BufferInfo (not needed?)
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
if (mediaMuxerRunnable != null) {
//這一步就是新增視訊資料到混合器了,在呼叫新增資料之前,一定要確保視軌和音軌都新增到了混合器
mediaMuxerRunnable.addMuxerData(new MediaMuxerRunnable.MuxerData(
MediaMuxerRunnable.TRACK_VIDEO, outputBuffer, mBufferInfo
));
}
}
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);//釋放資源
}
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
} while (outputBufferIndex >= 0);
}
群友補充1:
上段程式碼中的NV21toI420SemiPlanar
實現方法, 這個編碼的視訊是黑白的,把這個方法的實現,改為:
private void NV21toI420SemiPlanar(byte[] nv21bytes, byte[] i420bytes, int width, int height) {
final int iSize = width * height;
System.arraycopy(nv21bytes, 0, i420bytes, 0, iSize);
for (int iIndex = 0; iIndex < iSize / 2; iIndex += 2) {
i420bytes[iSize + iIndex / 2 + iSize / 4] = nv21bytes[iSize + iIndex]; // U
i420bytes[iSize + iIndex / 2] = nv21bytes[iSize + iIndex + 1]; // V
}
}
就會是彩色;
群友補充2:
上段程式碼中的
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
改為
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, System.nanoTime() / 1000);
即可提高成功率.
如果專案中遇到這兩個問題,大可以拿去。感謝群友 明天的現在
.
2:音訊的錄製和編碼
和視訊一樣,需要配置編碼器:
private static final String MIME_TYPE = "audio/mp4a-latm";
audioCodecInfo = selectAudioCodec(MIME_TYPE);//是不是似曾相識?沒錯,一樣是通過MIME拿到系統對應的編碼器資訊
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);//CHANNEL_IN_STEREO 立體聲
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
// audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length());
// audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
//過程都差不多~不解釋了;
獲取音訊裝置,用於獲取音訊資料:
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
try {
final int min_buffer_size = AudioRecord.getMinBufferSize(
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
if (buffer_size < min_buffer_size)
buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;
audioRecord = null;
for (final int source : AUDIO_SOURCES) {
try {
audioRecord = new AudioRecord(
source, SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
audioRecord = null;
} catch (final Exception e) {
audioRecord = null;
}
if (audioRecord != null) break;
}
} catch (final Exception e) {
Log.e(TAG, "AudioThread#run", e);
}
開始音訊資料的採集:
audioRecord.startRecording();//固定寫法
while (!isExit) {
buf.clear();
readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME);//讀取音訊資料到buf
if (readBytes > 0) {
buf.position(readBytes);
buf.flip();
encode(buf, readBytes, getPTSUs());//開始編碼
}
}
開始音訊編碼:
private void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) {
if (isExit) return;
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
/*向編碼器輸入資料*/
if (inputBufferIndex >= 0) {
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
if (buffer != null) {
inputBuffer.put(buffer);
}
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0,
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length,
presentationTimeUs, 0);
}
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
}
//上面的過程和視訊是一樣的,都是向輸入緩衝區輸入原始資料
/*獲取解碼後的資料*/
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
int encoderStatus;
do {
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
encoderOutputBuffers = mMediaCodec.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//特別注意此處, 此處和視訊編碼是一樣的
final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
if (mediaMuxerRunnable != null) {
//新增音軌,和新增視軌都是一樣的呼叫
mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_AUDIO, format);
}
} else if (encoderStatus < 0) {
} else {
final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
mBufferInfo.presentationTimeUs = getPTSUs();
//當保證視軌和音軌都新增完成之後,才可以新增資料到混合器
muxer.addMuxerData(new MediaMuxerRunnable.MuxerData(
MediaMuxerRunnable.TRACK_AUDIO, encodedData, mBufferInfo));
prevOutputPTSUs = mBufferInfo.presentationTimeUs;
}
mMediaCodec.releaseOutputBuffer(encoderStatus, false);
}
} while (encoderStatus >= 0);
}
3:混合器的操作
private Vector<MuxerData> muxerDatas;//緩衝傳輸過來的資料
public void start(String filePath) throws IOException {
isExit = false;
isVideoAdd = false;
//建立混合器
mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
if (audioRunnable != null) {
//音訊準備工作
audioRunnable.prepare();
audioRunnable.prepareAudioRecord();
}
if (videoRunnable != null) {
//視訊準備工作
videoRunnable.prepare();
}
new Thread(this).start();
if (audioRunnable != null) {
new Thread(audioRunnable).start();//開始音訊解碼執行緒
}
if (videoRunnable != null) {
new Thread(videoRunnable).start();//開始視訊解碼執行緒
}
}
//混合器,最重要的就是保證再新增資料之前,要先新增視軌和音軌,並且儲存響應軌跡的索引,用於新增資料的時候使用
public void addTrackIndex(@TrackIndex int index, MediaFormat mediaFormat) {
if (isMuxerStart()) {
return;
}
int track = mediaMuxer.addTrack(mediaFormat);
if (index == TRACK_VIDEO) {
videoTrackIndex = track;
isVideoAdd = true;
Log.e("angcyo-->", "新增視軌");
} else {
audioTrackIndex = track;
isAudioAdd = true;
Log.e("angcyo-->", "新增音軌");
}
requestStart();
}
private void requestStart() {
synchronized (lock) {
if (isMuxerStart()) {
mediaMuxer.start();//在start之前,確保視軌和音軌已經添加了
lock.notify();
}
}
}
while (!isExit) {
if (muxerDatas.isEmpty()) {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
if (isMuxerStart()) {
MuxerData data = muxerDatas.remove(0);
int track;
if (data.trackIndex == TRACK_VIDEO) {
track = videoTrackIndex;
} else {
track = audioTrackIndex;
}
//新增資料...
mediaMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
}
}
}
如果您喜歡這篇文章,您也可以進行打賞, 金額不限.
聯絡作者
請使用QQ掃碼加群, 小夥伴們在等著你哦!
關注我的公眾號, 每天都能一起玩耍哦!