Android NDK MediaCodec在ijkplayer中的實踐

https://www.jianshu.com/p/41d3147a5e07

從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; 

里面有兩個比較重要的成員AMediaFormatAMediaCodec,他們就是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數據,也能保證軟硬解渲染通路的一致性。
當前測試數據不充分,兩種方式哪種性能、系統占用更優,還需要做更多的評估工作。



作者:金山視頻云
鏈接:https://www.jianshu.com/p/41d3147a5e07
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。

?

轉載于:https://www.cnblogs.com/jukan/p/9845673.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450917.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450917.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450917.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode-49-字母異位詞分組(神奇的哈希)

題目描述&#xff1a; 給定一個字符串數組&#xff0c;將字母異位詞組合在一起。字母異位詞指字母相同&#xff0c;但排列不同的字符串。 示例: 輸入: ["eat", "tea", "tan", "ate", "nat", "bat"], 輸出: [[&quo…

【精心總結】java內存模型和多線程必會知識

內存模型 &#xff08;1&#xff09;java內存模型到底是個啥子東西&#xff1f; java內存模型是java虛擬機規范定義的一種特定模型&#xff0c;用以屏蔽不同硬件和操作系統的內存訪問差異&#xff0c;讓java在不同平臺中能達到一致的內存訪問效果&#xff0c;是在特定的協議下…

工作流 activity 視頻教程 + redis 視頻教程 百度網盤分享地址

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 云盤下載都沒有密碼&#xff0c;直接下載&#xff0c;解壓有密碼&#xff1a;chongxiangmengxiangjiaoyu&#xff0c; 解壓完成后就可以…

快速解決 GRADLE 項目下載 gradle-*-all.zip 慢的問題

1、首先根據項目中 gradle\wrapper\gradle-wrapper.properties 文件的 distributionUrl 屬性的值 #Tue Feb 06 12:27:20 CET 2018 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists distributionUrlht…

[Python] 程序結構與控制流

1. 條件語句 if、else與elif語句用于控制條件代碼的執行。條件語句的一般格式如下&#xff1a; if expression:statements elif expression:statements elif expression:statements ... else:statements 如果不需要執行任何操作&#xff0c;可以省略條件語句的else和elif子句。…

webrtc 源碼結構

apiWebRTC 接口層。包括 DataChannel, MediaStream, SDP相關的接口。各瀏覽器都是通過該接口層調用的 WebRTC。call存放的是 WebRTC “呼叫&#xff08;Call&#xff09;” 相關邏輯層的代碼。audio存放音頻網絡邏輯層相關的代碼。音頻數據邏輯上的發送&#xff0c;接收等代碼。…

mysql查詢流程解析及重要知識總結

時光荏苒啊&#xff01;在過兩個月我就工作滿三年了&#xff0c;大學畢業的情景還歷歷在目&#xff0c;而我已經默默的向油膩中年大叔進發了。作為一名苦逼的后端工程師&#xff0c;我搞過一段時間python&#xff0c;現在靠java糊口&#xff0c;但后來才發現&#xff0c;始終不…

界面無小事(八):RecyclerView增刪item

界面無小事(一): RecyclerViewCardView了解一下 界面無小事(二): 讓RecyclerView展示更多不同視圖 界面無小事(三):用RecyclerView Toolbar做個文件選擇器 界面無小事(四):來寫個滾動選擇器吧! 界面無小事(五):自定義TextView 界面無小事(六):來做個好看得側拉菜單! 界面無小事…

Failed to install Tomcat7 service 解決

見&#xff1a; http://blog.csdn.net/desow/article/details/21446197 tomcat 安裝時出現 Failed to install Tomcat7 service 今天在安裝tomcat時提示 Failed to install Tomcat7 service了&#xff0c;花了大半天的時間找到了原因&#xff0c;下面分享給大家&#xff0c;希望…

保守官僚 諾基亞就這樣迷失在智能機時代?

