Android NDK MediaCodec在ijkplayer中的實踐

從API 21(Android 5.0)開始Android提供C層的NDK MediaCodec的接口。

Java MediaCodec是對NDK MediaCodec的封裝,ijkplayer硬解通路一直使用的是Java MediaCodecSurface的方式。

本文的主要內容是:在ijkplayer框架內適配NDK MediaCodec,不再使用Surface輸出,改用YUV輸出達到軟硬解通路一致的渲染流程。

下文提到的Java MediaCodec,如果不做特別說明,都指的Surface 輸出。
下文提到的NDK MediaCodec,如果不做特別說明,都指的YUV 輸出。

1. ijkplayer硬解碼的過程

在增加NDK MediaCodec硬解流程之前,先簡要說明Java MediaCodec的流程:

Android Java MediaCodec

圖中主要有三個步驟:AVPacket->Decode->AVFrame;

  1. read線程讀到packet,放入packet queue;
  2. 解碼得到一幀AVFrame,放入picture queue;
  3. picture queue取出一幀,渲染AVFrame(overlay)。

數據來源AVPacket不變,目標AVFrame不變,現在我們將步驟2 Decode中的Java Mediacodec替換成 Ndk Mediacodec ,其他地方都不需要改動。
但是有一點需要注意:我們從NDK MediaCodec得到的YUV數據,并不是像Java Mediacodec得到的是一個index,所以NDK MediaCodec解碼后渲染部分和軟解流程一樣,都是基于OpenGL。

1.1 打開視頻流

stream_component_open()函數打開解碼器,以及創建解碼線程:

//ff_ffplayer.c
static int stream_component_open(FFPlayer *ffp, int stream_index)
{
    ......
    codec = avcodec_find_decoder(avctx->codec_id);
    ......
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    ......  
    case AVMEDIA_TYPE_VIDEO:
        ......
        decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
        ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
        if (!ffp->node_vdec)
            goto fail;
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;       
    ......
}

FFmpeg軟解碼器默認打開,接著由IJKFF_Pipeline(IOS/Android),創建ffpipeline_open_video_decoder硬解解碼器結構體IJKFF_Pipenode。

1.2 創建解碼器

ffpipeline_open_video_decoder()會根據設置創建硬解碼器或軟解碼器IJKFF_Pipenode

//ffpipeline_android.c
static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;

    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }

    return node;
}

硬解碼器創建失敗會切到軟解碼器。

1.3 啟動解碼線程

啟動解碼線程decoder_start()

  //ff_ffplayer.c
int ffpipenode_run_sync(IJKFF_Pipenode *node)
{
    return node->func_run_sync(node);
}

IJKFF_Pipenode會根據func_run_sync函數指針,具體啟動軟解還是硬解線程。

1.4 解碼線程工作

//ffpipenode_android_mediacodec_vdec.c
static int func_run_sync(IJKFF_Pipenode *node)
{
...
    opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");
...
while (!q->abort_request) {
  ...
          ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &got_frame);
...
            ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
...
   }
}
  1. 可以看到解碼線程又創建了子線程,enqueue_thread_func()主要是用來將壓縮數據(H.264/H.265)放入解碼器,這樣往解碼器放數據在enqueue_thread_func()里面,從解碼器取數據在func_run_sync()里面;
  2. drain_output_buffer()從解碼器取出一個AVFrame,但是這個AVFrame->dataNULL并沒有數據,其中AVFrame->opaque指針指向一個SDL_AMediaCodecBufferProxy結構體:
struct SDL_AMediaCodecBufferProxy
{
    int buffer_id;
    int buffer_index;
    int acodec_serial;
    SDL_AMediaCodecBufferInfo buffer_info;
};

這些成員由硬解器SDL_AMediaCodecFake_dequeueOutputBuffer得來,它們在視頻渲染的時候會用到;

  1. 將AVFrame放入待渲染隊列。

2. 增加NDK MediaCodec解碼

根據上面的解碼流程,增加NDK MediaCodec就只需2個關鍵步驟:

  1. 創建IJKFF_Pipenode;
  2. 創建相應的解碼線程。

2.1 新建pipenode

NDK MediaCodec創建一個IJKFF_Pipenode。在func_open_video_decoder()打開解碼器時,軟件解碼器和Java Mediacodec都需要創建一個IJKFF_Pipenode,其中IJKFF_Pipenode->opaque為自定義的解碼結構體指針,所以定義一個IJKFF_Pipenode_Ndk_MediaCodec_Opaque結構體。

 //ffpipenode_android_ndk_mediacodec_vdec.c
