Android音视频开发基础(六):学习MediaCodec API,完成视频H.264的解码

1,665 阅读7分钟

前言

在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》。本文是Android音视频任务列表的其中一个, 对应的要学习的内容是:学习MediaCodec API,完成视频H.264的解码。(本文是最基本的H264的解码,进阶内容以后会讲解)


音视频任务列表

音视频任务列表: 点击此处跳转查看.


目录

在这里插入图片描述


(一)编码解码基础概念

1.1 为什么要进行视频编码

视频是由一帧帧图像组成,就如常见的gif图片,如果打开一张gif图片,可以发现里面是由很多张图片组成。一般视频为了不让观众感觉到卡顿,一秒钟至少需要24帧画面(一般是30帧),假如该视频是一个1280x720分辨率的视频,那么不经过编码一秒钟的大小: 结果:1280x720x4x24/(1024*1024)≈84.375M 所以不经过编码的视频根本没法保存,更不用说传输了。

1.2 视频压缩编码标准

视频中存在很多冗余信息,比如图像相邻像素之间有较强的相关性,视频序列的相邻图像之间内容相似,人的视觉系统对某些细节不敏感等,对这部分冗余信息进行处理的过程就是视频编码。 视频编码主要分为H.26X系列和MPEG-X系列,这篇文章主要讲解大名鼎鼎的H.264 H.264:H.264/MPEG-4第十部分,或称AVC(Advanced Video Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。

1.3 H.264基本概念

1.3.1 H.264编码原理

对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小,B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。如果发现D帧与C帧差异很大,那就先编码出一个完整的图像帧D,继续重复之前过程。

1.3.2 H.264三种帧

在H.264中定义了三种帧: I帧:完整编码的帧叫I帧 P帧:参考之前的I帧生成的只包含差异部分编码的帧叫P帧 B帧:参考前后的帧编码的帧叫B帧 在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符

1.3.2 H.264解码原理

遍历H.264数据流,获取H.264的每一帧,如果把每一帧填充到MediaCodec,MediaCodec进行相应处理,然后把处理的数据交给控件进行展示或者做一些其他操作


(二)H.264解码过程

2.1 简单回顾MediaCodec工作流程

如果不熟悉MediaCodec,请查看: Android音视频开发基础(五):学习MediaCodec API,完成音频AAC硬编、硬解

MediaCodec工作流程一句话总结:把数据填充到MediaCodec,MediaCodec进行相应处理,然后把处理的数据交给控件进行展示或者做一些其他操作 (1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer] (2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer] (3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理 (4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列 (5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer] (6)Client 对编解码后的 buffer 进行渲染/播放 (7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer] 在这里插入图片描述

2.2 我理解的MediaCodec的过程

以上过程为官方提供的,下面讲解一下我理解的MediaCodec的过程 在这里插入图片描述 图中1,2,3,4,5,6,7表示各种步骤 (1)获取一组缓冲区队列(8个) (2)得到可用的缓存区,通过MediaCodec.dequeueInputBuffer(int time)得到,参数为等待的时间,超过该时间,则获取不到缓冲区 (3)将一帧数据传入缓冲区,通过MediaCodec.queueInputBuffer();实现 (4)把数据传递给解码器,进行解码 (5)得到一个新的缓冲区 (6)将处理的数据交给新的缓冲区 (7)将处理的数据交给SurfaceView显示


(三)H.264解码过程源代码

3.1 准备资源

将源代码中res\raw\hh264.h264放入手机根目录下,源代码地址在文章最后

3.2 布局

activity_main:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <SurfaceView
        android:id="@+id/SurfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

3.3 代码

public class MainActivity extends AppCompatActivity {

    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    private Thread mDecodeThread;
    private MediaCodec mMediaCodec;
    private DataInputStream mInputStream;

    private final static String SD_PATH = Environment.getExternalStorageDirectory().getPath();
    private final static String H264_FILE = SD_PATH + "/hh264.h264";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取权限
        verifyStoragePermissions(this);
        mSurfaceView = (SurfaceView) findViewById(R.id.SurfaceView);
        // 获取文件输入流
        getFileInputStream();
        // 初始化解码器
        initMediaCodec();
    }

    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE"};

    public static void verifyStoragePermissions(Activity activity) {
        try {
            // 检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) {
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取需要解码的文件流
     */
    public void getFileInputStream() {
        try {
            File file = new File(H264_FILE);
            mInputStream = new DataInputStream(new FileInputStream(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            try {
                mInputStream.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    /**
     * 获得可用的字节数组
     * @param is
     * @return
     * @throws IOException
     */
    public static byte[] getBytes(InputStream is) throws IOException {
        int len;
        int size = 1024;
        byte[] buf;

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        buf = new byte[size];
        while ((len = is.read(buf, 0, size)) != -1) {
            // 将读取的数据写入到字节输出流
            bos.write(buf, 0, len);
        }
        // 将这个流转换成字节数组
        buf = bos.toByteArray();
        return buf;
    }

    /**
     * 初始化解码器
     */
    private void initMediaCodec() {
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                try {
                    // 创建编码器
                    mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 使用MediaFormat初始化编码器,设置宽,高
                final MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
                // 设置帧率
                mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 40);
                // 配置编码器
                mMediaCodec.configure(mediaFormat, holder.getSurface(), null, 0);

                startDecodingThread();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });
    }


    /**
     * 开启解码器并开启读取文件的线程
     */
    private void startDecodingThread() {
        mMediaCodec.start();
        mDecodeThread = new Thread(new DecodeThread());
        mDecodeThread.start();
    }

    private class DecodeThread implements Runnable {
        @Override
        public void run() {
            // 开始解码
            decode();
        }

        private void decode() {
            // 获取一组缓存区(8个)
            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
            // 解码后的数据,包含每一个buffer的元数据信息
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            // 获取缓冲区的时候,需要等待的时间(单位:毫秒)
            long timeoutUs = 10000;
            byte[] streamBuffer = null;
            try {
                // 返回可用的字节数组
                streamBuffer = getBytes(mInputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }

            int bytes_cnt = 0;
            // 得到可用字节数组长度
            bytes_cnt = streamBuffer.length;

            // 没有得到可用数组
            if (bytes_cnt == 0) {
                streamBuffer = null;
            }
            // 每帧的开始位置
            int startIndex = 0;
            // 定义记录剩余字节的变量
            int remaining = bytes_cnt;
            // while(true)大括号内的内容是获取一帧,解码,然后显示;直到获取最后一帧,解码,结束
            while (true) {
                // 当剩余的字节=0或者开始的读取的字节下标大于可用的字节数时  不在继续读取
                if (remaining == 0 || startIndex >= remaining) {
                    break;
                }

                // 寻找帧头部
                int nextFrameStart = findHeadFrame(streamBuffer, startIndex + 2, remaining);

                // 找不到头部返回-1
                if (nextFrameStart == -1) {
                    nextFrameStart = remaining;
                }
                // 得到可用的缓存区
                int inputIndex = mMediaCodec.dequeueInputBuffer(timeoutUs);
                // 有可用缓存区
                if (inputIndex >= 0) {
                    ByteBuffer byteBuffer = inputBuffers[inputIndex];
                    byteBuffer.clear();
                    // 将可用的字节数组(一帧),传入缓冲区
                    byteBuffer.put(streamBuffer, startIndex, nextFrameStart - startIndex);
                    // 把数据传递给解码器
                    mMediaCodec.queueInputBuffer(inputIndex, 0, nextFrameStart - startIndex, 0, 0);
                    // 指定下一帧的位置
                    startIndex = nextFrameStart;
                } else {
                    continue;
                }

                int outputIndex = mMediaCodec.dequeueOutputBuffer(info, timeoutUs);
                if (outputIndex >= 0) {
                    // 加入try catch的目的是让界面显示的慢一点,这个步骤可以省略
                    try {
                        Thread.sleep(33);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 将处理过的数据交给surfaceview显示
                    mMediaCodec.releaseOutputBuffer(outputIndex, true);
                }
            }
        }
    }

    /**
     * 查找帧头部的位置
     * 在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符
     * @param bytes
     * @param start
     * @param totalSize
     * @return
     */
    private int findHeadFrame(byte[] bytes, int start, int totalSize) {
        for (int i = start; i < totalSize - 4; i++) {
            if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01)) || ((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
                return i;
            }
        }
        return -1;
    }
}

3.4 代码注意事项

(1)加入权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

(2)解码是耗时操作,所以需要放在子线程中


源代码地址: Android音视频开发基础(六):学习MediaCodec API,完成视频H264的解码