Android 用Mediacodec硬解码视频包AVpacket

3,533 阅读4分钟
原文链接: blog.csdn.net

        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,就这样了:多捣鼓总会成功的!