Android音視頻(一) Camera2 API采集數(shù)據(jù)
Android音視頻(二)音頻AudioRecord和AudioTrack
Android音視頻(三)FFmpeg Camera2推流直播
MediaCodec類可以訪問底層媒體編解碼框架(StageFright 或 OpenMAX)剂府,即編解碼組件拧揽,它是Android基本的多媒體支持基礎(chǔ)架構(gòu)的一部分,通常和MediaExtractor腺占、MediaSync淤袜、MediaMuxer、MediaCrypto衰伯、MediaDrm铡羡、Image、Surface和AudioTrack一起使用意鲸。它本身并不是Codec烦周,它通過調(diào)用底層編解碼組件獲得了Codec的能力。
MediaCodec的工作方式
MediaCodec處理輸入數(shù)據(jù)產(chǎn)生輸出數(shù)據(jù)怎顾。當異步處理數(shù)據(jù)時读慎,使用一組輸入和輸出Buffer隊列。通常槐雾,在邏輯上贪壳,客戶端請求(或接收)數(shù)據(jù)后填入預(yù)先設(shè)定的空輸入緩沖區(qū),輸入Buffer填滿后將其傳遞到MediaCodec并進行編解碼處理蚜退。之后MediaCodec編解碼后的數(shù)據(jù)填充到一個輸出Buffer中。最后彪笼,客戶端請求(或接收)輸出Buffer钻注,消耗輸出Buffer中的內(nèi)容,用完后釋放配猫,給回MediaCodec重新填充輸出數(shù)據(jù)幅恋。
必須保證輸入和輸出隊列同時非空,即至少有一個輸入Buffer和輸出Buffer才能工作泵肄。
MediaCodec狀態(tài)周期圖
在MediaCodec的生命周期中存在三種狀態(tài) :Stopped捆交、Executing淑翼、Released。
Stopped狀態(tài)實際上還可以處在三種狀態(tài):Uninitialized品追、Configured玄括、Error。
Executing狀態(tài)也分為三種子狀態(tài):Flushed, Running肉瓦、End-of-Stream遭京。
從上圖可以看出:
- 當創(chuàng)建編解碼器的時候處于未初始化狀態(tài)。首先你需要調(diào)用configure(…)方法讓它處于Configured狀態(tài)泞莉,然后調(diào)用start()方法讓其處于Executing狀態(tài)哪雕。在Executing狀態(tài)下,你就可以使用上面提到的緩沖區(qū)來處理數(shù)據(jù)鲫趁。
- Executing的狀態(tài)下也分為三種子狀態(tài):Flushed, Running斯嚎、End-of-Stream。在start() 調(diào)用后挨厚,編解碼器處于Flushed狀態(tài)堡僻,這個狀態(tài)下它保存著所有的緩沖區(qū)。一旦第一個輸入buffer出現(xiàn)了幽崩,編解碼器就會自動運行到Running的狀態(tài)苦始。當帶有end-of-stream標志的buffer進去后,編解碼器會進入End-of-Stream狀態(tài)慌申,這種狀態(tài)下編解碼器不在接受輸入buffer陌选,但是仍然在產(chǎn)生輸出的buffer。此時你可以調(diào)用flush()方法蹄溉,將編解碼器重置于Flushed狀態(tài)咨油。
- 調(diào)用stop()將編解碼器返回到未初始化狀態(tài),然后可以重新配置柒爵。 完成使用編解碼器后役电,您必須通過調(diào)用release()來釋放它。
- 在極少數(shù)情況下棉胀,編解碼器可能會遇到錯誤并轉(zhuǎn)到錯誤狀態(tài)法瑟。 這是使用來自排隊操作的無效返回值或有時通過異常來傳達的。 調(diào)用reset()使編解碼器再次可用唁奢。 您可以從任何狀態(tài)調(diào)用它來將編解碼器移回未初始化狀態(tài)鸠蚪。 否則料身,調(diào)用 release()動到終端釋放狀態(tài)棠隐。
MediaCodec的優(yōu)缺點
優(yōu)點:功耗低斑举,速度快
缺點:擴展性不強,不同芯片廠商提供的支持方案不同,導(dǎo)致程序移植性差
適用場景:適合有固定的硬件方案的項目熬北,如智能家居類疙描;需要長時間攝像。
MediaCodec 編解碼實現(xiàn)
做了一個Demo讶隐,使用AudioRecord錄音起胰,使用MediaCodec 編碼為AAC并保存文件,然后可以從AAC解碼為PCM數(shù)據(jù)整份,再用AudioTrack播放待错。
1、編碼PCM數(shù)據(jù)烈评,保存為AAC文件
初始化AudioRecord和編碼器
private void initAudioRecord() {
int audioSource = MediaRecorder.AudioSource.MIC;
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
mAudioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, 2048));
}
/**
* 初始化編碼器
*/
private void initAudioEncoder() {
try {
mAudioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_BUFFER_SIZE);
mAudioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
}
if (mAudioEncoder == null) {
Log.e(TAG, "create mediaEncode failed");
return;
}
mAudioEncoder.start(); // 啟動MediaCodec,等待傳入數(shù)據(jù)
encodeInputBuffers = mAudioEncoder.getInputBuffers(); //上面介紹的輸入和輸出Buffer隊列
encodeOutputBuffers = mAudioEncoder.getOutputBuffers();
mAudioEncodeBufferInfo = new MediaCodec.BufferInfo();
}
開始錄音火俄、編碼
使用線程池,兩條線程讲冠,一個線程去錄音瓜客,另一個線程做編碼操作。錄音線程會將PCM數(shù)據(jù)存入一個隊列中竿开,編碼線程從隊列中取出數(shù)據(jù)編碼谱仪。
// 開啟錄音線程
mExecutorService.submit(new Runnable() {
@Override
public void run() {
startRecorder();
}
});
// 開啟編碼線程
mExecutorService.submit(new Runnable() {
@Override
public void run() {
encodePCM();
}
});
/**
* 將PCM數(shù)據(jù)存入隊列
*/
private void putPCMData(byte[] pcmChunk) {
Log.e(TAG, "putPCMData");
try {
queue.put(pcmChunk);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 從隊列取出PCM數(shù)據(jù)
*/
private byte[] getPCMData() {
try {
if (queue.isEmpty()) {
return null;
}
return queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
/**
* 添加ADTS頭,如果要與視頻流合并就不用添加否彩,單獨AAC文件就需要添加疯攒,否則無法正常播放
*/
public static void addADTStoPacket(int sampleRateType, byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int chanCfg = 2; // CPE
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (sampleRateType << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
音頻數(shù)據(jù)
/**
* 獲取音頻數(shù)據(jù)
*/
private void startRecorder() {
try {
mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/RecorderTest/" + System.currentTimeMillis() + ".aac";
mAudioFile = new File(mFilePath);
if (!mAudioFile.getParentFile().exists()) {
mAudioFile.getParentFile().mkdirs();
}
mAudioFile.createNewFile();
mFileOutputStream = new FileOutputStream(mAudioFile);
mAudioBos = new BufferedOutputStream(mFileOutputStream, 200 * 1024);
mAudioRecorder.startRecording();
start = System.currentTimeMillis();
while (mIsRecording) {
int read = mAudioRecorder.read(mBuffer, 0, 2048);
if (read > 0) {
byte[] audio = new byte[read];
System.arraycopy(mBuffer, 0, audio, 0, read);
putPCMData(audio); // PCM數(shù)據(jù)放入隊列,等待編碼
}
}
} catch (IOException | RuntimeException e) {
e.printStackTrace();
} finally {
if (mAudioRecorder != null) {
mAudioRecorder.release();
mAudioRecorder = null;
}
}
}
編碼
從隊列中循環(huán)取出數(shù)據(jù)列荔,MediaCodec 編碼敬尺,將編碼后的數(shù)據(jù)寫入文件中。
/**
* 編碼PCM
*/
private void encodePCM() {
int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
byte[] chunkAudio;
int outBitSize;
int outPacketSize;
byte[] chunkPCM;
while (mIsRecording || !queue.isEmpty()) {
chunkPCM = getPCMData();//獲取解碼器所在線程輸出的數(shù)據(jù) 代碼后邊會貼上
if (chunkPCM == null) {
continue;
}
inputIndex = mAudioEncoder.dequeueInputBuffer(-1);//同解碼器
if (inputIndex >= 0) {
inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
inputBuffer.clear();//同解碼器
inputBuffer.limit(chunkPCM.length);
inputBuffer.put(chunkPCM);//PCM數(shù)據(jù)填充給inputBuffer
mAudioEncoder.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知編碼器 編碼
}
outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
while (outputIndex >= 0) {
outBitSize = mAudioEncodeBufferInfo.size;
outPacketSize = outBitSize + 7;//7為ADTS頭部的大小
outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
outputBuffer.position(mAudioEncodeBufferInfo.offset);
outputBuffer.limit(mAudioEncodeBufferInfo.offset + outBitSize);
chunkAudio = new byte[outPacketSize];
addADTStoPacket(44100, chunkAudio, outPacketSize);//添加ADTS
outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC數(shù)據(jù) 取出到byte[]中 偏移量offset=7
outputBuffer.position(mAudioEncodeBufferInfo.offset);
try {
mAudioBos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內(nèi)存卡中 *.aac
} catch (IOException e) {
e.printStackTrace();
}
mAudioEncoder.releaseOutputBuffer(outputIndex, false);
outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
}
}
stopRecorder();
}
2贴浙、解碼AAC AudioTrack播放
初始化AudioTrack和解碼器
/**
* 初始化AudioTrack砂吞,等待播放數(shù)據(jù)
*/
private void initAudioTrack() {
int streamType = AudioManager.STREAM_MUSIC;
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int mode = AudioTrack.MODE_STREAM;
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat,
Math.max(minBufferSize, 2048), mode);
audioTrack.play();
}
/**
* 初始化解碼器
*/
private void initAudioDecoder() {
try {
mMediaExtractor = new MediaExtractor();
mMediaExtractor.setDataSource(mFilePath);
MediaFormat format = mMediaExtractor.getTrackFormat(0);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio")) {//獲取音頻軌道
mMediaExtractor.selectTrack(0);//選擇此音頻軌道
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 0);
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
format.setInteger(MediaFormat.KEY_IS_ADTS, 1);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, 0);
mAudioDecoder = MediaCodec.createDecoderByType(mime);//創(chuàng)建Decode解碼器
mAudioDecoder.configure(format, null, null, 0);
} else {
return;
}
} catch (IOException e) {
e.printStackTrace();
}
if (mAudioDecoder == null) {
Log.e(TAG, "mAudioDecoder is null");
return;
}
mAudioDecoder.start();//啟動MediaCodec ,等待傳入數(shù)據(jù)
}
解碼并播放
private void decodeAndPlay() {
boolean isFinish = false;
MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();
while (!isFinish && mIsPalying) {
int inputIdex = mAudioDecoder.dequeueInputBuffer(10000);//獲取可用的inputBuffer -1代表一直等待崎溃,0表示不等待 10000表示10秒超時
if (inputIdex < 0) {
isFinish = true;
}
ByteBuffer inputBuffer = mAudioDecoder.getInputBuffer(inputIdex);
inputBuffer.clear();//清空之前傳入inputBuffer內(nèi)的數(shù)據(jù)
int samplesize = mMediaExtractor.readSampleData(inputBuffer, 0);
if (samplesize > 0) {
mAudioDecoder.queueInputBuffer(inputIdex, 0, samplesize, 0, 0); //通知解碼器 解碼
mMediaExtractor.advance(); //MediaExtractor移動到下一取樣處
} else {
isFinish = true;
}
int outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000);//獲取解碼得到的byte[]數(shù)據(jù)
ByteBuffer outputBuffer;
byte[] chunkPCM;
//每次解碼完成的數(shù)據(jù)不一定能一次吐出 所以用while循環(huán)蜻直,保證解碼器吐出所有數(shù)據(jù)
while (outputIndex >= 0) {
outputBuffer = mAudioDecoder.getOutputBuffer(outputIndex);
chunkPCM = new byte[decodeBufferInfo.size];
outputBuffer.get(chunkPCM);
outputBuffer.clear();//數(shù)據(jù)取出后一定記得清空此Buffer MediaCodec是循環(huán)使用這些Buffer的,不清空下次會得到同樣的數(shù)
// 播放解碼后的PCM數(shù)據(jù)
audioTrack.write(chunkPCM, 0, decodeBufferInfo.size);
mAudioDecoder.releaseOutputBuffer(outputIndex, false);
outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取數(shù)據(jù)
}
}
stopPlay();
}
Demo完成袁串,手機測試效果不錯概而。MediaCodec的使用要比我預(yù)想的復(fù)雜,網(wǎng)上查了好久才完成這個Demo囱修,希望能幫到需要的人到腥。
如有問題歡迎留言,Github源碼 - MediaCodecActivity