視頻學習筆記:Android ffmpeg解碼多路h264視頻並顯示

背景

Android設備上使用ffmpeg解碼多路h264視頻,抽取了一個簡單demo方便日後參考,在此記錄一下。demo中主要涉及以下功能:

1.ffmpeg解碼h264視頻爲yuv幀
2.使用ffmpeg將yuv幀轉換爲可以在畫布上渲染的rgb幀
3.將Android的SurfaceView類傳入jni層並使用rgb幀進行渲染
4.使用java類包裝c++類,多線程解碼多路視頻
5.集成了opencv相關功能,在本例中可以使用相關api保存arg幀

其中解碼部分代碼參考了雷神博客的相關文章,並已經在項目中多處使用,surfaceview渲染部分參考了ijkplayer中的相關源碼。項目地址如下,具體功能可以參考源碼。

項目地址:https://git.oschina.net/vonchenchen/android_ffmpge_muti_decode.git

下面簡單介紹一下工程的主要內容

功能實現

總體功能

首先需要使用ndk編譯ffmpeg源碼,這部分網上已經有比較多的介紹,在此不再贅述,編譯好的文件已經在工程中可以直接使用。加載ffmpeg動態鏈接庫的命令在Android.mk中,內容如下:

#ffmpeg
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/libffmpeg.so
include $(PREBUILT_SHARED_LIBRARY)

也就是直接將其copy到jniLibs文件的對應目錄即可。

decoder.cpp文件負責具體調用ffmpeg的功能進行解碼。之前的項目中一直使用這個類,直接定義一個變量並調用相關解碼方法。對於解碼多路視頻,如果直接在jni文件中定義多個對象則顯得比較囉嗦,如果能使用java類包裝一下decode類,需要一個decode類就new一個對應的java類,不需要時讓java回收,這是比較理想的,對此專門學習了一下這種實現,原理見這裏的另外一篇博客http://blog.csdn.net/lidec/article/details/72872037

當我們解碼完畢後,需要把rgb幀渲染到畫布上,這個過程參考了ijkplayer的實現,將surface作爲一個參數傳入jni層,拿到surface的緩衝區後將生成的rgb數據直接copy到這個緩衝區即可完成顯示。相關ndk的api可以參考ndk文檔,鏈接https://developer.android.com/ndk/reference/group___native_activity.html。這裏注意,在編譯時,LOCAL_LDLIBS中需要加入-ljnigraphics -landroid兩個庫。

解碼與顯示

這個類參考了雷神的博客,使用c++簡單封裝了一下。主幹功能如下

int decoder::decodeFrame(const char *data, int length, void (*handle_data)(AVFrame *pFrame, void *param, void *ctx), void *ctx) {

    int cur_size = length;
    int ret = 0;

    memcpy(inbuf, data, length);
    const uint8_t *cur_ptr = inbuf;
    // Parse input stream to check if there is a valid frame.
    //std::cout << " in data  --  -- " << length<< std::endl;
    while(cur_size >0)
    {
        int parsedLength = av_parser_parse2(parser, codecContext, &avpkt.data,
                &avpkt.size, (const uint8_t*)cur_ptr , cur_size, AV_NOPTS_VALUE,
                AV_NOPTS_VALUE, AV_NOPTS_VALUE);
        cur_ptr += parsedLength;
        cur_size -= parsedLength;

        ...

        if (!avpkt.size) {
            continue;
        } else {

                int len, got_frame;
                len = avcodec_decode_video2(codecContext, frame, &got_frame,
                        &avpkt);

                if (len < 0) {
                   ...
                }

                if (got_frame) {
                    frame_count++;

                    LOGE("frame %d", frame_count);

                    if(img_convert_ctx == NULL){
                        img_convert_ctx = sws_getContext(codecContext->width, codecContext->height,
                                                         codecContext->pix_fmt, codecContext->width, codecContext->height,
                                                         pixelFormat, SWS_BICUBIC, NULL, NULL, NULL);

                        ...
                    }

                    if(img_convert_ctx != NULL) {
                        handle_data(frame, renderParam, ctx);
                    }
                }
        }
    }
    return length;
}

