MediaCodec硬解流程
一 MediaCodec概述
MediaCodec
是Android 4.1(api 16)版本引入的低层编解码接口,同时支持音视频的编码和解码。通常与MediaExtractor
、MediaMuxer
、AudioTrack
结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。MediaCodec
在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据。
1 数据格式
mediacodec的作用是处理输入的数据生成输出数据,有两种输入输出模式:
-
surface模式-------输入/输出以surface作为源
-
ByteBuffer模式--------输入/输出时以ByteBuffer作为源
MediaCodec
接受三种数据格式:压缩数据,原始音频数据和原始视频数据。
这三种数据都可以使用ByteBuffer
作为载体传输给MediaCodec
来处理。但是当使用原视频数据时,最好采用Surface
作为输入源来替代ByteBuffer
,这样效率更高,效果更好,因为surface
使用的更底层的视频数据,不会映射或者复制到ByteBuffer
缓冲区。在使用surface
作为输入源时,开发者不能访问到到原始视频数据,但是可以使用ImageReader
来获取到原始未加密的视频数据,这个地方我理解的是imagereader
的工作流程是接受自己的surface
数据来生成image
,将imagereader
的surface
传给mediacodec
作为解码器的输出surface
,就可以访问解码的数据,但是必须是未加密的,这种方式同样比使用ByteBuffer
更快,因为native
缓冲区会直接映射到directbytebuffer
区域,这是一块native
和java
共享的缓冲区。当使用ByteBuffer
模式时可以使用Image来获取原始视频数据,MediaCodec
提供了两个方法,getInput/OutputImage(int)
。
例如MediaCodec
解码H264数据,我们必须将分割符和NALU
单元作为一个完整的数据帧传给解码器才能正确解码,除非是标记了BUFFER_FLAG_PARTIAL_FRAME
的数据,这种方式不常用。
注:客户端处理完数据后,必须手动释放output
缓冲区,否则将会导致MediaCodec
输出缓冲被占用,无法继续解码。
MediaCodec
状态图,整体上分为三个大的状态:Sotpped
、Executing
、Released
。
Stoped
:包含了3个小状态:Error
、Uninitialized
、Configured
。
首先,新建MediaCodec
后,会进入Uninitialized
状态;
其次,调用configure
方法配置参数后,会进入Configured
;
Executing
:同样包含3个小状态:Flushed
、Running
、End of Stream
。
再次,调用start
方法后,MediaCodec
进入Flushed
状态;
接着,调用dequeueInputBuffer
方法后,进入Running
状态;
最后,当解码/编码结束时,进入End of Stream(EOF)
状态。
这时,一个视频就处理完成了。
Released
:最后,如果想结束整个数据处理过程,可以调用release
方法,释放所有的资源。
那么,Flushed
是什么状态呢?
从图中我们可以看到,在Running
或者End of Stream
状态时,都可以调用flush
方法,重新进入Flushed
状态。
当我们在解码过程中,进入了End of Stream
后,解码器就不再接收输入了,这时候,需要调用flush
方法,重新进入接收数据状态。
或者,我们在播放视频过程中,想进行跳播,这时候,我们需要Seek
到指定的时间点,这时候,也需要调用flush
方法,清除缓冲,否则解码时间戳会混乱。
二 MediaCodec用法
Android的硬解码接口MediaCodec
只能接收Annex-B
格式的H.264
数据,而iOS平台的VideoToolBox
则相反,只支持AVCC格式。这就导致:
在Android平台硬解播放flv/mp4/mkv
等封装的视频时,需要将AVCC
格式的extradata
以及NALU
数据转为Annex-B
格式;
在iOS平台播放ts
或ts
切片的hls
视频时,需要将Annex-B
格式的SPS/PPS NALU
转为AVCC
格式的extradata
,以及将其他以size方式分割的NALU
转为start code
方式。
初始化解码器,除了配置输入视频流的的编码格式、宽高以及输出格式之外,还需要配置一些额外的信息。 对于H.264
视频,需要填充Annex-B
格式的SPS/PPS
信息。
基本流程:
- 创建和配置MediaCodec对象
- 进行以下循环:
- 如果一个输入缓冲区准备好:
- 读取部分数据,复制到缓冲区
- 如果一个输出缓冲区准备好:
- 复制到缓冲区
- 销毁MediaCodec对象
MediaCodec API接口:
//根据视频编码创建解码器,这里是解码AVC编码的视频
MediaCodec mediaCodec =MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
//创建视频格式信息
MediaFormat mediaFormat = MediaFormat.createVideoFormat(mimeType, width, height);
//配置
mediaCodec.configure(mediaFormat, surfaceView.getHolder().getSurface(), null, 0);
mediaCodec.start();
//停止解码,此时可以再次调用configure()方法
mediaCodec.stop();
//释放内存
mediaCodec.release();
//一下是循环解码接口
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
queueInputBuffer:输入流入队列
dequeueInputBuffer:从输入流队列中取数据进行编码操作
getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
releaseOutputBuffer:处理完成,释放ByteBuffer数据
创建编/解码器
MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)两个方法来创建编解码器,它们均需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式如下:
● “video/x-vnd.on2.vp8” - VP8 video (i.e. video in .webm)
● “video/x-vnd.on2.vp9” - VP9 video (i.e. video in .webm)
● “video/avc” - H.264/AVC video
● “video/mp4v-es” - MPEG4 video
● “video/3gpp” - H.263 video
● “audio/3gpp” - AMR narrowband audio
● “audio/amr-wb” - AMR wideband audio
● “audio/mpeg” - MPEG1/2 audio layer III
● “audio/mp4a-latm” - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● “audio/vorbis” - vorbis audio
● “audio/g711-alaw” - G.711 alaw audio
● “audio/g711-mlaw” - G.711 ulaw audio
MediaCodec还提供了createByCodecName (String name)方法,支持使用组件的具体名称来创建编解码器。但是该方法使用起来有些麻烦,且官方是建议最好是配合MediaCodecList使用,因为MediaCodecList记录了所有可用的编解码器。
配置编/解码器
编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native-configure实现对编解码器的配置工作。在配置时,configure方法需要传入format、surface、crypto、flags参数,其中format为MediaFormat的实例,它使用”key-value”键值对的形式存储多媒体数据格式信息;surface用于指明解码器的数据源来自于该surface;crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。
MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480); // 创建MediaFormat
mFormat.setInteger(MediaFormat.KEY_BIT_RATE,600); // 指定比特率
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30); // 指定帧率
mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat); // 指定编码器颜色格式
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,10); // 指定关键帧时间间隔
mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
//码率控制模式有三种:
CQ 表示完全不控制码率,尽最大可能保证图像质量;
CBR 表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”;
VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;
// extradata中是Annex-B格式的SPS、PPS NALU数据
//SPS设为"csd-0", PPS设为"csd-1"
mediaFormat.setByteBuffer("csd-0", extradata);
// ...
mediaCodec.configure(mediaFormat, surface, 0, 0);
// ...
对于mp4/flv/mkv等封装,我们得到的是AVCC格式的extradata,需要先将该extradata转换为Annex-B格式的两个NALU, 然后用startcode进行分割。
///
如果编解码音频数据,则调用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法
Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:
原始数据 编码器
NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar
NV21 ———-> COLOR_FormatYUV420SemiPlanar
YV12(I420) ———-> COLOR_FormatYUV420Planar
启动编/解码器
当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。start()方法源码如下:
public final void start() {
native_start();
synchronized(mBufferLock) {
cacheBuffers(true /* input */);
cacheBuffers(false /* input */);
}
}
数据处理
MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous。本文主要介绍用得较多的同步编解码。当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。下面我们通过一段官方提供的代码,进行扩展分析:
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();
从上面代码可知,当编解码器start后,会进入一个for(;;)循环,该循环是一个死循环,以实现不断地去从编解码器的输入缓存池中获取包含数据的一个缓存区,然后再从输出缓存池中获取编解码好的输出数据。
AAC解码为PCM的示例
package com.maniu.h264player;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class AACToPCM {
private static final String TAG = "AACToPCM";
public static final int ERROR_INPUT_INVALID = 100;
public static final int ERROR_OUTPUT_FAILED = 200;
public static final int ERROR_OPEN_CODEC = 300;
public static final int OK = 0;
private static final int TIMEOUT_USEC = 0;
private MediaExtractor mExtractor;
private MediaFormat mFormat;
private MediaCodec mDecoder;
private FileOutputStream mFos;
private ByteBuffer[] mInputBuffers;
private ByteBuffer[] mOutputBuffers;
private boolean mDecodeEnd;
public AACToPCM() {}
private int checkPath(String path) {
if (path == null || path.isEmpty()) {
Log.d(TAG, "invalid path, path is empty");
return ERROR_INPUT_INVALID;
}
File file = new File(path);
if (!file.isFile()) {
Log.d(TAG, "path is not a file, path:" + path);
return ERROR_INPUT_INVALID;
} else if (!file.exists()) {
Log.d(TAG, "file not exists, path:" + path);
return ERROR_INPUT_INVALID;
} else {
Log.d(TAG, "path is a file, path:" + path);
}
return OK;
}
public int decodeAACToPCM(String audioPath, String pcmPath) {
int ret;
if (OK != (ret = openInput(audioPath))) {
return ret;
}
if (OK != (ret = openOutput(pcmPath))) {
return ret;
}
if (OK != (ret = openCodec(mFormat))) {
return ret;
}
mDecodeEnd = false;
while (!mDecodeEnd) {
if (OK != (ret = decode(mDecoder, mExtractor))) {
Log.d(TAG, "decode failed, ret=" + ret);
break;
}
}
close();
return ret;
}
private int decode(MediaCodec codec, MediaExtractor extractor) {
Log.d(TAG, "decode");
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_USEC);
if (inputIndex >= 0) {
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
inputBuffer = codec.getInputBuffer(inputIndex);
} else {
inputBuffer = mInputBuffers[inputIndex];
}
inputBuffer.clear();
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {//read end
codec.queueInputBuffer(inputIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inputIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {//TIMEOUT
Log.d(TAG, "INFO_TRY_AGAIN_LATER");//TODO how to declare this info
return OK;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.d(TAG, "output format changed");
return OK;
} else if (outputIndex < 0) {
Log.d(TAG, "outputIndex=" + outputIndex);
return OK;
} else {
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = codec.getOutputBuffer(outputIndex);
} else {
outputBuffer = mOutputBuffers[outputIndex];
}
byte[] buffer = new byte[bufferInfo.size];
outputBuffer.get(buffer);
try {
Log.d(TAG, "output write, size="+ bufferInfo.size);
mFos.write(buffer);
mFos.flush();
} catch (IOException e) {
e.printStackTrace();
return ERROR_OUTPUT_FAILED;
}
codec.releaseOutputBuffer(outputIndex, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mDecodeEnd = true;
}
}
return OK;
}
private int openCodec(MediaFormat format) {
Log.d(TAG, "openCodec, format mime:" + format.getString(MediaFormat.KEY_MIME));
try {
mDecoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
} catch (IOException e) {
e.printStackTrace();
return ERROR_OPEN_CODEC;
}
mDecoder.configure(format, null, null, 0);
mDecoder.start();
if (Build.VERSION.SDK_INT < 21) {
mInputBuffers = mDecoder.getInputBuffers();
mOutputBuffers = mDecoder.getOutputBuffers();
}
return OK;
}
private int openInput(String audioPath) {
Log.d(TAG, "openInput audioPath:" + audioPath);
int ret;
if (OK != (ret = checkPath(audioPath))) {
return ret;
}
mExtractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
mExtractor.setDataSource(audioPath);
for (int i = 0; i < mExtractor.getTrackCount(); ++i) {
MediaFormat format = mExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
Log.d(TAG, "mime=" + mime);
if (mime.startsWith("audio/")) {
audioTrack = i;
hasAudio = true;
mFormat = format;
break;
}
}
if (!hasAudio) {
Log.d(TAG, "input contain no audio");
return ERROR_INPUT_INVALID;
}
mExtractor.selectTrack(audioTrack);
} catch (IOException e) {
return ERROR_INPUT_INVALID;
}
return OK;
}
private int openOutput(String outputPath) {
Log.d(TAG, "openOutput outputPath:" + outputPath);
try {
mFos = new FileOutputStream(outputPath);
} catch (IOException e) {
return ERROR_OUTPUT_FAILED;
}
return OK;
}
private void close() {
mExtractor.release();
mDecoder.stop();
mDecoder.release();
try {
mFos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
原文链接:https://ffmpeg.0voice.com/forum.php?mod=viewthread&tid=567&extra=
原文链接:https://blog.csdn.net/wangbuji/article/details/125315474
android硬编解码MediaCodec
[Android 进阶]MediaCodec系列之MediaCodec简介
MediaCodec专题(一):简介
MediaCodec专题(二):使用
NDK中使用 MediaCodec 编解码视频
MediaCodec的使用介绍
Android平台MediaCodec避坑指北