typedef struct IJKFF_Pipenode_Ndk_MediaCodec_Opaque {
    FFPlayer                 *ffp;
    IJKFF_Pipeline           *pipeline;
    Decoder                  *decoder;
    SDL_Vout                 *weak_vout;
    SDL_Thread               _enqueue_thread;
    SDL_Thread               *enqueue_thread;

    ijkmp_mediacodecinfo_context mcc;

    char                      acodec_name[128];
    int                       frame_width;
    int                       frame_height;
    int                       frame_rotate_degrees;

    AVCodecContext           *avctx; // not own
    AVBitStreamFilterContext *bsfc;  // own
    size_t                    nal_size;
    AMediaFormat *ndk_format;
    AMediaCodec  *ndk_codec;
} IJKFF_Pipenode_Ndk_MediaCodec_Opaque;

里面有兩個比較重要的成員AMediaFormat、AMediaCodec,他們就是native層的編解碼器和媒體格式。定義函數ffpipenode_create_video_decoder_from_android_ndk_mediacodec()創建IJKFF_Pipenode

 //ffpipenode_android_ndk_mediacodec_vdec.c
IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_ndk_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout)
{
    if (SDL_Android_GetApiLevel() < IJK_API_21_LOLLIPOP)
        return NULL;
    IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Ndk_MediaCodec_Opaque));
    if (!node)
        return node;
    ... 
    IJKFF_Pipenode_Ndk_MediaCodec_Opaque *opaque = node->opaque;
    node->func_destroy  = func_destroy;
    node->func_run_sync = func_run_sync;
    opaque->ndk_format = AMediaFormat_new();
    ...
    AMediaFormat_setString(opaque->ndk_format , AMEDIAFORMAT_KEY_MIME, opaque->mcc.mime_type);
    AMediaFormat_setBuffer(opaque->ndk_format , "csd-0", convert_buffer, sps_pps_size);
    AMediaFormat_setInt32(opaque->ndk_format , AMEDIAFORMAT_KEY_WIDTH, opaque->avctx->width);
    AMediaFormat_setInt32(opaque->ndk_format , AMEDIAFORMAT_KEY_HEIGHT, opaque->avctx->height);
    AMediaFormat_setInt32(opaque->ndk_format , AMEDIAFORMAT_KEY_COLOR_FORMAT, 19);
    opaque->ndk_codec = AMediaCodec_createDecoderByType(opaque->mcc.mime_type); 
   
    if (AMediaCodec_configure(opaque->ndk_codec, opaque->ndk_format, NULL, NULL, 0) != AMEDIA_OK)
        goto fail;

    return node;
fail:
    ffpipenode_free_p(&node);
    return NULL;
}

NDK MediaCodec的接口和Java MediaCodec的接口是一樣的 。然后打開解碼器就可以改為:

//ffpipeline_android.c
static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;

    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_ndk_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }

    return node;
}

2.2 創建解碼線程func_run_sync

func_run_sync()也會再創建一個子線程enqueue_thread_func(),用于往解碼器放數據:

  //ffpipenode_android_ndk_mediacodec_vdec.c
static int func_run_sync(IJKFF_Pipenode *node)
{
    ...
    AMediaCodec_start(c);
    opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");  
    AVFrame* frame = av_frame_alloc();
    AMediaCodecBufferInfo info;
    ...
    while (!q->abort_request) {
        outbufidx = AMediaCodec_dequeueOutputBuffer(c, &info, AMC_OUTPUT_TIMEOUT_US);
        if (outbufidx >= 0)
        {
            size_t size;
            uint8_t* buffer = AMediaCodec_getOutputBuffer(c, outbufidx, &size);
            if (size)
            {
                int num;
                AMediaFormat *format = AMediaCodec_getOutputFormat(c); 
                AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, &num) ;
                if (num == 19)//YUV420P
                {
                    frame->width = opaque->avctx->width;
                    frame->height = opaque->avctx->height;
                    frame->format = AV_PIX_FMT_YUV420P;
                    frame->sample_aspect_ratio = opaque->avctx->sample_aspect_ratio;
                    frame->pts = info.presentationTimeUs;
                    double frame_pts = frame->pts*av_q2d(AV_TIME_BASE_Q);
                    double duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
                    av_frame_get_buffer(frame, 1);
                    memcpy(frame->data[0], buffer, frame->width*frame->height);
                    memcpy(frame->data[1], buffer+frame->width*frame->height, frame->width*frame->height/4);
                    memcpy(frame->data[2], buffer+frame->width*frame->height*5/4, frame->width*frame->height/4);
                    ffp_queue_picture(ffp, frame, frame_pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
                    av_frame_unref(frame);
                }
                else if (num == 21)// YUV420SP
                {
                }
            }
            AMediaCodec_releaseOutputBuffer(c,  outbufidx, false);
        }
        else {
            switch (outbufidx) {
                case AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED: {
                    AMediaFormat *format = AMediaCodec_getOutputFormat(c);   

                    int pix_format = -1;
                    int width =0, height =0;
                    AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &width);
                    AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, &height);
                    AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, &pix_format);
                    break;
                }
                case AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED:
                    break;
                case AMEDIACODEC_INFO_TRY_AGAIN_LATER:
                    break;
                default:
                    break;
            }
        }
    }