7月19日&#xff0c;諾基亞發布了二季度財報&#xff0c;凈虧損達到了17億美元&#xff0c;其中智能手機份額和銷售量進一步下滑&#xff0c;這個智能手機的領導者&#xff0c;正在因智能手機而急速墜落。諾記亞領先業界近十年就把握住了智能手機的趨勢&#xff0c;并推出了首款…

django集成ansibe實現自動化

動態生成主機列表和相關參數 def create_admin_domain(admin_node):workpath BASE_DIR /tools/ansible/scripthosts_file BASE_DIR /tools/ansible/host/ createhostfile()yml_file BASE_DIR /tools/ansible/yml/ create_admin_domain.ymldomain_path admin_node.doma…

extend 對象繼承

function extend(o, n, override) {for (var p in n) {if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))o[p] n[p];} }// 默認參數 var options {pageIndex: 1,pageTotal: 2 };// 新設置參數 var userOptions {pageIndex: 3,pageSize: 10 }extend(o…

【spring容器啟動】之bean的實例化和初始化(文末附:spring循環依賴原理)

本次我們通過源碼介紹ApplicationContext容器初始化流程&#xff0c;主要介紹容器內bean的實例化和初始化過程。ApplicationContext是Spring推出的先進Ioc容器&#xff0c;它繼承了舊版本Ioc容器BeanFactory&#xff0c;并進一步擴展了容器的功能&#xff0c;增加了bean的自動識…

如何將自己的Java項目部署到外網

見&#xff1a;http://jingyan.baidu.com/article/90bc8fc864699af653640cf7.html 做b/s模式的web開發不同于c/s模式的客戶端開發&#xff0c;c/s模式我們只要做好生成可執行文件發送給其他人&#xff0c;其他人就可以用了。但是c/s模式不同&#xff0c;在同一局域網下&#xf…

[Swift]LeetCode916.單詞子集 | Word Subsets

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★?微信公眾號&#xff1a;山青詠芝&#xff08;shanqingyongzhi&#xff09;?博客園地址&#xff1a;山青詠芝&#xff08;https://www.cnblogs.com/strengthen/&#xff09;?GitHub地址&a…

揭秘騰訊研究院輸出策略:產品和人才的孵化器

直到現在&#xff0c;騰訊研究院創始人鄭全戰仍堅持面試招入研究院的每一個人&#xff0c;并做詳細記錄。天賦上的靈性、性格中的包容是他看重的&#xff0c;當然首先人要踏實。大約6年前&#xff0c;鄭全戰加入騰訊&#xff0c;負責籌建中國互聯網公司中的第一個研究院&#x…

java后端必會【基礎知識點】

&#xff08;一&#xff09;java集合類&#xff08;done&#xff09; 在java集合類中最常用的是Collection和Map的接口實現類。Collection又分為List和Set兩類接口&#xff0c;List的實現類有ArrayList、LinkedList、Vector、Stack&#xff0c;Set接口的實現類有HashSet、Tree…

無法連接虛擬設備ide1:0,主機上沒有相對應的設備... 解決

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 運行虛擬機出現報錯&#xff1a; 無法連接虛擬設備ide1:0&#xff0c;主機上沒有相對應的設備&#xff0c;您 要在每次開啟此虛擬機時都…

繳滿15年能領多少錢 養老金計算公式網上瘋傳

社保人員稱我省計算方式與各設區市平均工資掛鉤&#xff0c;與網上不同 最近&#xff0c;關于“延遲退休”引起各方高度關注&#xff0c;成為廣大居民十分關心的話題。是否延遲退休尚無定論&#xff0c;但在網上有不少關于養老金的計算。那網上流傳的計算方法是否科學&#xff…

48_并發編程-線程-資源共享/鎖

一、數據共享多個線程內部有自己的數據棧&#xff0c;數據不共享&#xff1b;全局變量在多個線程之間是共享的。1 # 線程數據共享不安全加鎖2 3 import time4 from threading import Thread, Lock5 6 7 num 1008 9 def func(t_lock): 10 global num 11 t_lock.acquire…