前面几章介绍了关于
RGB
、YUV
格式,并介绍了如何采集和编码视频数据,那么本章就来介绍如何解码视频
一、MediaCodec
我们再来回顾下MediaCodec
,首先上谷歌官网的MediaCodec
流程图
这张图为我们展示了MediaCodec
的工作流程
之前在音频的部分,我们也具体介绍了关于MediaCodec
的工作流程,这里我们再简单描述下:
MediaCodec
具有解码和编码的功能,是Android
内置的编解码组件MediaCodec
分为两个部分,一个是客户端
,一个是服务端
,客户端可以理解为我们的调用端,而服务端则是具体编解码的硬件部分- 客户端从
Codec
中获取一个空的Buffer
,然后将待编/解码
的数据填入该Buffer
中,接着插入Codec
的输入队列
,再接着Codec
处理完数据后,我们再从Code
c中获取编/解码完成
的数据,至此,即完成了一次输入输出的过程,重复该过程,我们则得到完整的一次编/解码数据 MediaCodec
有两种模式,异步
和同步
,具体使用哪种模式视具体业务而定,比如,在视频编码那一章,我们使用的是异步的编码模式,这是因为从相机获取到的数据是通过回调,而在音频编码时,我们使用的是同步模式,这是因为AudioRecord
是支持同步获取数据
以上就是关于MediaCodec的介绍,更加具体的介绍和使用可以查看前几章的内容
关于视频解码,还涉及到一个组件MediaExtractor
,它能够解封装Mp4
,将其分为音轨
和视轨
,并能够一帧一帧
地读取,在前面音频解码的章节也有具体描述,不再赘述
二、视频解码
关于视频解码,我们将使用MediaCodec
的同步方式去实现
先来看整体流程
- 开启子线程
- 配置必要参数
- 从
MediaExtractor
中找到指定的Track
- 初始化
MediaCodec
- 开始解码
- 循环从
MediaExtractor
中读取数据,并送入编码器 - 解码完成后,可以得到解码后的数据,也可以直接进行渲染
- 停止解码,释放资源
下面对上述步骤,一一讲解
2.1 开启子线程
和音频解码一样,都需要在子线程中执行
private static class DecodeThread extends Thread {
public DecodeThread() {}
}
复制代码
2.2 配置必要参数
private static class DecodeThread extends Thread {
private static final long TIMEOUT_MS = 2000L;
private MediaExtractor mediaExtractor;
private MediaCodec mediaCodec;
private String path;
private Surface surface;
private String mime;
private MediaFormat format;
private boolean isStopDecode = false;
public DecodeThread(String path, Surface surface) {
this.path = path;
this.surface = surface;
}
}
复制代码
可以看到,在构造方法中有两个参数
path
:Mp4
地址,这个是必须的surface
:这个是解码用的Surface
,非必须的,我们可以直接得到解码后的数据,也可以将解码的数据渲染到指定的Surface
中,不过一般我们都是直接解码到Surface
上,因为解码后的数据就只是YUV
数据,使用起来也麻烦
2.3 从MediaExtractor中找到指定的Track
首先重写Thread
的run
方法
@Override
public void run() {
super.run();
initMediaExtractor();
initMediaCodec();
decode();
}
复制代码
接下来看initMediaExtractor()
方法
private void initMediaExtractor() {
if (TextUtils.isEmpty(path)) {
return;
}
try {
mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mediaExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!TextUtils.isEmpty(mime) && mime.startsWith("video/")) {
mediaExtractor.selectTrack(i);
this.mime = mime;
this.format = format;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
mediaExtractor = null;
mime = null;
format = null;
}
}
复制代码
该方法用于初始化MediaExtractor
,且获取对应视频的mime
和format
2.4 初始化MediaCodec
获取的mime
和format
主要是为了初始化MediaCodec
private void initMediaCodec() {
if (TextUtils.isEmpty(mime) || format == null || surface == null) {
return;
}
try {
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, surface, null, 0);
} catch (IOException e) {
e.printStackTrace();
mediaCodec = null;
}
}
复制代码
在MediaCodec
的config
方法中,传入了format
和surface
format
是在MediaExtractor
中获取的surface
是外部传入
2.5 开始解码
mediaCodec.start();
复制代码
调用MediaCodec
的start()
方法即开始解码
2.6 循环从MediaExtractor中读取数据,并送入编码器
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = mediaExtractor.readSampleData(inputBuffer, 0);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopDecode = true;
} else {
mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
mediaExtractor.advance();
}
}
复制代码
在该代码块中可以看到
-
从
MediaExtractor
中读取对应的数据,将其填充到从MediaCodec
获取的ByteBuffer
中 -
在填充完毕后,会有两种情况
- 一是
数据大于0
的情况,此时正常插入MediaCodec
即可,并在插入后调用MediaExtractor
的advance()
方法,该方法表示进入下一帧 - 还有一种情况是当
数据小于等于0
时,此时插入MediaCodec
就得传入一个BUFFER_FLAG_END_OF_STREAM
的flag
,表示整体输入流结束
- 一是
2.7 解码完成后,可以得到解码后的数据,也可以直接进行渲染
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
if (outputBuffer != null && info.size > 0) {
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
byte[] data = new byte[info.size];
outputBuffer.get(data);
outputBuffer.clear();
// 得到的data数据就是YUV数据,可以拿去做对应的业务
}
mediaCodec.releaseOutputBuffer(outputBufferId, true);
}
复制代码
在获取解码数据时,需要注意的是需要延时解码,因为解码时间和播放时间不一致,会导致播放变快,这也就解释了为什么直接解码不加延时,播放的视频会变得很快
至此,重复2.6 - 2.7
过程,即可进行完整的解码,整体解码代码如下
private void decode() {
if (mediaExtractor == null || mediaCodec == null) {
return;
}
long startMs = System.currentTimeMillis();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
mediaCodec.start();
for (; ; ) {
if (isStopDecode) {
release();
break;
}
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = mediaExtractor.readSampleData(inputBuffer, 0);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopDecode = true;
} else {
mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
mediaExtractor.advance();
}
}
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
if (outputBuffer != null && info.size > 0) {
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
byte[] data = new byte[info.size];
outputBuffer.get(data);
outputBuffer.clear();
// 得到的data数据就是YUV数据,可以拿去做对应的业务
}
mediaCodec.releaseOutputBuffer(outputBufferId, true);
}
}
}
复制代码
2.8 停止解码,释放资源
在完成一切之后,记得停止解码并释放资源
void stopDecode() {
isStopDecode = true;
try {
join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void release() {
if (mediaExtractor != null) {
mediaExtractor.release();
mediaExtractor = null;
}
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
}
复制代码