fail:
    av_frame_free(&frame);

    SDL_WaitThread(opaque->enqueue_thread, NULL);
    ALOGI("MediaCodec: %s: exit: %d", __func__, ret);
    return ret;
}
  1. 從解碼器拿到解碼后的數據buffer;
  2. 填充AVFrame結構體,申請相應大小的內存,由于我們設置解碼器的輸出格式是YUV420P,所以frame->format = AV_PIX_FMT_YUV420P,然后將buffer拷貝到frame->data;
  3. 放入待渲染隊列ffp_queue_picture,至此渲染線程就能像軟解一樣取到AVFrame。
 //ffpipenode_android_ndk_mediacodec_vdec.c
static int enqueue_thread_func(void *arg)
{
    ...
    while (!q->abort_request)
    {
        do
        {
            ...
            if (ffp_packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0) {
                ret = -1;
                goto fail;
            }
        }while(ffp_is_flush_packet(&pkt) || d->queue->serial != d->pkt_serial);

        if (opaque->avctx->codec_id == AV_CODEC_ID_H264 || opaque->avctx->codec_id == AV_CODEC_ID_HEVC) {
            convert_h264_to_annexb(pkt.data, pkt.size, opaque->nal_size, &convert_state);
            ...
        }

        ssize_t id = AMediaCodec_dequeueInputBuffer(c, AMC_INPUT_TIMEOUT_US);
        if (id >= 0)
        {
            uint8_t *buf = AMediaCodec_getInputBuffer(c, (size_t) id, &size);
            if (buf != NULL && size >= pkt.size) {
                memcpy(buf, pkt.data, (size_t)pkt.size);
                media_status = AMediaCodec_queueInputBuffer(c, (size_t) id, 0, (size_t) pkt.size,
                                                            (uint64_t) time_stamp,
                                                            keyframe_flag);
                if (media_status != AMEDIA_OK) {
                    goto fail;
                }
            }
        }
        av_packet_unref(&pkt);
    }
fail:
    return 0;
}

往解碼器放數據在enqueue_thread_func()線程里面,解碼的整體流程和Java MediaCodec一樣

2.3 其他需要修改的地方

修改Android.mk

LOCAL_LDLIBS += -llog -landroid -lmediandk
LOCAL_SRC_FILES += android/pipeline/ffpipenode_android_ndk_mediacodec_vdec.c

如果提示media/NdkMediaCodec.h找不到,可能是因為API級別<21,修改Application.mk:

APP_PLATFORM := android-21

3. 性能分析

測試情況使用的設備為Oppo R11 Plus(Android 7.1.1),測試序列H. 264 (1920x1080 25fps)視頻,Java MediaCodecNDK MediaCodec解碼時CPU及GPU的表現:

Java MediaCodec CPU 占用大約在5%左右

Java MediaCodec解碼CPU表現

NDK MediaCodec CPU占用大約在12%左右

NDK MediaCodec解碼CPU表現

Java MediaCodec GPU占用表現

Java MediaCodec解碼GPU表現

NDK MediaCodec GPU占用表現

NDK MediaCodec解碼GPU表現

3.1 測試數據分析

NDK MediaCodecCPU占比大約高出7%,但是GPU表現較好。

CPU為什么會比Java MediaCodec解碼時高呢?
我們這里一直評估的Java MediaCodec,都指的Surface輸出。這意味著接口內部完成了解碼和渲染工作,高度封裝的解碼和渲染,內部做了一些數據傳遞優化的工作。同時ijkplayer進程的CPU占用并不能體現MediaCodec本身的耗用。

3.2 后續優化

有一個原因是不可忽略的:在從解碼器拿到buffer時,會先申請內存,然后拷貝得到AVFrame。但這一步也可以優化,直接將buffer指向AVFrame->data,然后在OpenGL渲染完成之后,調用AMediaCodec_releaseOutputBufferbuffer還給解碼器,這樣就需要修改渲染的代碼,不能做到軟硬解邏輯一致。

4. 總結

當前的ijkplayer播放框架中,為了做到AndroidiOS跨平臺的設計,在Native層直接調用Java MediaCodec的接口。如果將API級別提高,在Native層調用NDK MediaCodec接口并輸出YUV數據,可以拿到解碼后的YUV數據,也能保證軟硬解渲染通路的一致性。
當前測試數據不充分,兩種方式哪種性能、系統占用更優,還需要做更多的評估工作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,702評論 25 708
  • MediaCodec的官方文檔 一、Android MediaCodec簡單介紹 Android中可以使用Medi...
    黃海佳閱讀 6,196評論 1 16
  • 元認知能力:對自己思考過程的認知與理解。 怎么通過刻意練習提高元認知能力呢: 1、坐享(冥想)2、興趣 3、反思 ...
    楊榮鵬閱讀 727評論 2 1
  • 不該把改變自己的命運放在婚姻上。會遺憾終身。寧可就這樣一直迷迷糊糊的過。
    誰管你深情似海閱讀 210評論 0 0