Android 用Mediacodec硬解码视频包AVpacket
我的视频课程(基础):《(NDK)FFmpeg打造Android万能音频播放器》
我的视频课程(进阶):《(NDK)FFmpeg打造Android视频播放器》
我的视频课程(编码直播推流):《Android视频编码和直播推流》
我的视频课程(C++ OpenGL):《Android C++ OpenGL教程》
FFmpeg是一个很不错的开源的音视频编解码库,其编解码器几乎涵盖所有格式的音视频。但是它是利用CPU来编解码的,在PC等设备上面解码能力还能满足需求,但是在移动设备上面解码720p及其以上的视频时就显得很尴尬了,解码速度不够导致解码视频帧的速度太慢,造成播放卡顿并且耗电也快。如果能用移动设备上的GPU来解码视频帧的话,那效率将会提高很多倍的,这就需要用到硬解码器MediaCodec了。
FFmpeg解码出AVpacket的速度是完全够的,因此我们就会想如果我们能用MediaCodec来解码AVpacket包里面的视频原始压缩数据的话,那就能播放高清视频了并且还不会太耗电。幸好,经过测试,这种方式是完全可行的。
那么开始我们的MediaCodec解码AVpacket之旅吧。还是先看效果:都为720p的视频(源码下载wlplayer)
一、Mediacodec解码过程
1.1、首先的配置MediaFormat,来告诉MediaCodec解码的视频时怎样的,有哪些信息,如下代码:
public void mediacodecInit(int mimetype, int width, int height, byte[] csd0, byte[] csd1) { if(surface != null) { try { wlGlSurfaceView.setCodecType(1); String mtype = getMimeType(mimetype); mediaFormat = MediaFormat.createVideoFormat(mtype, width, height); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height); mediaFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, width * height); mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(csd0)); mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(csd1)); Log.d("ywl5320", mediaFormat.toString()); mediaCodec = MediaCodec.createDecoderByType(mtype); if(surface != null) { mediaCodec.configure(mediaFormat, surface, null, 0); mediaCodec.start(); } } catch (Exception e) { e.printStackTrace(); } } else { if(wlOnErrorListener != null) { wlOnErrorListener.onError(WlStatus.WL_STATUS_SURFACE_NULL, "surface is null"); } } }
参数mimeType:是告诉MediaCodec要解码的视频的编码格式,如:video/avc、video/hevc等。
width和height:表示视频的宽和长。
csd0和csd1:都对应于AVCodecContext里面的extradata字段。
这样就配置好了MediaFormat。
1.2、MediaCodec解码AVpacket:
MediaCodec解码视频的过程为:其里面有2个Buffer队列,一个是InputBuffer队列,负责把视频压缩数据送给MediaCodec解码器解码,然后清空数据并以此循环指定结束;另一个是OutputBuffer队列,负责把MediaCodec解码后的数据给surface渲染,然后清空数据并以此循环指定结束;说白了就是一个负责往MediaCodec喂数据,一个负责把MediaCodec(排出的数据)送给surface渲染,循环这个过程,就能播放视频了。
了解了MediaCodec的解码过程,我们就知道从何入手了,就在喂数据(InputBuffer)那里开刀,获取MediaCodec的InputBuffer,然后把AVpacket里面的视频压缩数据添加到里面,并用queueInputBuffer方法送给MediaCodec,这样MediaCodec就有了解码的原始数据,那么代码怎么写呢:
public void mediacodecDecode(byte[] bytes, int size, int pts) { if(bytes != null && mediaCodec != null && info != null) { try { int inputBufferIndex = mediaCodec.dequeueInputBuffer(10000); if(inputBufferIndex >= 0) { ByteBuffer byteBuffer = mediaCodec.getInputBuffers()[inputBufferIndex]; byteBuffer.clear(); byteBuffer.put(bytes); mediaCodec.queueInputBuffer(inputBufferIndex, 0, size, pts, 0); } int index = mediaCodec.dequeueOutputBuffer(info, 10000); if (index >= 0) { //ByteBuffer buffer = mediaCodec.getOutputBuffers()[index]; //buffer.position(info.offset); //buffer.limit(info.offset + info.size); mediaCodec.releaseOutputBuffer(index, true); } }catch (Exception e) { e.printStackTrace(); } } }
上面代码参数byte[]就是从C++传过来的AVpacket里面的视频原始压缩数据,然后获取InputBuffer并把byte数据添加到里面,最好送给MediaCodec。
解码过程没什么变化,和官方过程一样。
二、C++提供AVpacket数据:
2.1、封装调用Java的方法:
void WlJavaCall::onDecMediacodec(int type, int size, uint8_t *packet_data, int pts) { if(type == WL_THREAD_CHILD) { JNIEnv *jniEnv; if(javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {// LOGE("%s: AttachCurrentThread() failed", __FUNCTION__); return; } jbyteArray data = jniEnv->NewByteArray(size); jniEnv->SetByteArrayRegion(data, 0, size, (jbyte*)packet_data); jniEnv->CallVoidMethod(jobj, jmid_dec_mediacodec, data, size, pts); jniEnv->DeleteLocalRef(data); javaVM->DetachCurrentThread(); } else { jbyteArray data = jniEnv->NewByteArray(size); jniEnv->SetByteArrayRegion(data, 0, size, (jbyte*)data); jniEnv->CallVoidMethod(jobj, jmid_dec_mediacodec, data, size, pts); jniEnv->DeleteLocalRef(data); }}
这里分了主线程和子线程,不过解码是在子线程的,所以不会用到主线程的。
2.2、添加数据头:
因为AVpacket里面的压缩数据是很纯粹的,这种数据MediaCodec是不能解码或者解码出来也不能播放的,因此需要将AVpacket添加相应的数据头,这就要用到FFmpeg的av_bitstream_filter_filter方法,如:
mimType = av_bitstream_filter_init("h264_mp4toannexb");if(mimType != NULL && !isavi) { uint8_t *data; av_bitstream_filter_filter(mimType, pFormatCtx->streams[wlVideo->streamIndex]->codec, NULL, &data, &packet->size, packet->data, packet->size, 0); uint8_t *tdata = NULL; tdata = packet->data; packet->data = data; if(tdata != NULL) { av_free(tdata); } }
注:这里会导致内存泄漏,经过av_bitstream_filter_filter处理的AVpacket的data的地址和原来的是不一样的,不释放原来的地址就会造成内存泄漏。
2.3、传递AVpacket数据到MediaCodec播放:
wljavaCall->onDecMediacodec(WL_THREAD_CHILD, packet->size, packet->data, clock);
直接把AVpacket的size和data传给MediaCodec就可以了。
2.4、释放AVpacket
由于我们在解复用时对AVpacket的data进行了操作,如果直接av_packet_free的话,会报释放地址错误,所以这里就单独释放AVpacket里面的指针就行了:
av_free(packet->data); av_free(packet->buf); av_free(packet->side_data); packet = NULL;
完整实例可参考:wlplayer
OK,就这样了:多捣鼓总会成功的!
更多相关文章
- Android开发视频教程-深入浅出系列Lesson20-AFF111029学习Androi
- android的多媒体数据库
- android 启动过程及init.rc
- Android 根文件系统启动过程
- android主要有5中数据存储方式
- Android视频开发浅析
- Android数据存储--sqlite
- Android FrameWork――Touch事件派发过程详解
- android 横竖屏切换属性和播放视频全屏切换