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