• MediaCodec视频解码流程详解及参考demo


    一、MediaCodec简介

            MediaCodec是Android自带的底层多媒体支持架构的一部分(通常与 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。可以用来访问底层媒体编解码器,即编码器/解码器的组件。

            使用MediaCodec编解码即我们常说的硬编解码,它的优势是使用底层硬件编解码,效率比软编解码FFmpeg要高出不少。但劣势是由于硬编解码依赖于手机厂商的硬件设计,导致不同机型的效果可能千差万别,不好管控和统一。

            因此在实际开发中,使用软编解码或硬编解码还是要看具体的实际场景。

    二、MediaCodec生命周期

            MediaCodec 有三种状态,分别是执行(Executing)、停止(Stopped)和释放(Released),其中执行和停止分别有三个子状态,执行的三个字状态分别是 Flushed、Running 和 Stream-of-Stream,停止的三个子状态分别是 Uninitialized、Configured 和 Error,MediaCodec 生命周期示意图如下:

            同步模式下:

             异步模式下:

             当创建了 MediaCodec 之后,是处于未初始化的 Uninitialized 状态,调用 configure 方法之后就处于 Configured 状态,调用了 start 方法之后,就处于 Executing 状态。

            在 Executing 状态下开始处理数据,它又有三个子状态,分别是:Flushed、RunningEnd of Stream。当一调用 start 方法之后,就进入了 Flushed 状态,从输入缓冲区队列中取出一个缓冲区就进入了 Running 状态,当入队的缓冲区带有 EOS 标志时, 就会切换到 End of Stream 状态, MediaCodec 不再接受入队的缓冲区,但是仍然会对已入队的且没有进行编解码操作的缓冲区进行操作、输出,直到输出的缓冲区带有 EOS 标志,表示编解码操作完成了。

            在 Executing 状态下可以调用 flush 方法,使 MediaCodec 切换到 Flushed 状态。

            在 Executing 状态下可以调用 stop 方法,使 MediaCodec 切换到 Uninitialized 状态,然后再次调用 configure 方法进入 Configured 状态。另外,当调用 reset 方法也会进入到 Uninitialized 状态。

            当不再需要 MediaCodec 时,调用 release 方法将它释放掉,进入 Released 状态。

            当 MediaCodec 工作发生异常时,会进入到 Error 状态,此时还是可以通过 reset 方法恢复过来,进入 Uninitialized 状态。

    三、MediaCodec解码流程

            下面将讲解使用mediacodec对mp4的视频文件进行解码的API使用流程。本文主要是采用同步的解码方式。

            解码流程工作模型:

     

    1、创建mediacodec并初始化

            可通过createDecoderByType来创建mediacodec:

    mediaCodec = MediaCodec.createDecoderByType("video/avc");

            上面表示创建了一个解码器,并指定了解码类型为avc的视频解码器。

            初始化主要是为了给这个解码器设置一些格式,配置等,如下完整的创建和初始代码:

    1. private void initMediaCodecSys() {
    2. try {
    3. //创建
    4. mediaCodec = MediaCodec.createDecoderByType("video/avc");
    5. //格式
    6. mediaFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
    7. mediaExtractor = new MediaExtractor();
    8. //MP4 文件存放位置
    9. mediaExtractor.setDataSource(MainActivity.MP4_PLAY_PATH);
    10. Log.d(TAG, "getTrackCount: " + mediaExtractor.getTrackCount());
    11. for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
    12. MediaFormat format = mediaExtractor.getTrackFormat(i);
    13. String mime = format.getString(MediaFormat.KEY_MIME);
    14. Log.d(TAG, "mime: " + mime);
    15. if (mime.startsWith("video")) {
    16. mediaFormat = format;
    17. mediaExtractor.selectTrack(i);
    18. }
    19. }
    20. } catch (IOException e) {
    21. e.printStackTrace();
    22. }
    23. Surface surface = MainActivity.getSurface();
    24. //配置
    25. mediaCodec.configure(mediaFormat, surface, null, 0);
    26. mediaCodec.start();
    27. }

    2、视频解码线程

            视频解码线程主要内容就是进行解码的流程控制。这个阶段的相应API有如下:

    1. // 获取可用的输入缓冲区的索引
    2. public int dequeueInputBuffer (long timeoutUs)
    3. // 获取输入缓冲区
    4. public ByteBuffer getInputBuffer(int index)
    5. // 将填满数据的inputBuffer提交到编码队列
    6. public final void queueInputBuffer(int index,int offset, int size, long presentationTimeUs, int flags)
    7. // 获取已成功编解码的输出缓冲区的索引
    8. public final int dequeueOutputBuffer(BufferInfo info, long timeoutUs)
    9. // 获取输出缓冲区
    10. public ByteBuffer getOutputBuffer(int index)
    11. // 释放输出缓冲区
    12. public final void releaseOutputBuffer(int index, boolean render)

            获取可用的输入缓冲区的索引:

    int inputIndex = mediaCodec.dequeueInputBuffer(-1);

            获取输入缓冲区:

    ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inputIndex);

            读取相关数据:

    1. //读取一片或者一帧数据
    2. int sampSize = mediaExtractor.readSampleData(byteBuffer, 0);
    3. //读取时间戳
    4. long time = mediaExtractor.getSampleTime();

            将填满数据的inputBuffer提交到编码队列:

    1. if (sampSize > 0 && time > 0) {
    2. mediaCodec.queueInputBuffer(inputIndex, 0, sampSize, time, 0);
    3. //读取一帧后必须调用,提取下一帧
    4. //控制帧率在30帧左右
    5. mSpeedController.preRender(time);
    6. mediaExtractor.advance();
    7. }

            获取已成功编解码的输出缓冲区的索引:

    1. BufferInfo bufferInfo = new BufferInfo();
    2. int outIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);

            输出缓冲区:

    1. if (outIndex >= 0) {
    2. mediaCodec.releaseOutputBuffer(outIndex, true);
    3. }

            完整的视频解码线程代码如下:

    1. /**
    2. * Play the MP4 file Thread
    3. * 解码主流程
    4. */
    5. private class DecoderMP4Thread extends Thread {
    6. long pts = 0;
    7. @Override
    8. public void run() {
    9. super.run();
    10. while (!isDecodeFinish) {
    11. int inputIndex = mediaCodec.dequeueInputBuffer(-1);
    12. Log.d(TAG, "inputIndex: " + inputIndex);
    13. if (inputIndex >= 0) {
    14. ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inputIndex);
    15. //读取一片或者一帧数据
    16. int sampSize = mediaExtractor.readSampleData(byteBuffer, 0);
    17. //读取时间戳
    18. long time = mediaExtractor.getSampleTime();
    19. if (sampSize > 0 && time > 0) {
    20. mediaCodec.queueInputBuffer(inputIndex, 0, sampSize, time, 0);
    21. //读取一帧后必须调用,提取下一帧
    22. //控制帧率在30帧左右
    23. mSpeedController.preRender(time);
    24. mediaExtractor.advance();
    25. }
    26. }
    27. BufferInfo bufferInfo = new BufferInfo();
    28. int outIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
    29. if (outIndex >= 0) {
    30. mediaCodec.releaseOutputBuffer(outIndex, true);
    31. }
    32. }
    33. }
    34. }

    3、解码结束关闭回收

            完成解码播放后,对相关内存回收和关闭处理:

    1. public void close() {
    2. try {
    3. Log.d(TAG, "close start");
    4. if (mediaCodec != null) {
    5. isDecodeFinish = true;
    6. try {
    7. if (mDecodeMp4Thread != null) {
    8. mDecodeMp4Thread.join(2000);
    9. }
    10. } catch (InterruptedException e) {
    11. Log.e(TAG, "InterruptedException " + e);
    12. }
    13. boolean isAlive = mDecodeMp4Thread.isAlive();
    14. Log.d(TAG, "close end isAlive :" + isAlive);
    15. mediaCodec.stop();
    16. mediaCodec.release();
    17. mediaCodec = null;
    18. mSpeedController.reset();
    19. }
    20. } catch (IllegalStateException e) {
    21. e.printStackTrace();
    22. }
    23. instance = null;
    24. }

            至此,整个视频解码流程就讲解完毕。

    四、demo运行

            本人写了一个最简单的demo,使用mediacodec对mp4文件进行纯视频的解码播放,采用同步方式。

            该demo的解码对象mp4文件是内置在了raw/video.mp4,如果需要替换视频文件,可以替换这里:

             demo运行效果如下:

             点击START进行播放:

             播放过程中点击STOP可以停止播放。

            完整例子已经放到github上,如下:

            https://github.com/weekend-y/mediacodec_demo/tree/master/MediaCodec_DecodeMP4

            

  • 相关阅读:
    如何将EasyCVR平台RTSP接入的设备数据迁移到EasyNVR中?
    想学好C语言这些关键字不能少(深度解剖)
    奇舞周刊第 470 期:(10 月最新) 前端图形学实战:从零开发几何画板 (vue3 + vite 版)...
    基于TCP的网络聊天系统
    Windows动态链接库使用详解
    linux-进程管理
    flutter 中最详细的继承,多态,接口讲解
    Zlog日志框架学习笔记
    npm更换成淘宝镜像源及cnpm使用
    笔试强训(三十七)
  • 原文地址:https://blog.csdn.net/weekend_y45/article/details/127745961