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
更多相关文章
- 【Android 内存优化】Android 工程中使用 libjpeg-turbo 压缩图
- Android(线程二) 线程池详解
- 在Android中利用SQLite实现对数据的增删查改
- Android利用Handler异步获取子线程中的产生的值
- Android操作SQLite数据库(增、删、改、查、分页等)及ListView显
- Android 单线程模型详解及实例
- Android中使用Thread+Handler实现非UI线程更新UI界面