一 前言

最近在看一些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。

Android 硬解码MediaCodec配合SurfaceView的踏坑之旅_第1张图片

把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节点下的各个视频格式的支持情况
Android 硬解码MediaCodec配合SurfaceView的踏坑之旅_第2张图片
既然知道是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

更多相关文章

  1. 【Android 内存优化】Android 工程中使用 libjpeg-turbo 压缩图
  2. Android(线程二) 线程池详解
  3. 在Android中利用SQLite实现对数据的增删查改
  4. Android利用Handler异步获取子线程中的产生的值
  5. Android操作SQLite数据库(增、删、改、查、分页等)及ListView显
  6. Android 单线程模型详解及实例
  7. Android中使用Thread+Handler实现非UI线程更新UI界面

随机推荐

  1. Android车载方案公司,你该何去何从?
  2. Android开发签名问题
  3. [置顶] Android中的XML解析与生成——SAX
  4. 谷歌无限期推迟Android(安卓)2.1操作平台
  5. Android(安卓)studio 错误提示,英文转中文
  6. Android切近实战(二)
  7. 关于 apk文件反编译的方法(dex2jar和JD-G
  8. android手机安全问题汇总(非技术)
  9. 自动裁剪Android(安卓)ICON并保存到对应
  10. Android禁止ViewPager的左右滑动