MediaCodec框架剖析中讲到了可以使用MediaCodec来播放视频,我们现在就来试下如何使用MediaCodec 播放视频。
上图是播放器播放视频的主要流程,其中深色的部分是视频流处理的流程;
- 从视频中分离出音频流和视频流;
- 要处理视频流,就要对视频流数据进行解码,Android平台上自然要用到MediaCodec 解码工具;
- 解码出来的数据要写入到Surface中,借助SurfaceFlinger来合成一个个surface数据;
1.分离音频视频流
mExtractor = new MediaExtractor();
try {
mExtractor.setDataSource(mVideoPath);
} catch (Exception e) {
e.printStackTrace();
}
for(int index = 0; index < mExtractor.getTrackCount(); index++) {
MediaFormat format = mExtractor.getTrackFormat(index);
Log.i(TAG, "index="+index+", format="+format);
String mime = format.getString(MediaFormat.KEY_MIME);
if(mime.startsWith("video/")) {
//Do something.
}
}
分离音频视频流当然要借助MediaExtractor 执行,我们只需要播放视频就行,音频不需要管;取出视频的MediaFormat,打印出视频的MediaFormat信息:
2020-02-03 15:04:30.696 16773-16810/? I/ACodec: [OMX.qcom.video.decoder.avc]configureCodec AMessage(what = 'conf', target = 1) = {
int32_t track-id = 1
int32_t level = 512
string mime = "video/avc"
int32_t profile = 1
string language = "```"
int32_t color-standard = 1
int32_t display-width = 1280
Buffer csd-1 = {
00000000: 00 00 00 01 68 ce 06 e2 ....h...
}
int32_t color-transfer = 3
int64_t durationUs = 21393466
int32_t display-height = 720
int32_t width = 1280
int32_t color-range = 2
int32_t rotation-degrees = 90
int32_t max-input-size = 156947
int32_t frame-rate = 30
int32_t height = 720
Buffer csd-0 = {
00000000: 00 00 00 01 67 42 80 1f da 01 40 16 e9 58 30 30 [email protected]
00000010: 30 36 85 09 a8 06...
}
RefBase *native-window = 0x7f2325b000
}
哇,很赞,连 csd-0 和 csd-1都打印出来了;
csd-0 表示 H264 的SPS;SPS是必须的,如果缺失,视频播不出来;
csd-1 表示 H264 的PPS;PPS可以缺失;
2.创建MediaCodec实例
上一步把视频的mime解析出来,是 string mime = "video/avc",
mExtractor.selectTrack(index);
try {
mCodec = MediaCodec.createDecoderByType(mime);
} catch (Exception e) {
e.printStackTrace();
}
mCodec.configure(format, mSurface, null, 0);
if (mCodec == null) {
return;
}
mCodec.start();
这个函数执行的流程上一章已经讲得非常清楚了
3.输入/输出缓冲区处理
ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mCodec.getOutputBuffers();
我们通常认为MediaCodec 的工作方式是提供一个input数据就会对应一个output数据,实际上不是这样;
MediaCodec读入input数据之后,会根据代码逻辑来处理编码数据,或者解码数据,然后将处理的结果输出,MediaCodec的input 和output之间没有对应关系。
我们知道MediaCodec有同步解码和异步解码两种;对于网络资源,还是尽量使用异步解码,这样性能可以保证;
ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mCodec.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean isEOS = false;
long start = System.currentTimeMillis();
while(!Thread.interrupted()) {
if(!isEOS) {
int inIndex = mCodec.dequeueInputBuffer(10 * 1000); //10ms
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int sampleSize = mExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
mCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEOS = true;
} else {
mCodec.queueInputBuffer(inIndex, 0, sampleSize, mExtractor.getSampleTime(), 0);
mExtractor.advance();
}
}
}
int outIndex = mCodec.dequeueOutputBuffer(bufferInfo, 10 * 1000);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
outputBuffers = mCodec.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
default:
ByteBuffer buffer = outputBuffers[outIndex];
while(bufferInfo.presentationTimeUs / 1000 > System.currentTimeMillis() - start) {
try {
sleep(10);
} catch (Exception e) {
e.printStackTrace();
break;
}
}
mCodec.releaseOutputBuffer(outIndex, true);
break;
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
mCodec.stop();
mCodec.release();
mExtractor.release();
- MediaCodec 处理输入数据产生数据数据,当异步处理数据时,使用一组 inputBuffers 和 ouputBuffers;
- 客户端请求数据填入预先设定的空输入缓冲区,就是定义好的MediaCodec.BufferInfo 对象,inputBuffers 填满数据后将其传递到 MediaCodec 并进行编解码处理;
- MediaCodec 编解码后的数据被填充到 ouputBuffers 中;
- 客户端请求 ouputBuffers ,消耗 ouputBuffers 中的内容,用完后释放,给MediaCodec 重新填充 ouputBuffers;
一定一定要注意的是:
必须要保证输入 和 输出队列同时非空,至少有一个输入buffer和输出buffer才能正常工作;
下面附在demo地址:https://github.com/JeffMony/MediaCodecDemo
感谢关注公众号JeffMony,持续给你带来音视频方面的知识。