Android 硬解码MediaCodec配合SurfaceView的踏坑之旅

一 前言

最近在看一些Android硬解码的内容,顺便写了一个硬解码demo,简直就是踏坑之旅。使用Android自带的MediaCodec会有很多问题,动不动就卡死甚至crash。废话少说直接上代码,最后会将踩过的坑列觉出来并给出fix的办法

二 demo

1 初始化
首先 使用MediaCodec的静态方法创建一个解码器MediaCodec,记住是解码器,后面的mMimeType的参数就是解码视频的类型(video/avc video/mp4v-es video/hevc等等)
其次 再设置一些参数MediaCodec.configure(mediaformat, mSurface, null, 0)
最后直接调用mMediaCodec.start()我们的硬解码初始化就搞定啦!

public void init() {

        Log.i(TAG, "init");

        try {
            //通过多媒体格式名创建一个可用的解码器
            mMediaCodec = MediaCodec.createDecoderByType(mMimeType);
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "Init Exception " + e.getMessage());
        }
        //初始化解码器格式 预设宽高
        MediaFormat mediaformat = MediaFormat.createVideoFormat(mMimeType, VIDEO_WIDTH, VIDEO_HEIGHT);
        //设置帧率
        mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        //crypto:数据加密 flags:编码器/编码器
        mMediaCodec.configure(mediaformat, mSurface, null, 0);
        mMediaCodec.start();
    }

2 如何解码

解码需要给解码器喂h264/h265的流数据,所以一般解码分两个线程:一个线程专门用来接收设备传过来的视频数据并存到一个队列里面,称之为接收线程,另外一个线程专门从这个线程拿数据然后直接开始解码,称之为解码线程。
那么MediaCodec是如何进行硬解码的呢,我这里直接将我的解码线程丢出来,里面有详细的解码说明。

private class DecodeThread extends Thread {

        private boolean isRunning = true;

        public synchronized void stopThread() {
            isRunning = false;
        }

        public boolean isRunning() {
            return isRunning;
        }

        @Override
        public void run() {

            Log.i(TAG, "===start DecodeThread===");

            //存放目标文件的数据
            ByteBuffer byteBuffer = null;
            //解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            long startMs = System.currentTimeMillis();
            byte[] bytes = null;
            while (isRunning) {

                if (mFrmList.isEmpty()) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                bytes = mFrmList.remove(0);

                //1 准备填充器
                int inIndex = mMediaCodec.dequeueInputBuffer(0);

                if (inIndex >= 0) {
                    //2 准备填充数据
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                        byteBuffer = mMediaCodec.getInputBuffers()[inIndex];
                        byteBuffer.clear();
                    } else {
                        byteBuffer = mMediaCodec.getInputBuffer(inIndex);
                    }

                    if (byteBuffer == null) {
                        continue;
                    }

                    byteBuffer.put(bytes, 0, bytes.length);
                    //3 把数据传给解码器
                    mMediaCodec.queueInputBuffer(inIndex, 0, bytes.length, 0, 0);

                } else {
                    SystemClock.sleep(50);
                    continue;
                }

                //这里可以根据实际情况调整解码速度
                long sleep = 50;

                if (mFrmList.size() > 20) {
                    sleep = 0;
                }

                SystemClock.sleep(sleep);

                //4 开始解码
                int outIndex = mMediaCodec.dequeueOutputBuffer(info, 0);

                if (outIndex >= 0) {

                    //帧控制
                    while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    boolean doRender = (info.size != 0);

                    //对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
                    //调用这个api之后,SurfaceView才有图像
                    mMediaCodec.releaseOutputBuffer(outIndex, doRender);

                    if (mOnDecodeListener != null) {
                        mOnDecodeListener.decodeResult(mVideoWidth, mVideoHeight);
                    }

                    System.gc();

                }
            }

            Log.i(TAG, "===stop DecodeThread===");
        }

    }

好!一般的硬解码就这样搞定了

三 开始踏坑

当我美滋滋的写完最简单的demo之后,被测试人员测试出60%的crash/anr/不出图。我顿时感觉人生都黑暗了。下面列举一些常见问题

1.最常见问题:部分机型MediaCodec.configure直接crash

这是最常见的问题,有机型一调用这个api就直接crash,贼尴尬。这个api的第一个参数是MediaFormat,我们翻到MediaFormat的初始化源码。最后两个参数就是视频流的预设宽高,如果这个值高于当前手机支持的解码最大分辨率(后文称max),那么在调用MediaCodec.configure的时候就会crash。

这里写图片描述

把MediaFormat.createVideoFormat时候的宽高设置小一点就ok了。
那么就会有另外一个问题,就是如果我设置1080*720的后,视频流来了一个1920*1080的会不会有影响?如果当前设备的max高于这个值,就算预设值不一样,也还是可以正常解码并显示1290*1080的画面。那么如果低于这个值呢?两种情况 绿屏/MediaCodec.dequeueInputBuffer的值一直抛IllegalStateException

2.如何获取当前手机支持的解码最大分辨率

上面已经解释了为什么画面会绿屏,是因为视频超过了max这个值,那么问题来了,怎么知道手机支持的最大分辨率。
adb pull /system/etc/media_codecs.xml (your path)
每个手机下都有这样一个文件,使用上面的adb命令后就可以拿到了。这是一个xml文件,可以直接看到MediaCodecs–>Decoders节点下的各个视频格式的支持情况
这里写图片描述
既然知道是xml文件,那就直接进行xml解析就可以在app里面拿到max数据啦~

3.如何获取解码视频的宽和高