這裏簡單進行介紹,傳入h264視頻buffer後利用av_parser_parse2解析出h264頭在當前buffer中的偏移量,然後使用avcodec_decode_video2函數進行解碼,最後得到一個AVFrame幀,這個幀就是h264流中的yuv幀。拿到這個幀和其他相關信息後,我們將這些內容傳遞給handle_data這個函數指針,由外面傳入的handle_data函數處理生成的yuv幀。
幀處理回調在com_vonchenchen_android_video_demos_codec_CodecWrapper.cpp中實現,這個函數主要完成將yuv數據使用ffmpeg轉換爲rgb幀,並且獲取surface的緩衝區,將rgb拷貝到這段緩衝區中。具體實現如下:

void handle_data(AVFrame *pFrame, void *param, void *ctx){

    RenderParam *renderParam = (RenderParam *)param;

    //yuv420p轉換爲rgb565
    AVFrame *rgbFrame = yuv420p_2_argb(pFrame, renderParam->swsContext, renderParam->avCodecContext, pixelFormat);//AV_PIX_FMT_RGB565LE

    LOGE("width %d height %d",rgbFrame->width, rgbFrame->height);

    //for test decode image
    //save_rgb_image(rgbFrame);

    //用於傳遞上下文信息
    EnvPackage *envPackage = (EnvPackage *)ctx;

    //創建一個用來維護顯示緩衝區的變量
    ANativeWindow_Buffer nwBuffer;
    //將java中的Surface對象轉換爲ANativeWindow指針
    ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(envPackage->env, *(envPackage->surface));
    if (aNativeWindow == NULL) {
        LOGE("ANativeWindow_fromSurface error");
        return;
    }

    //用來縮放rgb幀與顯示窗口的大小,使得rgb數據可以適應窗口大小
    int retval = ANativeWindow_setBuffersGeometry(aNativeWindow, rgbFrame->width, rgbFrame->height,  WINDOW_FORMAT_RGB_565);

    //鎖定surface 
    if (0 != ANativeWindow_lock(aNativeWindow, &nwBuffer, 0)) {
        LOGE("ANativeWindow_lock error");
        return;
    }

    //將rgb數據拷貝給suface的緩衝區
    if (nwBuffer.format == WINDOW_FORMAT_RGB_565) {
        memcpy((__uint16_t *) nwBuffer.bits, (__uint16_t *)rgbFrame->data[0], rgbFrame->width * rgbFrame->height *2);
    }

    //解鎖surface並顯示新的緩衝區
    if(0 !=ANativeWindow_unlockAndPost(aNativeWindow)){
        LOGE("ANativeWindow_unlockAndPost error");
        return;
    }
    //清理垃圾
    ANativeWindow_release(aNativeWindow);

    //清理rgb幀結構以及幀結構所指向的rgb緩衝區
    av_free(rgbFrame->data[0]);
    av_free(rgbFrame);
}

yuv轉rgb

AVFrame *yuv420p_2_argb(AVFrame *frame, SwsContext *swsContext, AVCodecContext *avCodecContext, enum AVPixelFormat format){
    AVFrame *pFrameRGB = NULL;
    uint8_t  *out_bufferRGB = NULL;
    pFrameRGB = av_frame_alloc();

    pFrameRGB->width = frame->width;
    pFrameRGB->height = frame->height;

    //給pFrameRGB幀加上分配的內存;  //AV_PIX_FMT_ARGB
    int size = avpicture_get_size(format, avCodecContext->width, avCodecContext->height);
    //out_bufferRGB = new uint8_t[size];
    out_bufferRGB = av_malloc(size * sizeof(uint8_t));
    avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, format, avCodecContext->width, avCodecContext->height);
    //YUV to RGB
    sws_scale(swsContext, frame->data, frame->linesize, 0, avCodecContext->height, pFrameRGB->data, pFrameRGB->linesize);

    return pFrameRGB;
}

總結

本文簡單介紹了Android ffmpeg解碼多路h264視頻並顯示的流程並附有完整示例,希望能夠給有相關需求並且還在探索的同學一個參考,也希望大家多多指正,一起進步。

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