從API 21(Android 5.0)開始Android提供C層的NDK MediaCodec的接口。
Java MediaCodec是對NDK MediaCodec的封裝,ijkplayer硬解通路一直使用的是Java MediaCodec接Surface的方式。
本文的主要內容是:在ijkplayer框架內適配NDK MediaCodec,不再使用Surface輸出,改用YUV輸出達到軟硬解通路一致的渲染流程。
下文提到的Java MediaCodec,如果不做特別說明,都指的Surface 輸出。
下文提到的NDK MediaCodec,如果不做特別說明,都指的YUV 輸出。
1. ijkplayer硬解碼的過程
在增加NDK MediaCodec硬解流程之前,先簡要說明Java MediaCodec的流程:
圖中主要有三個步驟:AVPacket->Decode->AVFrame;
- read線程讀到packet,放入packet queue;
- 解碼得到一幀AVFrame,放入picture queue;
- 從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);
...
}
}
- 可以看到解碼線程又創建了子線程,
enqueue_thread_func()
主要是用來將壓縮數據(H.264/H.265)放入解碼器,這樣往解碼器放數據在enqueue_thread_func()
里面,從解碼器取數據在func_run_sync()
里面; -
drain_output_buffer()
從解碼器取出一個AVFrame
,但是這個AVFrame->data
為NULL
并沒有數據,其中AVFrame->opaque
指針指向一個SDL_AMediaCodecBufferProxy
結構體:
struct SDL_AMediaCodecBufferProxy
{
int buffer_id;
int buffer_index;
int acodec_serial;
SDL_AMediaCodecBufferInfo buffer_info;
};
這些成員由硬解器SDL_AMediaCodecFake_dequeueOutputBuffer
得來,它們在視頻渲染的時候會用到;
- 將AVFrame放入待渲染隊列。
2. 增加NDK MediaCodec解碼
根據上面的解碼流程,增加NDK MediaCodec就只需2個關鍵步驟:
- 創建IJKFF_Pipenode;
- 創建相應的解碼線程。
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;
}
- 從解碼器拿到解碼后的數據buffer;
- 填充
AVFrame
結構體,申請相應大小的內存,由于我們設置解碼器的輸出格式是YUV420P,所以frame->format = AV_PIX_FMT_YUV420P
,然后將buffer拷貝到frame->data
; - 放入待渲染隊列
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 MediaCodec和NDK MediaCodec解碼時CPU及GPU的表現:
Java MediaCodec CPU 占用大約在5%左右
NDK MediaCodec CPU占用大約在12%左右
Java MediaCodec GPU占用表現
NDK MediaCodec GPU占用表現
3.1 測試數據分析
NDK MediaCodec的CPU占比大約高出7%,但是GPU表現較好。
CPU為什么會比Java MediaCodec解碼時高呢?
我們這里一直評估的Java MediaCodec,都指的Surface輸出。這意味著接口內部完成了解碼和渲染工作,高度封裝的解碼和渲染,內部做了一些數據傳遞優化的工作。同時ijkplayer進程的CPU占用并不能體現MediaCodec本身的耗用。
3.2 后續優化
有一個原因是不可忽略的:在從解碼器拿到buffer時,會先申請內存,然后拷貝得到AVFrame
。但這一步也可以優化,直接將buffer指向AVFrame->data
,然后在OpenGL渲染完成之后,調用AMediaCodec_releaseOutputBuffer
將buffer還給解碼器,這樣就需要修改渲染的代碼,不能做到軟硬解邏輯一致。
4. 總結
當前的ijkplayer播放框架中,為了做到Android和iOS跨平臺的設計,在Native層直接調用Java MediaCodec的接口。如果將API級別提高,在Native層調用NDK MediaCodec接口并輸出YUV數據,可以拿到解碼后的YUV數據,也能保證軟硬解渲染通路的一致性。
當前測試數據不充分,兩種方式哪種性能、系統占用更優,還需要做更多的評估工作。