0.前言
前面我们介绍了Android音视频开发框架的几个主要的组件,今天我们正式介绍Android的编解码组件MediaCodec。所谓编码,就是将原始的音视频帧进行压缩,转换成压缩后的数据帧,这类压缩数据无法直接使用,但便于传输和存储,因此,在播放时需要对其进行解压缩,这一过程即是解码。
1.MediaCodec
1.1 状态转移
MediaCodec的生命周期如图所示,一个Codec会处在Stopped
、Executing
、Released
三种基本状态之一。其中,Stopped
状态又分为Configured
、Uninitialized
、Error
三个子状态,而Executing
则分为Flushed
、Running
、End of Stream
,三种子状态。
当创建一个MediaCodec时,它就处于Uninitialized
状态,此时需要调用configure()
系列方法,设置对应的MediaFormat,输出的Surface等,调用configure之后就进入了Configured
状态。
在配置完之后调用start()
方法Codec进入到Executing
状态,此时即处于Flushed
子状态,并且所有的Buffer都由Codec掌控。等到应用层调用dequeueInputBuffer()
将第一个input buffer出队,此时Codec进入Running
子状态,之后持续地控制Buffer出入队进行编解码处理。直到将最后一个标记为EOS(end of stream)的buffer加入到队列后,此时Codec进入到End of Stream
状态,此时仍然可以产生output buffer,直到输出流也到了EOS。对于解码器而言,可以通过调用flush()
从Executing
的任意子状态跳转到Flushed
状态(对于编码器,行为可能未定义的)。
在Executing
下调用stop()
会返回到Uninitialized
状态,随即可以重新配置和运行。当不再使用Cdec时,可以调用release()
方法释放掉资源。
1.2 内存管理
MediaCodec的内存管理流程如上图所示,对于Buffer的创建和释放均由MediaCodec来管理。应用层使用MediaCodec的方式就是一个典型的"C-S"模式。
对于输入数据,应用层需要先向Codec申请一个空的inputBuffer,之后填充输入数据后,加入到Codec的输入队列中等待处理。之后Codec会异步地对数据进行处理。
对于输出数据,应用层可以通过同步请求/异步回调的方式,来获得一个已经填满输出数据的outputBuffer,之后使用buffer,并交由MediaCodec释放掉。
MediaCodec的buffer支持以下三种数据类型:压缩数据(Compressed Buffers),原始图像数据(Raw Video Buffers),原始音频数据(Raw Audio Buffers)。
一个具体的MediaCodec,要么是执行编码任务的Encoder,要么是执行解码任务的Decoder,对于这两种任务,输入输出数据是不同的类型。
1.2.1 压缩数据
压缩数据一般是作为解码器Decoder的输入,或者是Encoder的输出,也就是编码后的视频流/音频流数据,对应ffmpeg中的AVPacket结构。
根据输入的Codec所指定的format不同(可以由MediaExtractor得到),其中具体包含的数据类型也不同,如视频流的mime "video/avc",也就是H264流;如音频流的mime "audio/mp4a-latm",也是音频AAC流的编码的一种格式。
对于视频流,它一般是一帧单独的压缩后的视频帧,对于音频流,它一般包含几百毫秒的音频信号后的数据。
1.2.2 原始图像数据
原始的图像数据,一般是作为视频流Decoder的输出,以及视频流Encoder的输入,可以理解成ffmepg中的AVFrame结构,如H264编码时的YUV420P格式的图像数据。对于原始的图像数据,Codec也可以直接交给Surface进行渲染。
对于MediaCodec的图像数据参数,可以通过getCodecInfo()
来拿到对应的MediaCodecInfo,继而获取到它对应的能力信息,这部分可以参考前面一篇文章的内容。
1.2.3 原始音频数据
原始音频数据,一般是作为音频流Decoder的输出,以及音频流Encoder的输入,在ffmpeg中也是用AVFrame进行承载,其数据内容往往是PCM(Pulse Code Modulation, 脉冲编码调制)。
1.3 同步/异步使用方式
MediaCodec支持同步/异步调用的方式。
1.3.1 同步调用方式
同步模式下是使用dequeueInputBuffer()
和dequeueOutputBuffer()
来获得对应的输入和输出Buffer的索引。
java
// 输入 ---------------------------------------
// 同步接口,获取inputBuffer的id
public int dequeueInputBuffer(long timeoutUs);
// 拿到对应的inputIndex对应的ByteBuffer
public ByteBuffer getInputBuffer(int index)
// 填充完ByteBuffer后,将buffer加入到Codec的队列中等待处理
public void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
// 输出 ---------------------------------------
// 同步接口,获取outputBuffer的id,info
public int dequeueOutputBuffer(@NonNull BufferInfo info, long timeoutUs)
// 拿到对应的outputIndex对应的ByteBuffer
public ByteBuffer getOutputBuffer(int index)
// 释放outputBuffer,如果有设置Surface,也可以设置render = true,渲染到Surface上
public void releaseOutputBuffer(int index, boolean render)
以下是官方文档上的一个同步接口的使用示例:
java
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, ...);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(...);
// fill inputBuffer with valid data
...
codec.queueInputBuffer(inputBufferId, ...);
}
int outputBufferId = codec.dequeueOutputBuffer(...);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
...
codec.releaseOutputBuffer(outputBufferId, ...);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
1.3.2 异步调用方式
对于异步调用方式,需要在codec开始处理前设置对应的回调方法来获得buffer的index。
java
// 设置异步回调
public void setCallback(@Nullable Callback cb);
// 异步回调的接口
public abstract static class Callback {
public Callback();
// 当inputBuffer就绪时回调,返回mediaCodec结构,和对应的inputBufferIndex
public abstract void onInputBufferAvailable(MediaCodec mc, int inputBufferId);
// 当outputBuffer就绪时回调
public abstract void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, BufferInfo info);
// 错误时回调
public abstract void onError(@NonNull MediaCodec var1, @NonNull CodecException var2);
// 如果buffer是有加密的,可以配置一个MediaCrypto解密,一般没用上。
// 解密失败时回调
public void onCryptoError(@NonNull MediaCodec codec, @NonNull CryptoException e);
// 当输出的运行时format改变时回调
public abstract void onOutputFormatChanged(@NonNull MediaCodec var1, @NonNull MediaFormat var2);
}
同样地,官方文档上对于异步模式的使用示例
java
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
...
codec.queueInputBuffer(inputBufferId, ...);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, ...) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
...
codec.releaseOutputBuffer(outputBufferId, ...);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(...) {
...
}
@Override
void onCryptoError(...) {
...
}
});
codec.configure(format, ...);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
2. 例子
一个使用MediaCodec解码,并在Surface上显示的例子,包含同步和异步两个方式。
java
public class MediaCodecActivity extends AppCompatActivity {
final String TAG = "MediaCodecActivity";
final int MAX_SIZE = -1;
int frameIdx = 0;
Button btnCodecList;
Button btnDecodeAsync;
Button btnDecodeSync;
SurfaceView surfaceView;
SurfaceHolder surfaceHolder;
long lastFrameTimeMs = -1;
// MediaCodec
MediaCodec mediaCodec;
MediaExtractor extractor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_codec);
initView();
}
private void initView() {
btnCodecList = findViewById(R.id.btn_show_codec_list);
btnCodecList.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showCodecList();
}
});
btnDecodeAsync = findViewById(R.id.btn_decode_async);
btnDecodeAsync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
codecTestAsync();
}
});
btnDecodeSync = findViewById(R.id.btn_decode_sync);
btnDecodeSync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Thread thread = new Thread() {
@Override
public void run() {
super.run();
codecTestSync();
}
};
thread.start();
}
});
surfaceView = findViewById(R.id.surface_media_codec);
surfaceHolder = surfaceView.getHolder();
}
// 展示所有的Codec
protected void showCodecList() {
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for (int i = 0; i < infos.length; ++i) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.i(TAG, i + " : " + infos[i].getName() + " , hw : " + infos[i].isHardwareAccelerated());
String[] supportTypes = infos[i].getSupportedTypes();
for(String type : supportTypes) {
MediaCodecInfo.CodecCapabilities capabilities = infos[i].getCapabilitiesForType(type);
String mime = capabilities.getMimeType();
Log.i(TAG, " - mime : " + mime);
MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
if(videoCapabilities != null) {
Range<Integer> fpsRange = videoCapabilities.getSupportedFrameRates();
Log.i(TAG, " - fps : [" + fpsRange.getLower() + ", " + fpsRange.getUpper() + "]");
Range<Integer> bitRateRange = videoCapabilities.getBitrateRange();
Log.i(TAG, " - bitrate : [" + bitRateRange.getLower() + ", " + bitRateRange.getUpper() + "]");
}
}
}
}
}
// 异步解码方式
protected void codecTestAsync() {
String filename = Environment.getExternalStorageDirectory().getPath() + "/" + getString(R.string.test_file);
try {
// demuxer
extractor = new MediaExtractor();
extractor.setDataSource(filename);
int i = 0;
String mime = "";
MediaFormat format = null;
for(; i < extractor.getTrackCount(); ++i) {
format = extractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if(mime.startsWith("video")) {
break;
}
}
extractor.selectTrack(i);
int frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
long frameIntervalMs = (long) (1000.0 / frameRate);
// decoder
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, surfaceHolder.getSurface(), null, 0);
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) {
ByteBuffer buffer = mediaCodec.getInputBuffer(i);
int bufferSize = extractor.readSampleData(buffer, 0);
if(frameIdx == MAX_SIZE || bufferSize <= 0) {
mediaCodec.queueInputBuffer(i, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
return;
}
mediaCodec.queueInputBuffer(i, 0, bufferSize, extractor.getSampleTime() * 1000, 0);
extractor.advance();
++frameIdx;
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int i, @NonNull MediaCodec.BufferInfo bufferInfo) {
if(frameIdx == MAX_SIZE && ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)) {
return;
}
long currentTimeMs = System.currentTimeMillis();
long delay = frameIntervalMs - (currentTimeMs - lastFrameTimeMs);
if(delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Log.e(TAG, "Exception : " + e.toString());
}
}
lastFrameTimeMs = currentTimeMs;
mediaCodec.releaseOutputBuffer(i, true);
}
@Override
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
}
});
mediaCodec.start();
} catch (Exception e) {
Log.i(TAG, "Exception : " + e);
}
}
// 同步解码方式
protected void codecTestSync() {
String filename = Environment.getExternalStorageDirectory().getPath() + "/" + getString(R.string.test_file);
try {
// demuxer
extractor = new MediaExtractor();
extractor.setDataSource(filename);
int i = 0;
String mime = "";
MediaFormat format = null;
for (; i < extractor.getTrackCount(); ++i) {
format = extractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video")) {
break;
}
}
extractor.selectTrack(i);
int frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
long frameIntervalMs = (long) (1000.0 / frameRate);
// decoder
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, surfaceHolder.getSurface(), null, 0);
mediaCodec.start();
boolean isEof = false;
while(!isEof) {
// Input
int inputIdx = -1;
if((inputIdx = mediaCodec.dequeueInputBuffer(-1)) >= 0) {
ByteBuffer buffer = mediaCodec.getInputBuffer(inputIdx);
int sampleSize = extractor.readSampleData(buffer, 0);
if(sampleSize > 0) {
mediaCodec.queueInputBuffer(inputIdx, 0, sampleSize, extractor.getSampleTime() * 1000, extractor.getSampleFlags());
extractor.advance();
} else {
mediaCodec.queueInputBuffer(inputIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEof = true;
}
}
// Output
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outputIdx = -1;
// 超时10ms
outputIdx = mediaCodec.dequeueOutputBuffer(info, 10000);
if(outputIdx >= 0) {
long currentTimeMs = System.currentTimeMillis();
long delay = frameIntervalMs - (currentTimeMs - lastFrameTimeMs);
if(delay > 0) {
Thread.sleep(delay);
}
lastFrameTimeMs = currentTimeMs;
mediaCodec.releaseOutputBuffer(outputIdx, true);
}
}
} catch (Exception e) {
Log.i(TAG, "Exception : " + e);
}
}
@Override
protected void onDestroy() {
if(mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if(extractor != null) {
extractor.release();
}
super.onDestroy();
}
}
界面如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MediaCodecActivity"
android:orientation="vertical">
<Button
android:id="@+id/btn_show_codec_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="CodecList"
/>
<Button
android:id="@+id/btn_decode_async"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Decode Async"
/>
<Button
android:id="@+id/btn_decode_sync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Decode Sync"
/>
<SurfaceView
android:id="@+id/surface_media_codec"
android:layout_width="match_parent"
android:layout_height="300dp"
/>
</LinearLayout>
显示效果: