这是我的第一篇博客,因为在视频解码方面做了比较多的工作,平时也用得比较多,所以第一篇博客我选择写视频解码。
大家都知道,Android开发播放视频我们一般都选取用MediaPlayer来播放,使用MediaPlayer来播放视频的优点是简单快捷,但是简单快捷带来的缺点就是可控制少,例如不能控制视频播放的帧率,不能倒着播,没有现成的接口监控视频播放进度等等。而用MediaCodec解码,播放过程完全由我们自己控制,在系统性能范围内,想以什么帧率播放就以什么帧率播放,而且还可以倒着播放视频,视频播放到哪里都在我们的掌握之中。
MediaCodec解码有同步和异步两种解码方式,官方推荐的是异步解码。同步解码,网上也有一些资料,而异步解码,网上资料较少。本篇文章将介绍异步解码的实现。
MediaCodec一般与MediaExtractor,MediaFormat配合使用,MediaCodec负责解码显示,MediaExtractor负责获取视频的数据给解码器。首先我们要创建一个MediaExtractor和MediaFormat来获取视频数据,假如视频文件位于Android工程的assets目录下,所以初始化MediaExtractor和MediaFormat应该是这样的:
然后设置视频资源,假如视频是在Android工程的assets目录下:
private MediaExtractor mMediaExtractor;
private MediaFormat mMediaFormat;
private String mMediaFormatMime;
/**
* @param context
* @param fileName 位于assets目录下的视频文件的名字,例如test.mp4,注意其他格式视频文件要统一改成后缀名为.mp4的文件
*/
public void initMediaExtractor(Context context, String fileName) {
mMediaExtractor = new MediaExtractor();
try {
AssetFileDescriptor assetFileDescriptor = context.getAssets().openFd(fileName);
mMediaExtractor.setDataSource(assetFileDescriptor.getFileDescriptor(), assetFileDescriptor.getStartOffset(),
assetFileDescriptor.getLength());
} catch (IOException e) {
e.printStackTrace();
}
for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) {//视频有音轨和视频轨,所以要选择视频轨,遍历视频的轨道
mMediaFormat = mMediaExtractor.getTrackFormat(i);
mMediaFormatMime = mMediaFormat.getString(MediaFormat.KEY_MIME);
if (mediaFormatMime.startsWith("video/")) {//当轨道为视频轨时,选定轨道
mMediaExtractor.selectTrack(i);
break;
}
}
}
初始化了媒体视频资源类,接下来初始化解码器
private Deque mOutputtIndexQueue;
private MediaCodec mMediaCodec;
try {
mMediaCodec = MediaCodec.createDecoderByType(mediaFormatMime);
mOutputtIndexQueue = new ArrayDeque<>();
mMediaCodec.setCallback(new MediaCodec.Callback() {//异步解码的回调方法
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {//解码器异步方法返回InputBuffer的序列值,用以填充视频数据,并且返回我们初始化的解码器。
ByteBuffer inputBuffer = null;
try {
inputBuffer = codec.getInputBuffer(index);
} catch (Exception ex) {
ex.printStackTrace();
}
if (inputBuffer != null) {
int sampleSize = mMediaExtractor.readSampleData(inputBuffer, 0);//获取视频帧的数据大小
long presentationTimeUs = mMediaExtractor.getSampleTime();//获取视频帧的时间戳
int flag = mMediaExtractor.getSampleFlags();//获取视频帧的标志位,例如是否是关键帧
try {
codec.queueInputBuffer(index, 0, sampleSize, presentationTimeUs, flag);//将视频帧的相关信息送入解码器的InputBuffer队列
mMediaExtractor.advance();
} catch (Exception ex) {
}
}
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
//解码器解码视//频帧完成回调在这里可以直接通过 codec.releaseOutputBuffer(index,true)把解码的内容显示出来,但是这样做我们达
//不到控制视频播放速度的目的,所以我们把解码器解码后的OutputBuffer队列存起来,用一个线程来执行codec.releaseOutputBuffer(index,true)显示;
mOutputtIndexQueue.add(index);//把解码器解码后的OutputBufferAvailable的序列用队列存起来
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {//解码器解码错误回调
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {//视频格式变化回调
}
});
mMediaCodec.configure(mMediaFormat, surface, null, 0);//配置视频格式,设置用以显示播放的Surface
mMediaCodec.start();//解码器开始工作,异步回调方法中的onInputBufferAvailable(MediaCodec codec, int index)将会回
//调如果我们往解码器送入视频数据,因为我们在这个回调方法里送了视频数据,所以只要有onInputBufferAvailable回调,那么解码器就会解码。
} catch (Exception e) {
}
初始化了解码器之后,我们也在异步回调了送了视频数据给解码器去解码,并且我们也得到了解码后的数据,接下来我们就要起一个线程去显示了,显示的线程应该是这样的:
private Handler mHandler;
private Runnable mRunnable;
/**
* @param frameInterval 每一帧播放的间隔,如果想播放的视频的帧率为30帧,那么参数就为1000/30=33ms
*/
private void initDecodeThread(final long frameInterval) {
mHandler = new Handler();
mRunnable = new Runnable() {
@Override
public void run() {
if (!mOutputtIndexQueue.isEmpty()) {//解码器解码后的输出Buffer不为空,即解码成功
mMediaCodec.releaseOutputBuffer(mOutputtIndexQueue.removeFirst(), true);//释放解码器的OutputBuffer,正是因为这一步释放,解码器的Buffer才会循环利用, onInputBufferAvailable(MediaCodec codec, int index)才会持续回调,
// 并且把解码的视频数据显示,第二个参数就是是否显示解码的视频帧
}
mHandler.postDelayed(mRunnable, frameInterval);//利用Handler不断post一个Runnable,达到相同时间播放视频一帧的目的
}
};
}
初始化了显示视频帧的线程后,在我们需要开始播放视频的时候调用mHandler.post(mRunnable);那么视频就会以我们设定好的帧率持续播放了
注意,应用退出时要释放解码器
mMediaCodec.stop();
mMediaCodec.release();
好了,用MediaCodec异步解码的过程就这样了,下一篇博客我们将用MediaCodec和SurfaceView或者TextureView自定义一个类似于Android VideoView的自定义View,让视频真正播起来,欢迎给我留言一起探讨相关细节。