如果不能确定视频流的分辨率,如何获取解码后的宽高呢?在MediaCodec.releaseOutputBuffer显示图像之前,调用以下api就可以获取到啦

MediaFormat newFormat = mMediaCodec.getOutputFormat();
                            int videoWidth = newFormat.getInteger("width");
                            int videoHeight = newFormat.getInteger("height");

4.部分机型MediaCodec.dequeueInputBuffer 一直IllegalStateException

我们上面解码的时候有这么一行:mMediaCodec.dequeueInputBuffer(0)
我们写入的参数long timeoutUs是0,其实是不对的,需要填入一个时间戳,可以直接写当前系统时间。因为部分机型需要这个时间戳来进行计算,不然就会一直小于0。

5.部分机型MediaCodec.dequeueOutputBuffer报IllegalStateException之后MediaCodec.dequeueInputBuffer一直报IllegalStateException(timeoutUs参数已填入系统时间)

该机型硬解码最大配置分辨率低于当前视频流的分辨率

6.部分机型卡死在MediaCodec.dequeueOutputBuffer

后面的timeoutUs参数不能跟dequeueInputBuffer的timeoutUs参数一样,写0即可

7.部分机型卡死在切换分辨率后卡死在MediaCodec.dequeueInputBuffer

目前有一些视频流在切到高分辨率后,解码线程会直接卡死在MediaCodec.dequeueInputBuffer这个api,目前没有更好的解决办法,只能在获取到设备在切分辨率后,重新开始解码

四 终稿

private class DecodeThread extends Thread {

        private boolean isRunning = true;

        public synchronized void stopThread() {
            isRunning = false;
        }

        public boolean isRunning() {
            return isRunning;
        }

        @Override
        public void run() {

            Log.i(TAG, "===start DecodeThread===");

            //存放目标文件的数据
            ByteBuffer byteBuffer = null;
            //解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            long startMs = System.currentTimeMillis();
            DataInfo dataInfo = null;
            while (isRunning) {

                if (mFrmList.isEmpty()) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                dataInfo = mFrmList.remove(0);

                long startDecodeTime = System.currentTimeMillis();

                //1 准备填充器
                int inIndex = -1;

                try {
                    inIndex = mMediaCodec.dequeueInputBuffer(dataInfo.receivedDataTime);
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                    Log.e(TAG, "IllegalStateException dequeueInputBuffer ");
                    if (mSupportListener != null) {
                        mSupportListener.UnSupport();
                    }
                }

                if (inIndex >= 0) {
                    //2 准备填充数据
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                        byteBuffer = mMediaCodec.getInputBuffers()[inIndex];
                        byteBuffer.clear();
                    } else {
                        byteBuffer = mMediaCodec.getInputBuffer(inIndex);
                    }

                    if (byteBuffer == null) {
                        continue;
                    }

                    byteBuffer.put(dataInfo.mDataBytes, 0, dataInfo.mDataBytes.length);
                    //3 把数据传给解码器
                    mMediaCodec.queueInputBuffer(inIndex, 0, dataInfo.mDataBytes.length, 0, 0);

                } else {
                    SystemClock.sleep(50);
                    continue;
                }

                //这里可以根据实际情况调整解码速度
                long sleep = 50;

                if (mFrmList.size() > 20) {
                    sleep = 0;
                }

                SystemClock.sleep(sleep);


                int outIndex = MediaCodec.INFO_TRY_AGAIN_LATER;

                //4 开始解码
                try {
                    outIndex = mMediaCodec.dequeueOutputBuffer(info, 0);
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                    Log.e(TAG, "IllegalStateException dequeueOutputBuffer " + e.getMessage());
                }

                if (outIndex >= 0) {

                    //帧控制
                    while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    boolean doRender = (info.size != 0);

                    //对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
                    //调用这个api之后,SurfaceView才有图像
                    mMediaCodec.releaseOutputBuffer(outIndex, doRender);

                    if(mOnDecodeListener != null){
                        mOnDecodeListener.decodeResult(mVideoWidth, mVideoHeight);
                    }

                    Log.i(TAG, "DecodeThread delay = " + (System.currentTimeMillis() - dataInfo.receivedDataTime) + " spent = " + (System.currentTimeMillis() - startDecodeTime) + " size = " + mFrmList.size());
                    System.gc();

                } else {
                    switch (outIndex) {
                        case MediaCodec.INFO_TRY_AGAIN_LATER: {

                        }
                        break;
                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: {
                            MediaFormat newFormat = mMediaCodec.getOutputFormat();
                            mVideoWidth = newFormat.getInteger("width");
                            mVideoHeight = newFormat.getInteger("height");

                            //是否支持当前分辨率
                            String support = MediaCodecUtils.getSupportMax(mMimeType);
                            if (support != null) {
                                String width = support.substring(0, support.indexOf("x"));
                                String height = support.substring(support.indexOf("x") + 1, support.length());
                                Log.i(TAG, " current " + mVideoWidth + "x" + mVideoHeight + " mMimeType " + mMimeType);
                                Log.i(TAG, " Max " + width + "x" + height + " mMimeType " + mMimeType);
                                if (Integer.parseInt(width) < mVideoWidth || Integer.parseInt(height) < mVideoHeight) {
                                    if (mSupportListener != null) {
                                        mSupportListener.UnSupport();
                                    }
                                }
                            }
                        }
                        break;
                        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: {

                        }
                        break;
                        default: {

                        }
                    }
                }
            }

            Log.i(TAG, "===stop DecodeThread===");
        }

    }

有任何问题欢迎指出
附上完整demo,已经包含h264的本地资源,下载即可跑
http://download.csdn.net/download/u012521570/10155781

  • 6
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值