告别MediaPlayer,基于MediaCodec的异步视频解码方案,让你的视频想怎么播就怎么播

这是我的第一篇博客,因为在视频解码方面做了比较多的工作,平时也用得比较多,所以第一篇博客我选择写视频解码。

大家都知道,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,让视频真正播起来,欢迎给我留言一起探讨相关细节。

你可能感兴趣的:(音视频解码)