Android音视频--H.264视频流解码

5,665 阅读4分钟

1. 简介

H.264是比较多开发者使用较多的一种数字视频压缩格式,主要用于直播流的传输与视频网站的视频流传输,也有不少开发者开始使用H.265进行视频压缩,性能较H.264提升较大。本篇文章着重介绍使用MediaCodec硬件H.264裸字节流数据的实现方式,有关于更多H.264的介绍可以查看参考文章中H.264的结构介绍。

2.使用MediaCodec硬解码

2.1 MediaCodec介绍

  • MediaCodec类Android提供的用于访问低层多媒体编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。

  • Android 底层多媒体模块采用的是 OpenMax 框架,任何 Android 底层编解码模块的实现,都必须遵循 OpenMax 标准。Google 官方默认提供了一系列的软件编解码器:包括:OMX.google.h264.encoder,OMX.google.h264.encoder, OMX.google.aac.encoder, OMX.google.aac.decoder 等等,而硬件编解码功能,则需要由芯片厂商依照 OpenMax 框架标准来完成,所以,一般采用不同芯片型号的手机,硬件编解码的实现和性能是不同的。

  • Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等。

2.2 MediaCodec 工作流程

编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。简要地说,一般的处理步骤如下

  • 请求一个空的输入 input buffer
  • 填入数据、并将其交给 MediaCodec
  • MediaCodec 处理数据后,将处理后的数据放在一个空的 output buffer
  • 获取填充数据了的 output buffer,得到其中的数据,然后将其返还给 MediaCodec

2.3 MediaCodec API 说明

MediaCodec可以处理具体的视频流,主要有这几个方法:

  • configure:配置为编码器start:成功地配置组件后,调用start方法。
  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
  • queueInputBuffer:输入流入队列dequeueInputBuffer:从输入流队列中取数据进行编码操作
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据
  • stop:完成解码/编码任务后,需注意的是codec任然处于活跃状态且准备重新start。
  • flush:冲洗组件的输入和输出端口release:释放codec实例使用的资源。
  • reset:使codec返回到初始(未初始化)状态。

2.4 Talk is cheap, Show me the code

初始化MediaCodec

    /**
    * 视频类型
    */
    private final static String MIME_TYPE = "video/avc";

    /**
     * 初始化播放
     */
    private void initVideo(SurfaceHolder holder) {
        try {
            // 初始化MediaCodec,方法有两种,分别是通过名称和类型来创建
            // 这里使用通过类型来创建
            mMediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
            // 获取视频的宽高
            mVideoHeight = holder.getSurfaceFrame().width();
            mVideoWidth = holder.getSurfaceFrame().height();
            // MediaFormat,这个类包含了比特率、帧率、关键帧间隔时间等,其中比特率如果太低就会造成类似马赛克的现象。
            mMediaFormat = MediaFormat.createVideoFormat(MIME_TYPE,
                    1080, 1920);
            // 设置比特率 
            mMediaFormat.setInteger(KEY_BIT_RATE,
                    mVideoHeight * mVideoWidth * 5);
            // 设置帧率 
            mMediaFormat.setInteger(KEY_FRAME_RATE, 30);
            
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                // 描述编码器要使用的所需比特率模式的键
                // BITRATE_MODE_CQ: 表示完全不控制码率,尽最大可能保证图像质量
                //BITRATE_MODE_CBR: 表示编码器会尽量把输出码率控制为设定值
                //BITRATE_MODE_VBR: 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;
                mMediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
            }
            mMediaFormat.setInteger(KEY_I_FRAME_INTERVAL, 1);

            byte[] headerSps = {0, 0, 0, 1, 103, 66, 0, 41, -115, -115, 64, 80,
                    30, -48, 15, 8, -124, 83, -128};
            byte[] headerPps = {0, 0, 0, 1, 104, -54, 67, -56};

            mMediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(headerSps));
            mMediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(headerPps));

            mMediaCodec.configure(mMediaFormat, holder.getSurface(), null, 0);
            mMediaCodec.start();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

视频解码部分代码

将接收到或从文件读取到的byte[]传入onFrame中

    /**
     * 解码数据并显示视频
     * buf 视频数据组
     * offset 数据偏移量
     * length 有效长度
     */
    private void onFrame(byte[] buf, int offset, int length) {
        try {
            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0);
                if (inputBufferIndex >= 0) {
                    ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                    inputBuffer.clear();
                    inputBuffer.put(buf, offset, length);
                    mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount
                            * 30, 0);
                    mCount++;
                }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            while (outputBufferIndex >= 0) {
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, true);
                outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                if (!isPlayingSound) {
                        mHandler.postDelayed(() -> isPlayingSound = true, 1000);
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

3. 使用FFmpeg进行解码

使用与原理可以浏览参考文章3。 具体代码实现方式可参考此类

4. 参考文章

  1. Android MediaCodec 官方文档介绍
  2. Android原生编解码接口 MediaCodec 之——完全解析
  3. FFmpeg解码H.264
  4. H.264结构