MediaCodec官方图解:
MediaCodec操作步骤图解:
从API 16开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,与MediaPlayer/VideoView等high-level APIs相比,MediaCodec是low-level APIs,因此它提供了更加完善、灵活、丰富的接口,开发者可以实现更加灵活的功能。
MediaCodec类可用于访问Android底层的多媒体编解码器,例如编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface以及AudioTrack一起使用)。
1 MediaCodec使用步骤
- 创建并配置一个MediaCodec对象
- 循环直到完成:
- 通过请求一个空的输入缓存(ByteBuffer),向其中填充满数据并将它传递给编解码器MediaCodec处理
- 编解码器MediaCodec处理完这些数据并将处理结果输出至一个空的输出缓存(ByteBuffer)中
- 使用完输出缓存的数据之后,将输出缓存容器释放回编解码器MediaCodec再次使用
- 释放MediaCodec
上述的操作流程如下:
数据读入空的输入缓冲区->
MediaCodec从输入缓冲区异步读取数据解码->
MediaCodec将解码后的数据重新编码后输入空的输出缓冲区->
使用完输出的数据后将空的输出缓冲区释放回MediaCodec再次使用
2 MediaCodec可以处理的数据类型
MediaCodec编解码器可以处理的三种类型的数据均可以利用ByteBuffers进行处理:
- 压缩数据(即经过H264、H265等编码的视频数据或AAC等编码的音频数据)
- 原始音频数据(PCM)
- 原始视频数据(YUV或RGB【Y控制亮度,U和V控制色度;YUV主要分为两大类:YUV xxx P和YUV xxxSP,例如原格式是NV21,就是YUV420SP】)
3 MediaCodec对原始视频数据的特殊处理:
对于原始视频数据编解码时应提供一个Surface以提高编解码器的性能。Surface直接使用本地视频数据缓存(native video buffers,而没有映射或复制数据到ByteBuffers,因此,这种方式会更加高效。在使用Surface的时候,通常不能直接访问原始视频数据,但是可以使用ImageReader类来访问非安全的解(原始)视频帧。这仍然比使用ByteBuffers更加高效,因为一些本地缓存(native buffer)可以被映射到direct ByteBuffers。当使用ByteBuffer模式,你可以利用Image类和getInput/OutputImage方法来访问到原始视频数据帧。
上述的原始视频数据处理简要如下:
- 原始视频编解码要提供一个Surface;
- Surface不能直接访问原始视频数据,要使用ImageReader获取视频帧
4 MediaCodec生命周期
MediaCodec生命周期:
在编解码器的生命周期内有三种理论状态:停止态(Stopped)、执行态(Executing)、释放态(Released)。
1、停止态包括了三种子状态:
- 未初始化(Uninitialized)
- 配置(Configured)
- 错误(Error)
2、执行态包括了三种子状态:
- 刷新(Flushed)
- 运行(Running)
- 流结束(End-Of-Stream)
MediaCodec生命周期状态切换时机:
任意工厂方法创建了MediaCodec | 未初始化状态(Uninitialized) |
config(…) | 配置状态(Configured) |
start() | 刷新状态(Flushed),此时编解码器会拥有所有的缓存 |
第一个输入缓存被移除队列 | 运行状态(Running) |
一个带有end-of-stream标记的输入缓存入队列时 | 流结束状态(End-of-Stream),此时仍然产生输出缓存直到end-of-stream标记到达输出端 |
在执行状态(Executing)调用flush() | 刷新状态(Flushed) |
stop() | 未初始化状态(Uninitialized),此时编解码器可以重新配置 |
队列操作时返回错误或异常 | 错误状态(Error),可以通过调用reset()使编解码器再次可用 |
reset() | 未初始化状态(Uninitialized) |
release() | 释放状态(Released) |
以上状态切换简要如下:
正常生命周期切换:
Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released用户手动切换:
Uninitialized->
Configured->
Flushed->
Running->
调用stop()或reset()->
Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released发生错误:
Uninitialized->
Configured->
Flushed->
Running->
Error->
Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released
MediaCodec数据读取相关方法:
- MediaCodec.getInputBuffers():获取缓存输入缓冲区数组
- MediaCodec.getOutputBuffers():获取缓存输出缓冲区数组
- MediaCodec.dequeueInputBuffer():获取缓存数组的输入缓冲区索引
- MediaCodec.queueInputBuffer():将输入读入输入缓冲区
- MediaCodec.dequeueOutputBuffer():获取缓存数组的输出缓冲区索引
- MediaCodec.releaseOutputBuffer():释放输出缓冲区回MediaCodec
5 案例:将音频文件解码为PCM后再重新编码为AAC输出
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_pcm_codec_aac"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PCM音频编码为AAC" />
<TextView
android:id="@+id/tv_target_file_length"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_current_file_length"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
package com.example.media.codec;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.example.media.R;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
public class MediaCodecActivity extends AppCompatActivity {
private static final String TAG = MediaCodecActivity.class.getSimpleName();
private static final int TAG_CODEC_PROGRESS = 0x0001; // 正在编解码
private static final int TAG_CODEC_COMPLETED = 0x0002; // 完成编解码
private Button mBtnPCMcodecAAC;
private TextView mTvTargetFileLength;
private TextView mTvCurrentFileLength;
private String mTargetAudioPath;
private String mOutputAudioPath;
// 解码
private MediaExtractor mMediaDecoderExtractor; // 分离音频文件获取音频轨PCM数据
private MediaCodec mMediaDecoder; // 解码器
private ByteBuffer[] mDecoderInputBuffers; // 解码输入缓冲区数组
private ByteBuffer[] mDecoderOutputBuffers; // 解码输出缓冲区数组
private MediaCodec.BufferInfo mDecoderBufferInfo;
// 编码
private MediaCodec mMediaEncoder; // 编码器
private ByteBuffer[] mEncoderInputBuffers; // 编码输入缓冲区数组
private ByteBuffer[] mEncoderOutputBuffers; // 编码输出缓冲区数组
private MediaCodec.BufferInfo mEncoderBufferInfo;
private FileOutputStream fos;
private BufferedOutputStream bos;
private int mBitRate; //
private int mChannelCount; // 声道数量
private int mSampleRate; // 采样率
private int mSampleRateType; // 采样率类型
private int mTargetFileTotalLength;
private int mCurrentFileLength;
private ArrayBlockingQueue<byte[]> mPCMQueue; // 存放解码的PCM队列
private boolean mIsDecodeOver; // 解码完成标志
private Handler mHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_codec);
mTvTargetFileLength = findViewById(R.id.tv_target_file_length);
mTvCurrentFileLength = findViewById(R.id.tv_current_file_length);
mBtnPCMcodecAAC = findViewById(R.id.btn_pcm_codec_aac);
mBtnPCMcodecAAC.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBtnPCMcodecAAC.setEnabled(false);
mBtnPCMcodecAAC.setText("正在编解码中....");
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.v(TAG, "external storage no exists");
mBtnPCMcodecAAC.setEnabled(true);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
"/Android/data/" + getPackageName() + "/audio/";
File rootDir = new File(rootPath);
if (!rootDir.exists()) {
if (!rootDir.mkdirs()) {
Log.v(TAG, "root dir create failed");
mBtnPCMcodecAAC.setEnabled(true);
return;
}
}
mTargetAudioPath = rootPath + "heavy.mp3";
if (!new File(mTargetAudioPath).exists()) {
Log.v(TAG, "target audio file no exists");
return;
}
mTargetFileTotalLength = (int) new File(mTargetAudioPath).length();
mOutputAudioPath = rootPath + "audio_aac.aac";
mPCMQueue = new ArrayBlockingQueue<>(10);
try {
fos = new FileOutputStream(new File(mOutputAudioPath));
bos = new BufferedOutputStream(fos, 200 * 1024);
} catch (FileNotFoundException e) {
e.printStackTrace();
Toast.makeText(MediaCodecActivity.this, "FileNotFoundException", Toast.LENGTH_SHORT).show();
return;
}
initMediaDecoder();
initMediaEncoder();
startCodec();
}
}
});
mHandler = new Handler(getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case TAG_CODEC_PROGRESS:
String targetFileLength = Formatter.formatFileSize(MediaCodecActivity.this, mTargetFileTotalLength);
String currentFileLength = Formatter.formatFileSize(MediaCodecActivity.this, msg.arg1);
mTvTargetFileLength.setText(targetFileLength);
mTvCurrentFileLength.setText(currentFileLength);
break;
case TAG_CODEC_COMPLETED:
mBtnPCMcodecAAC.setEnabled(true);
mBtnPCMcodecAAC.setText("编码完成");
break;
default:
break;
}
}
};
mOnCodecProgressListener = new OnCodecProgressListener() {
@Override
public void onProgress(long currentFileLength, long targetFileTotalLength) {
Log.v(TAG, "currentFileLength = " + currentFileLength
+ ", targetFileTotalLength = " + targetFileTotalLength);
}
@Override
public void onCompleted() {
release();
}
};
}
@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
release();
super.onDestroy();
}
/**
* 开启线程进行编解码
*/
private void startCodec() {
new Thread(new DecoderRunnable()).start();
new Thread(new EncoderRunnable()).start();
}
/**
* 编码线程
*/
private class EncoderRunnable implements Runnable {
@Override
public void run() {
while (!mIsDecodeOver || !mPCMQueue.isEmpty()) {
PCMtoAAC();
}
Message msg = Message.obtain();
msg.what = TAG_CODEC_COMPLETED;
mHandler.sendMessage(msg);
if (mOnCodecProgressListener != null) {
mOnCodecProgressListener.onCompleted();
}
}
}
/**
* 解码线程
*/
private class DecoderRunnable implements Runnable {
@Override
public void run() {
while (!mIsDecodeOver) {
decodeToPcm();
}
}
}
/**
* 初始化编码
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void initMediaEncoder() {
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, mSampleRate,
mChannelCount);
format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
try {
mMediaEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); // Uninitialized
mMediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // Configured
} catch (IOException e) {
e.printStackTrace();
}
if (mMediaEncoder == null) {
Log.v(TAG, "create encoder failed");
return;
}
mMediaEncoder.start(); // Executing,Flushed
mEncoderInputBuffers = mMediaEncoder.getInputBuffers();
mEncoderOutputBuffers = mMediaEncoder.getOutputBuffers();
mEncoderBufferInfo = new MediaCodec.BufferInfo();
}
/**
* 初始化解码
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void initMediaDecoder() {
try {
mMediaDecoderExtractor = new MediaExtractor();
mMediaDecoderExtractor.setDataSource(mTargetAudioPath);
int trackCount = mMediaDecoderExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mMediaDecoderExtractor.getTrackFormat(i);
String mimeType = format.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("audio")) {
mMediaDecoderExtractor.selectTrack(i);
mBitRate = format.getInteger(MediaFormat.KEY_BIT_RATE);
mChannelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
mSampleRateType = ADTSUtils.getSampleRateType(mSampleRate);
mMediaDecoder = MediaCodec.createDecoderByType(mimeType); // Uninitialized
mMediaDecoder.configure(format, null, null, 0); // Configured
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
if (mMediaDecoder == null) {
Log.v(TAG, "create decoder failed");
return;
}
mMediaDecoder.start(); // Executing,Flushed
mDecoderInputBuffers = mMediaDecoder.getInputBuffers();
mDecoderOutputBuffers = mMediaDecoder.getOutputBuffers();
mDecoderBufferInfo = new MediaCodec.BufferInfo();
}
/**
* 将音频文件解码为PCM
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void decodeToPcm() {
// dequeueInputBuffer():获取输入缓冲区数组索引
// queueInputBuffer():将数据读入输入缓冲区
// dequeueOutputBuffer():获取输出缓冲区数组索引
// releaseOutputBuffer():释放输出缓冲区给MediaCodec
// 解码流程:
// dequeueInputBuffer()获取输入缓冲区索引->根据获取获取输入缓冲区->MediaExtractor获取数据->
// queueInputBuffer()将数据读入输入缓冲区解码->dequeueOutputBuffer()获取输出缓冲区索引->
// 将解码后的数据另外写入队列中->releaseOutputBuffer()释放输出缓冲区回MediaCodec
for (int i = 0; i < mDecoderInputBuffers.length - 1; i++) {
// 获取输入缓存数组索引
// 传入参数为等待时间,单位微秒,-1一直等待,0不等待,建议-1避免丢帧
int inputBufferIndex = mMediaDecoder.dequeueInputBuffer(-1);
if (inputBufferIndex < 0) {
mIsDecodeOver = true;
return;
}
ByteBuffer inputBuffer = mDecoderInputBuffers[inputBufferIndex]; // 根据索引从缓存数组获取到输入缓冲区
inputBuffer.clear(); // 清空一下之前传入inputBuffer的数据
int sampleSize = mMediaDecoderExtractor.readSampleData(inputBuffer, 0); // 读取数据
if (sampleSize < 0) {
mIsDecodeOver = true;
} else {
// 通知MediaCodec解码刚才传入的数据sampleSize
mMediaDecoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, 0, 0);
mMediaDecoderExtractor.advance();
mCurrentFileLength += sampleSize;
Message msg = Message.obtain();
msg.what = TAG_CODEC_PROGRESS;
msg.arg1 = mCurrentFileLength;
mHandler.sendMessage(msg);
if (mOnCodecProgressListener != null) {
mOnCodecProgressListener.onProgress(mCurrentFileLength, mTargetFileTotalLength);
}
}
}
// 获取输出缓存数组索引
// 传入参数为等待时间,单位微秒,-1一直等待,0不等待,建议不要传入-1,有些时候没有数据输出那么就会一直卡住等待
int outputBufferIndex = mMediaDecoder.dequeueOutputBuffer(mDecoderBufferInfo, 10000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mDecoderOutputBuffers[outputBufferIndex];
byte[] pcmBuffer = new byte[mDecoderBufferInfo.size];
outputBuffer.get(pcmBuffer); // 将输出缓冲区的数据写入pcmBuffer
outputBuffer.clear(); // 要清空缓冲区,否则下次会拿到相同的数据
putPCMData(pcmBuffer);
mMediaDecoder.releaseOutputBuffer(outputBufferIndex, false); // 释放输出缓冲区给MediaCodec,没有该操作将不能向外输出数据
outputBufferIndex = mMediaDecoder.dequeueOutputBuffer(mDecoderBufferInfo, 10000); // 重新获取
}
}
/**
* 将PCM编码为AAC
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void PCMtoAAC() {
for (int i = 0; i < mEncoderInputBuffers.length - 1; i++) {
byte[] pcm = getPCMData();
if (pcm == null) {
break;
}
int inputBufferIndex = mMediaEncoder.dequeueInputBuffer(-1);
ByteBuffer inputBuffer = mEncoderInputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.limit(pcm.length);
inputBuffer.put(pcm); // PCM数据写入inputBuffer
// 通知MediaCodec编码
mMediaEncoder.queueInputBuffer(inputBufferIndex, 0, pcm.length, 0, 0);
}
int outputBufferIndex = mMediaEncoder.dequeueOutputBuffer(mEncoderBufferInfo, 10000);
while (outputBufferIndex >= 0) {
int outBitSize = mEncoderBufferInfo.size;
int outPacketSize = outBitSize + 7; // 7为ADTS头部的大小
ByteBuffer outputBuffer = mEncoderOutputBuffers[outputBufferIndex];
outputBuffer.position(mEncoderBufferInfo.offset);
outputBuffer.limit(mEncoderBufferInfo.offset + outBitSize);
byte[] aacBuffer = new byte[outPacketSize];
addADTStoPacket(aacBuffer, outPacketSize); // 添加ADTS
outputBuffer.get(aacBuffer, 7, outBitSize); // 将编码得到的AAC数据写入字节数组aacBuffer
outputBuffer.position(mEncoderBufferInfo.offset);
try {
bos.write(aacBuffer, 0, aacBuffer.length);
} catch (IOException e) {
e.printStackTrace();
}
mMediaEncoder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaEncoder.dequeueOutputBuffer(mEncoderBufferInfo, 10000);
}
}
private void putPCMData(byte[] pcm) {
try {
Log.v(TAG, "pcm size = " + pcm.length);
mPCMQueue.put(pcm);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private byte[] getPCMData() {
try {
if (mPCMQueue.isEmpty()) {
return null;
}
return mPCMQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
private void addADTStoPacket(byte[] packet, int packetLength) {
int profile = 2;
int freqIdx = mSampleRateType;
int chanCfg = 2;
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLength >> 11));
packet[4] = (byte) ((packetLength & 0x7FF) >> 3);
packet[5] = (byte) (((packetLength & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void release() {
if (mMediaDecoder != null) {
mMediaDecoder.stop();
mMediaDecoder.release();
mMediaDecoder = null;
}
if (mMediaEncoder != null) {
mMediaEncoder.stop();
mMediaEncoder.release();
mMediaEncoder = null;
}
if (mMediaDecoderExtractor != null) {
mMediaDecoderExtractor.release();
mMediaDecoderExtractor = null;
}
closeSilently(fos);
closeSilently(bos);
}
private void closeSilently(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static final class ADTSUtils {
private static Map<String, Integer> sSampleRateTypes;
static {
sSampleRateTypes = new HashMap<>();
sSampleRateTypes.put("96000", 0);
sSampleRateTypes.put("88200", 1);
sSampleRateTypes.put("64000", 2);
sSampleRateTypes.put("48000", 3);
sSampleRateTypes.put("44100", 4);
sSampleRateTypes.put("32000", 5);
sSampleRateTypes.put("24000", 6);
sSampleRateTypes.put("22050", 7);
sSampleRateTypes.put("16000", 8);
sSampleRateTypes.put("12000", 9);
sSampleRateTypes.put("11025", 10);
sSampleRateTypes.put("8000", 11);
sSampleRateTypes.put("7350", 12);
}
static int getSampleRateType(int sampleRate) {
return sSampleRateTypes.get(sampleRate + "");
}
}
private OnCodecProgressListener mOnCodecProgressListener;
public interface OnCodecProgressListener {
void onProgress(long currentFileLength, long targetFileTotalLength);
void onCompleted();
}
public void setOnCodecProgressListener(OnCodecProgressListener listener) {
this.mOnCodecProgressListener = listener;
}
}