音视频开发路线:
Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门
demo地址:
GitHub - wygsqsj/videoPath: 音视频学习路线demo
MediaCodec
使用MediaCodec编解码实际是通过底层的硬件来对我们的音视频数据进行处理的,俗称硬编硬解,ffmpeg编解码是软解,效率不如MediaCodec,MediaCodec的主要实现是通过Native层去访问dsp芯片,让dsp芯片去编/解码流,整个流程按我的理解就是类似一个火腿肠加工厂,我给他一车猪,他拿走两头,一顿加工输入一筐火腿肠,我把这筐火腿肠取走,工厂收回筐,再抓两头猪进行加工,更详细的说明:MediaCodec API完成音频 AAC 硬编、硬解 - 简书
API说明
- 构建MediaCodec
- MediaCodec.createDecoderByType("要解码的类型")构建解码器;
- MediaCodec.createEncoderByType("要编码码的类型")构建编码码器;配置config
- 配置config
configure(
@Nullable MediaFormat format//media格式控制 ;参数3 ;
@Nullable Surface surface,//surface用于渲染编解码后的视频
@Nullable MediaCrypto crypto,//加密用的对象,可根据自身需要定制
@ConfigureFlag int flags)//标志位,解码为0,编码为1
- start开启编解码
- dequeueInputBuffer() 可以理解为往工厂运猪的小推车,返回-1代表没有车子,返回大于-1代表有车子,获取到序号后就可以通过getInputBuffers()[序号]获取到小车子,再把猪放到车子里就可以了
- queueInputBuffer 理解成把小推车里的猪运进工厂
void queueInputBuffer(
int index,//小推车序号
int offset, //偏移量,一般为0,表示从猪的那个地方开始加工
int size, //猪的大小
long presentationTimeUs, //时间戳,理解当前猪在一车猪中的顺序
int flags//屠宰的标志,0进行加工 4就是没猪了
)
- dequeueOutputBuffer(“延时获取的微秒值”) 返回已处理好的数据buffer序号,理解成装火腿肠的筐序号,通过getOutputBuffers()[序号]获取装载数据的Buffer; 他除了序号的作用,还可以用作标志位,大于等于0表示序号,小于0表示当前codec返回的一些标志:
- MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 输出的format已更改
- MediaCodec.INFO_TRY_AGAIN_LATER 超时,没获取到
- MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED 输出缓冲区已更改
- releaseOutputBuffer(筐的序号, 标志位) 表示把筐还给工厂,第二个参数为true时将数据放到我们配置的surface里面
- stop() release()释放资源
总之同步的方式就是配置完MediaCodec后开启while循环不断的dequeueInputBuffer -> queueInputBuffer填充数据 -> dequeueOutputBuffer -> releaseOutputBuffer
使用
- input数据
//所有的猪都运进猪厂后不再添加
if (hasAudio) {
//从猪肉工厂获取装猪的小推车,填充数据后发送到猪肉工厂进行处理
ByteBuffer[] inputBuffers = decodeCodec.getInputBuffers();//所有的小推车
int inputIndex = decodeCodec.dequeueInputBuffer(0);//返回当前可用的小推车标号
if (inputIndex != -1) {
Log.i(LOG_TAG, "找到了input 小推车" + inputIndex);
//将MediaCodec数据取出来放到这个缓冲区里
ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到小推车
inputBuffer.clear();//扔出去里面旧的东西
//将audioExtractor里面的猪装载到小推车里面
int readSize = audioExtractor.readSampleData(inputBuffer, 0);
//audioExtractor没猪了,也要告知一下
if (readSize < 0) {
Log.i(LOG_TAG, "当前音频已经读取完了");
decodeCodec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
hasAudio = false;
} else {//拿到猪
Log.i(LOG_TAG, "读取到了音频数据,当前音频数据的数据长度为:" + readSize);
//告诉工厂这头猪的小推车序号、猪的大小、猪在这群猪里的排行、屠宰的标志
decodeCodec.queueInputBuffer(inputIndex, 0, readSize, audioExtractor.getSampleTime(), 0);
//读取音频的下一帧
audioExtractor.advance();
}
} else {
Log.i(LOG_TAG, "没有可用的input 小推车");
}
}
- output
int outputIndex = decodeCodec.dequeueOutputBuffer(decodeBufferInfo, 0);//返回当前筐的标记
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(LOG_TAG, "输出的format已更改" + decodeCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(LOG_TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(LOG_TAG, "输出缓冲区已更改");
break;
default:
Log.i(LOG_TAG, "获取到解码后的数据了,当前解析后的数据长度为:" + decodeBufferInfo.size);
//获取所有的筐
ByteBuffer[] outputBuffers = decodeCodec.getOutputBuffers();
//拿到当前装满火腿肠的筐
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = decodeCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
//将火腿肠放到新的容器里,便于后期装车运走
byte[] pcmData = new byte[decodeBufferInfo.size];
outputBuffer.get(pcmData);//写入到字节数组中
outputBuffer.clear();//清空当前筐
//装车
fos.write(pcmData);//数据写入文件中
fos.flush();
//把筐放回工厂里面
decodeCodec.releaseOutputBuffer(outputIndex, false);
break;
}
- 完整代码
package com.wish.videopath.demo5;
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.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.wish.videopath.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import static com.wish.videopath.MainActivity.LOG_TAG;
/**
* 类名称:EncodeAACThread
* 类描述:将AAC通过MediaCodec接码成PCM文件
*/
class DecodeAACThread extends Thread {
private Context context;
private MediaFormat audioFormat;
private File pcmFile;
private boolean hasAudio = true;
private FileOutputStream fos = null;
public DecodeAACThread(Demo5Activity demo5Activity) {
context = demo5Activity;
pcmFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC), "demo5.pcm");
try {
pcmFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void run() {
super.run();
//通过MediaExtractor获取音频通道
MediaExtractor audioExtractor = new MediaExtractor();
MediaCodec decodeCodec = null;
//pcm文件输出
// FileOutputStream fos = null;
try {
fos = new FileOutputStream(pcmFile.getAbsoluteFile());
audioExtractor.setDataSource(context.getResources().openRawResourceFd(R.raw.demo5));
int count = audioExtractor.getTrackCount();
for (int i = 0; i < count; i++) {
audioFormat = audioExtractor.getTrackFormat(i);
if (audioFormat.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
audioExtractor.selectTrack(i);
Log.i(LOG_TAG, "aac 找到了通道" + i);
break;
}
}
//初始化MiediaCodec
decodeCodec = MediaCodec.createDecoderByType(audioFormat.getString(MediaFormat.KEY_MIME));
//数据格式,surface用来渲染解析出来的数据;加密用的对象;标志 encode :1 decode:0
decodeCodec.configure(audioFormat, null, null, 0);
MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
//启动解码
decodeCodec.start();
/*
* 同步方式,流程是在while中
* dequeueInputBuffer -> queueInputBuffer填充数据 -> dequeueOutputBuffer -> releaseOutputBuffer显示画面
*/
boolean hasAudio = true;
while (true) {
//所有的猪都运进猪厂后不再添加
if (hasAudio) {
//从猪肉工厂获取装猪的小推车,填充数据后发送到猪肉工厂进行处理
ByteBuffer[] inputBuffers = decodeCodec.getInputBuffers();//所有的小推车
int inputIndex = decodeCodec.dequeueInputBuffer(0);//返回当前可用的小推车标号
if (inputIndex != -1) {
Log.i(LOG_TAG, "找到了input 小推车" + inputIndex);
//将MediaCodec数据取出来放到这个缓冲区里
ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到小推车
inputBuffer.clear();//扔出去里面旧的东西
//将audioExtractor里面的猪装载到小推车里面
int readSize = audioExtractor.readSampleData(inputBuffer, 0);
//audioExtractor没猪了,也要告知一下
if (readSize < 0) {
Log.i(LOG_TAG, "当前音频已经读取完了");
decodeCodec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
hasAudio = false;
} else {//拿到猪
Log.i(LOG_TAG, "读取到了音频数据,当前音频数据的数据长度为:" + readSize);
//告诉工厂这头猪的小推车序号、猪的大小、猪在这群猪里的排行、屠宰的标志
decodeCodec.queueInputBuffer(inputIndex, 0, readSize, audioExtractor.getSampleTime(), 0);
//读取音频的下一帧
audioExtractor.advance();
}
} else {
Log.i(LOG_TAG, "没有可用的input 小推车");
}
}
//工厂已经把猪运进去了,但是是否加工成火腿肠还是未知的,我们要通过装火腿肠的筐来判断是否已经加工完了
int outputIndex = decodeCodec.dequeueOutputBuffer(decodeBufferInfo, 0);//返回当前筐的标记
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(LOG_TAG, "输出的format已更改" + decodeCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(LOG_TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(LOG_TAG, "输出缓冲区已更改");
break;
default:
Log.i(LOG_TAG, "获取到解码后的数据了,当前解析后的数据长度为:" + decodeBufferInfo.size);
//获取所有的筐
ByteBuffer[] outputBuffers = decodeCodec.getOutputBuffers();
//拿到当前装满火腿肠的筐
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = decodeCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
//将火腿肠放到新的容器里,便于后期装车运走
byte[] pcmData = new byte[decodeBufferInfo.size];
outputBuffer.get(pcmData);//写入到字节数组中
outputBuffer.clear();//清空当前筐
//装车
fos.write(pcmData);//数据写入文件中
fos.flush();
//把筐放回工厂里面
decodeCodec.releaseOutputBuffer(outputIndex, false);
break;
}
if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(LOG_TAG, "表示当前编解码已经完事了");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (audioExtractor != null) {
audioExtractor.release();
}
if (decodeCodec != null) {
decodeCodec.stop();
decodeCodec.release();
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
文章评论