MediaCodeC是Android 4.1(API16 ) 版本加入的一个新的音视频处理API,旨在提高Android平台的音视频编码能力,Mediacodec类可用于访问底层的媒体编解码器,即编码器/解码器组件。这是Android底层的多媒体支持基础设施的一部分(通常与 MediaExtractor
, MediaSync
, MediaMuxer
, MediaCrypto
, MediaDrm
, Image
, Surface
, AudioTrack
)
MediaCodeC工作流程:
MediaCodeC 状态机
codec
(编解码器)通过异步的方式对输入的数据进行处理,输出处理后的数据,过程中需要一系列的输入/输出Buffers
。最简单的情况下,先把数据放进一个空的输入buffer(申请或者获取得到)中,发送给codec,codec对数据进行处理转换后放进一个输出buffer中,拿到输出buffer后,自行处理输出buffer中的数据,之后释放输出buffer并返回给codec。
使用MediaCodeC需要注意几点:
1.MediaCodeC
是4.1(API16)才开始引入的,后续的几个版本中API不断更改API,(大部分核心API都会发现标记为过时,需要更换新的版本) , 5.0才加入了异步模式,一般来说,5.0以下是同步获取的,需要准备一个缓冲区不断的刷新请求MediaCodeC获取解码后的数据(后面有详细代码示例);
2.MediaCodeC
由于众所周知的原因,Google并没有预制很多解码器,相反,解码器都是后期手机厂商自己register进去的,MediaCodeC只提供抽象的接口,具体实现在各个手机上,当然,Android源码里面引入了一套AAC编码器(因为AAC是开放标准的 ),而大家常用的MP3是有版权的,所以原生MediaCodeC无法编码MP3,除非厂商提供MP3编码器)
3.MediaCodeC
适用于音视频编/解码,其底层最后调用的是 OpenGL ES,最终调用手机 GPU 进行工作,是 Android 提供给我们的一种音视频编解码高级 API,所以编/解码效率跟手机CPU 有很大的关系,经多台测试机调查得知,高通的 CPU 明显有优势,同时,手机性能有限,编/解码需要耗费大量的内存及 cpu 时间,亲测骁龙801解码一个5分钟的音频,大概花费了30S(慢的不能忍受是吧,兄弟,你还没测试过联发科(MTK )呢,那才叫一个慢),所以,能不在手机上做编/解码工作,尽量不要做(那不废话吗,不然我讲啥!!)
那如何查看自己的手机自带哪些解码器呢:
在我们手机system/etc/目录下,有一个media_codecs.xml文件,里面记录了手机厂商在出厂时配置的音视频软、硬件解码库及其支持的媒体编码格式。我们可以使用MediaCodecList相关的api来查看是否支持;
5.0(API16)以上,你可以这样获取
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void checkMediaDecoder() {
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] codecInfos= mediaCodecList.getCodecInfos();
for (MediaCodecInfo codecInfo : codecInfos) {
Log.i("TAG", "codecInfo =" + codecInfo.getName());
}
}
打印出(HTC E9pw):
codecInfo =OMX.MTK.AUDIO.DECODER.MP3
codecInfo =OMX.google.opus.decoder
codecInfo =OMX.MTK.AUDIO.DECODER.ADPCM.MS
codecInfo =OMX.MTK.AUDIO.DECODER.ADPCM.DVI
codecInfo =OMX.MTK.AUDIO.DECODER.GSM
codecInfo =OMX.MTK.AUDIO.DECODER.RAW
codecInfo =OMX.MTK.AUDIO.DECODER.G711.ALAW
codecInfo =OMX.MTK.AUDIO.DECODER.G711.MLAW
codecInfo =OMX.MTK.AUDIO.DECODER.FLAC
codecInfo =OMX.MTK.AUDIO.DECODER.WMA
codecInfo =OMX.MTK.AUDIO.DECODER.WMAPRO
codecInfo =OMX.MTK.AUDIO.DECODER.APE
codecInfo =OMX.MTK.AUDIO.DECODER.ALAC
codecInfo =OMX.google.amrnb.decoder
codecInfo =OMX.google.amrwb.decoder
codecInfo =OMX.google.aac.decoder
codecInfo =OMX.google.vorbis.decoder
codecInfo =OMX.MTK.VIDEO.DECODER.HEVC
codecInfo =OMX.MTK.VIDEO.DECODER.MPEG2
codecInfo =OMX.MTK.VIDEO.DECODER.MPEG4
codecInfo =OMX.MTK.VIDEO.DECODER.H263
codecInfo =OMX.MTK.VIDEO.DECODER.AVC
codecInfo =OMX.MTK.VIDEO.DECODER.VPX
codecInfo =OMX.MTK.VIDEO.DECODER.VP9
codecInfo =OMX.MTK.VIDEO.DECODER.VC1
codecInfo =OMX.MTK.VIDEO.DECODER.DIVX
codecInfo =OMX.MTK.VIDEO.DECODER.DIVX3
codecInfo =OMX.MTK.VIDEO.DECODER.XVID
codecInfo =OMX.MTK.VIDEO.DECODER.S263
codecInfo =OMX.google.vp8.decoder
codecInfo =OMX.google.h264.decoder
codecInfo =OMX.dolby.ac3.decoder
codecInfo =OMX.dolby.ec3.decoder
codecInfo =OMX.dolby.ec3_joc.decoder
codecInfo =OMX.ARICENT.VIDEO.DEC.WMV
codecInfo =OMX.MTK.VIDEO.ENCODER.MPEG4
codecInfo =OMX.MTK.VIDEO.ENCODER.H263
codecInfo =OMX.MTK.VIDEO.ENCODER.AVC
codecInfo =OMX.MTK.VIDEO.ENCODER.HEVC
codecInfo =OMX.MTK.AUDIO.ENCODER.VORBIS
codecInfo =OMX.MTK.AUDIO.ENCODER.ADPCM.MS
codecInfo =OMX.MTK.AUDIO.ENCODER.ADPCM.DVI
codecInfo =OMX.google.aac.encoder
codecInfo =OMX.google.amrnb.encoder
codecInfo =OMX.google.amrwb.encoder
codecInfo =OMX.google.flac.encoder
codecInfo =OMX.google.vp8.encoder
同时, 创建MediaCodeC的时候也会打印出来解码器类型
比如,创建一个 MediaCodeC 音频编码器,会打印出
I/OMXClient: Using client-side OMX mux.
E/OMXMaster: A component of name 'OMX.qcom.audio.decoder.aac'
可以看到,手头的测试机(OPPO x9007 4.4)自带解码器为
OMX.qcom.audio.decoder.aac
说明可以对音频进行 AAC 编解码处理;
当然,系统版本越高,手机厂商加入的编码器越多,这里打印的内容越多
理解了 MediaCodeC 的一些基本概念,那音视频解码是怎么回事?
1.先创建一个 MediaCodeC
MediaCodeC 采用工厂方法模式,需要调用其createDecoderByType
方法
MediaCodec mediaDecode = MediaCodec.createDecoderByType(mime);
参数 mine 做过音视频的童鞋肯定知道,代表解码的文件类型,以下是官方提供的类型列表
"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/hevc" - H.265/HEVC 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
列出了 Android 所支持的所有类型
如果是从一个音/视频文件中,如何获取该文件的mine呢
那MediaExtractor
闪亮登场,专门用户提取文件中的 音/视频中的一些关键信息
官方实例如下:
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...);//设置数据源
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance();
}
extractor.release();
extractor = null;
extractor.getTrackCount()
获取文件的媒体轨道,我们知道,音频和视频.字幕分别在不同的轨道,遍历完轨道后用extractor.getTrackFormat(i);
获取当前轨道的格式信息对象MediaFormat
拿到MediaFromat
后,你可以获取任何你想要的信息
其中,MediaFromat
是用一个 Map
存储所有的信息,而Key
为属性名,
使用方法如下
//获取音频的时长
long audioLength = format.getLong(MediaFormat.KEY_DURATION);
//获取文件的 mine(类型是音频还是视频)
String mime = format.getString(MediaFormat.KEY_MIME);
如果需要别的信息,你可以在MediaFormat
定义的Static 属性里面找;
开始解码:
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);//配置 MediaCodeC
codec.start();//开始解码
ByteBuffer[] inputBuffers = codec.getInputBuffers();//获取输入的缓存区
ByteBuffer[] outputBuffers = codec.getOutputBuffers();//获取输出的缓冲区
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();
最终得到的解码后的数据存放到outputBuffers
中,可以将outputBuffers
中的数据编码成为您想要的格式,整个流程结束;
下面就 Android 平台常用的将 MP3转换成 WAV 格式功能来更深入的理解 MediaCodeC的强大功能:
初始化解码器:
/** * 初始化解码器 */
private void initMediaDecode() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mediaExtractor = new MediaExtractor();// 此类可分离视频文件的音轨和视频轨道
// 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
// final int kSampleRates[] = {8000, 11025, 22050, 44100, 48000, 24000};
// MediaFormat.createAudioFormat(
// MediaFormat.KEY_PCM_ENCODING, kSampleRates[3], 2);
// 比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
// final int kBitRates[] = {64000, 96000, 128000};
// int minBufferSize = AudioRecord.getMinBufferSize(kSampleRates[3],
// AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
mediaExtractor.setDataSource(srcPath);// 媒体文件的位置
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {// 遍历媒体轨道 此处我们传入的是音频文件,所以也就只有一条轨道
MediaFormat format = mediaExtractor.getTrackFormat(i);
// 比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
format.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
try {
audioLength = format.getLong(MediaFormat.KEY_DURATION);
} catch (Exception e) {
e.printStackTrace();
}
// format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[1]);
// format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize);
// format.setInteger(MediaFormat.KEY_SAMPLE_RATE, kSampleRates[3]);
// format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, AudioFormat.CHANNEL_IN_STEREO);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio")) {// 获取音频轨道
mediaExtractor.selectTrack(i);// 选择此音频轨道
mediaDecode = MediaCodec.createDecoderByType(mime);// 创建Decode解码器
mediaDecode.configure(format, null, null, 0);
break;
}
}
if (mediaDecode == null) {
Log.e(TAG, "create mediaDecode failed");
return;
}
mediaDecode.start();// 启动MediaCodec ,等待传入数据
decodeInputBuffers = mediaDecode.getInputBuffers();// MediaCodec在此ByteBuffer[]中获取输入数据
decodeOutputBuffers = mediaDecode.getOutputBuffers();// MediaCodec将解码后的数据放到此ByteBuffer[]中
// 我们可以直接在这里面得到PCM数据
decodeBufferInfo = new MediaCodec.BufferInfo();// 用于描述解码得到的byte[]数据的相关信息
showLog("buffers:" + decodeInputBuffers.length);
}
} catch (Exception e) {
e.printStackTrace();
}
}
5.0以下解码逻辑:
/** * 解码{@link #srcPath}音频文件 得到PCM数据块 * * @return 是否解码完所有数据 */
private void srcAudioFormatToPCM() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (decodeInputBuffers != null) {
try {
for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
;// 获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
int inputIndex = mediaDecode.dequeueInputBuffer(-1);
if (inputIndex < 0) {
codeOver = true;
return;
}
ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];// 拿到inputBuffer
inputBuffer.clear();// 清空之前传入inputBuffer内的数据
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);// MediaExtractor读取数据到inputBuffer中
if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
codeOver = true;
} else {
mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);// 通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance();// MediaExtractor移动到下一取样处
}
}
// 获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
// 此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 1000000);
ByteBuffer outputBuffer;
byte[] chunkPCM;
while (outputIndex >= 0) {// 每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
outputBuffer = decodeOutputBuffers[outputIndex];// 拿到用于存放PCM数据的Buffer
chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
outputBuffer.get(chunkPCM);// 将Buffer内的数据取出到字节数组中
outputBuffer.clear();// 数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后
// 将不能向外输出数据
outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);// 再次获取数据,如果没有数据输出则outputIndex=-1
// 循环结束
}
if (codeOver) {
PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
if (onCompleteListener != null) {
onCompleteListener.completed(desPath);
}
}
} catch (Exception e) {
e.printStackTrace();
codeOver = true;
PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
if (onCompleteListener != null) {
onCompleteListener.completed(desPath);
}
}
}
}
}
5.0以上解码逻辑:
/** * android 5.0以上 */
private void srcAudioFormatToPCMHigherApi() {
if (Build.VERSION.SDK_INT >= 21) {
boolean sawOutputEOS = false;
final long kTimeOutUs = 10000;
long presentationTimeUs;
while (!sawOutputEOS) {
try {
int inputIndex = mediaDecode.dequeueInputBuffer(-1);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = mediaDecode.getInputBuffer(inputIndex);
if (inputBuffer != null) {
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
sawOutputEOS = true;
codeOver = true;
break;
} else {
presentationTimeUs = mediaExtractor.getSampleTime();
mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);// 通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance();
}
}
} else {
sawOutputEOS = true;
codeOver = true;
}
int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
ByteBuffer outputBuffer;// = mediaDecode.getOutputBuffer(outputIndex);//
// 拿到用于存放PCM数据的Buffer
while (outputIndex >= 0) {
outputBuffer = mediaDecode.getOutputBuffer(outputIndex);
boolean doRender = (decodeBufferInfo.size != 0);
if (doRender && outputBuffer != null) {
outputBuffer.position(decodeBufferInfo.offset);
outputBuffer.limit(decodeBufferInfo.offset + decodeBufferInfo.size);
byte[] chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
outputBuffer.get(chunkPCM);
outputBuffer.clear();// 数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后将不能向外输出数据
outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
}
}
} catch (Exception e) {
e.printStackTrace();
sawOutputEOS = true;
codeOver = true;
}
}
if (codeOver) {
PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
if (onCompleteListener != null) {
onCompleteListener.completed(desPath);
}
}
}
}
解码成功后,将 MP3格式的文件解码成最原始的字节码即 PCM
格式,需要对 PCM 做进一步转换成WAV
格式便于设备播放
PCM 转换成 wav
/**
* pcm文件转wav文件
*
* @param inFilename 源文件路径
* @param outFilename 目标文件路径
*/
public void pcmToWav(String inFilename, String outFilename) {
FileInputStream in;
FileOutputStream out;
long totalAudioLen;
long totalDataLen;
long longSampleRate = mSampleRate;
int channels = 2;
long byteRate = 16 * mSampleRate * channels / 8;
byte[] data = new byte[mBufferSize];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加入wav文件头
*/
private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels, long byteRate)
throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W'; //WAVE
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) (2 * 16 / 8); // block align
header[33] = 0;
header[34] = 16; // bits per sample
header[35] = 0;
header[36] = 'd'; //data
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}