音视频开发之旅(六)MediaCodec工作原理、流程与实践
目录
- MediaCodec介绍
- 工作原理和基本流程
- 数据格式
- 生命周期
- 同步和异步模式
- 流控
- AAC解码为PCM同步和异步的两种实践
- 遇到的问题
- 参考
- 收获
一、介绍
Android底层多媒体模块采用的是OpenMax框架,实现方都要遵循OpenMax标准。Google默认提供了一系列的软编软解的实现,而硬编硬解则由芯片厂商完成,所以不同芯片的手机,硬编硬解的实现和性能是会有差异的。比如我手机的编解码实现部分如下
MediaCodec提供了一套访问底层多媒体模块的接口共应用层实现编解码功能。
二、工作原理和基本流程
MediaCodec使用的基本流程如下:
- createByCodeName/createEncoderByType/createDecoderByType: (静态工厂构造MediaCodec对象)--Uninitialized状态
- configure: (配置) -- configure状态
- start (启动)--进入Running状态
- while(1) {
try{
- dequeueInputBuffer (从编解码器获取输入缓冲区buffer)
- queueInputBuffer (buffer被生成方client填满之后提交给编解码器)
- dequeueOutputBuffer (从编解码器获取输出缓冲区buffer)
- releaseOutputBuffer (消费方client消费之后释放给编解器)
} catch(Error e){
- error (出现异常 进入error状态)
}
}
- stop (编解码完成后,释放codec)
- release
基本流程结合代码两张Buffer队列示意图和生命周期图一起看,整个流程就会很清晰。
下面我们重点看下核心的部分Buffer队列的操作。
MediaCodec采用了2个缓冲区队列(inputBuffer和outputBuffer),异步处理数据,
1. 数据生成方(左侧Client)从input缓冲队列申请empty buffer—》dequeueinputBuffer
2. 数据生产方(左侧Client)把需要编解码的数据copy到empty buffer,然后繁缛到input缓冲队列 —》queueInputBuffer
3. MediaCodec从input缓冲区队列中取一帧进行编解码处理
4. 编解码处理结束后,MediaCodec将原始inputbuffer置为empty后放回左侧的input缓冲队列,将编解码后的数据放入到右侧output缓冲区队列
5. 消费方Client(右侧Client)从output缓冲区队列申请编解码后的buffer —》dequeueOutputBuffer
6. 消费方client(右侧Client)对编解码后的buffer进行渲染或者播放
7. 渲染/播放完成后,消费方Client再将该buffer放回到output缓冲区队列 —》releaseOutputBuffer
三、数据格式
Mediacodec接受三种数据格式和两种载体 分别如下:
压缩数据、原始音频数据和原始视频数据
以Surface作为载体或者ByteBuffer作为载体
- 压缩数据
压缩数据可以作为解码器的输入、编码器的输出,需要指定数据的格式,这样codec才知道如何处理这些压缩数据 - 原始音频数据 — PCM音频数据帧
- 原始视频数据
视频看解码支持的常用的色彩格式有 native raw video format和 flexible YUV buffers
native raw video format : COLOR_FormatSurface,可以用来处理surface模式的数据输入输出。
flexible YUV buffers : 例如COLOR_FormatYUV420Flexible,可以用来处理surface模式的输出输出,在使用ByteBuffer模式的时候可以用getInput/OutputImage(int)方法来获取image数据。
四、生命周期
MediaCodec生命周期状态分为三种 Stopped、Executing和Released
在上面第一部分工作原理和基本流程中我们也简单提到了,下面我们详细说下
其中Stopped包含三种子状态 Uninitialized(为初始化状态)、Configured(已配置状态)、Error(异常状态)
Executing也包含三个子状态 Flushed(刷新状态)、Running(运行状态)和EOS(流结束状态)
我们重点看下Executing状态
在调用mediacodec.start()方法后编解码器立即进入Executing状态的Flush子状态,此时编解码器会拥有所有的inputBuffer
一旦第一个输入缓存inputbuffer被移出队列(即:queueInputBuffer),编解码器转为Running状态,编解码器的大部分生命周期会在此状态下。
当带有end-of-stream标记的inputBuffer入队列时(queueInputBuffer),编解码器将转入EOS状态。在这种状态下,编解码器不再接收新的inputBuffer,但是仍然产生outputBuffer,知道end-of-stream标记到达输出端。
可以在Executiong下的任何时候调用flush()使编解码器重新回到Flushed状态。
五、同步异步模式
Buffer的生产消费有种模式,一种是同步模式,即本文第一部分介绍的流程方式。从android5.0 google推出了异步模式,通过给codec设置回调setCalback进行buffer的生产消费操作。
(img)
官方给出的典型代码如下:
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(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
六、MediaCodec 流控
编码器可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,在编码过程中实际可以控制的并不是最终的输出码率,而是编码过程中的一个量化参数(Quantiaztion Parameter QP),它和码率并没有固定的关系,而是取决于图像内容。
android码率控制有两种模式
一种是设置cofigure时设定目标码率和码率控制模式,
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mVideoCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
码率控制模式有三种:
CQ 表示完全不控制码率,尽最大可能保证图像质量, 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择这种策略;
CBR 表示编码器会尽量把输出码率控制为设定值,输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择;
VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低,优点是稳定可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般使用的是CBR;
另一种是动态的调整目标码率。
Bundle param = new Bundle();
param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
mediaCodec.setParameters(param);
七、把AAC转码成PCM (音频解码)
目的:通过该功能的实践,熟悉mediaCodec的流程,以及同步和异步两种方式实现
具体实现如下:
package com.av.mediajourney.mediacodec;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class AACToPCMDelegate {
private static final String TAG = "AACToPCMDelegate";
private static final long TIMEOUT_US = 0;
private Context mContext;
private ByteBuffer[] inputBuffers;
private ByteBuffer[] outputBuffers;
private MediaExtractor mediaExtractor;
private MediaCodec decodec;
private FileOutputStream fileOutputStream;
public AACToPCMDelegate(MediaCodecActivity context) {
this.mContext = context;
}
/**
* 通过aacToPCM 熟悉mediaCodec的流程,以及通过同步和异步两种方式实现
*/
void aacToPCM() {
boolean isAsync = true;
if (isAsync && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
isAsync = false;
}
//1. initFile
File file = initFile(isAsync);
if (file == null) {
return;
}
//2. 初始化mediaCodec
initMediaCodec(file.getAbsolutePath(), isAsync);
if (decodec == null) {
Toast.makeText(mContext, "decodec is null", Toast.LENGTH_SHORT).show();
return;
}
if (!isAsync) {
//同步处理
decodecAacToPCMSync();
}
}
private File initFile(boolean isAsync) {
String child = isAsync ? "aacToPcmAsync.pcm" : "aacToPcmSync.pcm";
File outputfile = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC), child);
if (outputfile.exists()) {
outputfile.delete();
}
try {
fileOutputStream = new FileOutputStream(outputfile.getAbsolutePath());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
File file = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC), "sanguo.aac");
if (!file.exists()) {
Toast.makeText(mContext, "文件不存在", Toast.LENGTH_SHORT).show();
return null;
}
return file;
}
private void initMediaCodec(String path, boolean isASync) {
mediaExtractor = new MediaExtractor();
try {
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
String mime = trackFormat.getString(MediaFormat.KEY_MIME);
if (TextUtils.isEmpty(mime)) {
continue;
}
Log.i(TAG, "initMediaCodec: mime=" + mime);
if (mime.startsWith("audio/")) {
mediaExtractor.selectTrack(i);
}
//生成MediaCodec,此时处于Uninitialized状态
decodec = MediaCodec.createDecoderByType(mime);
//configure 处于Configured状态
decodec.configure(trackFormat, null, null, 0);
if (isASync) {
setAsyncCallBack();
}
//处于Excuting状态 Flushed子状态
decodec.start();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
inputBuffers = decodec.getInputBuffers();
outputBuffers = decodec.getOutputBuffers();
Log.i(TAG, "initMediaCodec: inputBuffersSize=" + inputBuffers.length + " outputBuffersSize=" + outputBuffers.length);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 成功输出到目标文件
* 到处生成的pcm,用ffplay播放pcm文件 发现和之前的aac是一样的
* ffplay -ar 44100 -channels 2 -f s16le -i /Users/yabin/Desktop/tmp/aacToPcm.pcm
*/
private void decodecAacToPCMSync() {
boolean isInputBufferEOS = false;
boolean isOutPutBufferEOS = false;
while (!isOutPutBufferEOS) {
if (!isInputBufferEOS) {
//1. 从codecInputBuffer中拿到empty input buffer的index
int index = decodec.dequeueInputBuffer(TIMEOUT_US);
if (index >= 0) {
ByteBuffer inputBuffer;
//2. 通过index获取到inputBuffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = decodec.getInputBuffer(index);
} else {
inputBuffer = inputBuffers[index];
}
if (inputBuffer != null) {
Log.i(TAG, "decodecAacToPCMSync: " + " index=" + index);
inputBuffer.clear();
}
//extractor读取sampleData
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
//3. 如果读取不到数据,则认为是EOS。把数据生产端的buffer 送回到code的inputbuffer
Log.i(TAG, "decodecAacToPCMSync: sampleSize=" + sampleSize);
if (sampleSize < 0) {
isInputBufferEOS = true;
decodec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
decodec.queueInputBuffer(index, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
//读取下一帧
mediaExtractor.advance();
}
}
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//4. 数据消费端Client 拿到一个有数据的outputbuffer的index
int index = decodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (index < 0) {
continue;
}
ByteBuffer outputBuffer;
//5. 通过index获取到inputBuffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = decodec.getOutputBuffer(index);
} else {
outputBuffer = outputBuffers[index];
}
Log.i(TAG, "decodecAacToPCMSync: outputbuffer index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
//把数据写入到FileOutputStream
byte[] bytes = new byte[bufferInfo.size];
outputBuffer.get(bytes);
try {
fileOutputStream.write(bytes);
fileOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
//6. 然后清空outputbuffer,再释放给codec的outputbuffer
decodec.releaseOutputBuffer(index, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
isOutPutBufferEOS = true;
}
}
close();
}
private void close() {
mediaExtractor.release();
decodec.stop();
decodec.release();
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void setAsyncCallBack() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
decodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
Log.i(TAG, "setAsyncCallBack - onInputBufferAvailable: index=" + index);
//1. 从codecInputBuffer中拿到empty input buffer的index
if (index >= 0) {
ByteBuffer inputBuffer;
//2. 通过index获取到inputBuffer
inputBuffer = decodec.getInputBuffer(index);
if (inputBuffer != null) {
Log.i(TAG, "setAsyncCallBack- onInputBufferAvailable: " + " index=" + index);
inputBuffer.clear();
}
//extractor读取sampleData
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
//3. 如果读取不到数据,则认为是EOS。把数据生产端的buffer 送回到code的inputbuffer
Log.i(TAG, "setAsyncCallBack- onInputBufferAvailable: sampleSize=" + sampleSize);
if (sampleSize < 0) {
decodec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
decodec.queueInputBuffer(index, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
//读取下一帧
mediaExtractor.advance();
}
}
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo bufferInfo) {
Log.i(TAG, "setAsyncCallBack - onOutputBufferAvailable: index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
//4. 数据消费端Client 拿到一个有数据的outputbuffer的index
if (index >= 0) {
ByteBuffer outputBuffer;
//5. 通过index获取到inputBuffer
outputBuffer = decodec.getOutputBuffer(index);
Log.i(TAG, "setAsyncCallBack - onOutputBufferAvailable: outputbuffer index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
//把数据写入到FileOutputStream
byte[] bytes = new byte[bufferInfo.size];
outputBuffer.get(bytes);
try {
fileOutputStream.write(bytes);
fileOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
//6. 然后清空outputbuffer,再释放给codec的outputbuffer
decodec.releaseOutputBuffer(index, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
close();
}
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
Log.e(TAG, "setAsyncCallBack - onError: ");
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
Log.i(TAG, "setAsyncCallBack - onOutputFormatChanged: ");
}
});
}
}
}
八、遇到的问题
java.lang.IllegalStateException
at android.media.MediaCodec.getBuffer(Native Method)
at android.media.MediaCodec.getInputBuffer(MediaCodec.java:3246)
int index = decodec.dequeueInputBuffer(TIMEOUT_US); _
之后直接进行了inputbuffer的处理,而没有判断index是否有效(index>=0)
九、参考
十、收获
强烈建议从示例代码开始了解MediaCodec,而不是试图从文档把它搞清楚。
- 了解了MediaCodec工作原理和基本流程
- 生命周期的应用了解
- codec的同步和异步的使用和场景
- 流控的设置
- 通过AAC解码为PCM,同步和异步两种实现逐步理解原理流程
- 遇到的问题总结复盘
感谢你的阅读。
下一篇我们接学习实践SurfaceView 、GLSurfaceView、TextureView 、SurfaceTexture、Surface,了了它们的关系,使用方法场景和优缺点。
欢迎关注“音视频开发之旅”,你的“再看”、“点赞”、“分享”都是莫大的支持。我们下篇见。
欢迎讨论