Android 用Mediacodec硬解碼視頻包AVpacket

 

我的視頻課程(基礎):《(NDK)FFmpeg打造Android萬能音頻播放器》

我的視頻課程(進階):《(NDK)FFmpeg打造Android視頻播放器》

我的視頻課程(編碼直播推流):《Android視頻編碼和直播推流》

 

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,就這樣了:多搗鼓總會成功的!

 

 

 


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章