ijkplayer系列(五) —— ijkplayer視頻解碼與播放 寫在前面 視頻解碼 顯示線程 msg_loop線程

寫在前面

前幾篇文章從播放器初始化一直分析到了音頻播放,其實從中基本能發現作者寫ijkplayer的思路的。所以分析到後面會越來越容易。

那麼今天,我們就來看看Ijkplayer的視頻解碼和播放部分。

視頻解碼

在數據讀取的線程裏,我們分析到了視頻解碼線程的創建是:

SDL_CreateThreadEx(&is->_video_tid, video_thread, ffp, "ff_video_dec");
    

然後在創建之後,有一個死循環:

for (;;) {
        if (is->abort_request)
            break;
        //ignore audio part
        ret = av_read_frame(ic, pkt);
        packet_queue_put(&is->videoq, pkt);   //將獲取到的視頻包推入videoq隊列中
    }

這裏循環把解析到的frame放入is->videoq中,那麼我們在後面解碼的時候,肯定是從這裏面讀取幀,然後再解碼。

現在我們來看看解碼線程:

ffp->node_vdec->func_run_sync(node);

哦?又是直接運行的ffplayer結構體裏面的函數,這裏肯定和音頻播放一樣,在之前初始化的時候給這個函數指針賦值過,那麼我們回過頭看看到底在哪裏爲它賦值的:

我們還是看到之前初始化的ijkmp_android_create()調用ffpipeline_create_from_android(),然後裏面有一句:

pipeline->func_open_video_decoder = func_open_video_decoder;

然後我們再返回看到在stream_component_open()/ff_ffplayer.c裏面有一句:

decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);

點進去ffpipeline_open_video_decoder(ffp->pipeline, ffp);

IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    return pipeline->func_open_video_decoder(pipeline, ffp);
}

看到沒有?結合上面的分析,在stream_component_open()/ff_ffplayer.c裏面其實運行了func_open_video_decoder(),然後把返回值賦值給ffp->node_vdec;繼續跟着流程到func_open_video_decoder()。由於這裏跳轉比較麻煩,我就直接上:

    func_open_video_decoder()
            |(調用)
    //(這裏其實有個判斷,是用硬解碼還是軟解,我們只分析硬解碼)
    node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    return node;
            |(執行)
    //alloc一個IJKFF_Pipenode 
    IJKFF_Pipenode *node =  ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
    node->func_destroy  = func_destroy;
    node->func_run_sync = func_run_sync;
    node->func_flush    = func_flush;
    reutnr node;

最終ffp->node_vdec =node

現在終於找到賦值的地方了,前面調用的ffp->node_vdec->func_run_sync(node);現在看來就是調用的func_run_sync()函數:


static int func_run_sync(IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque *opaque   = node->opaque;
    FFPlayer              *ffp      = opaque->ffp;
    VideoState            *is       = ffp->is;
    Decoder               *d        = &is->viddec;
    PacketQueue           *q        = d->queue;

//...
    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);
     //...
    }
fail:
//...
}

由於篇幅限制,省略了大部分代碼,只保留了一些很重要的操作。

從上面可以看到,在視頻解碼流程裏面又創建了一個線程:

opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");

從命名來看,是一個input_thread,我們先來看看這個線程:


static int enqueue_thread_func(void *arg)
{
    IJKFF_Pipenode        *node     = arg;
    IJKFF_Pipenode_Opaque *opaque   = node->opaque;
    FFPlayer              *ffp      = opaque->ffp;
    VideoState            *is       = ffp->is;
    Decoder               *d        = &is->viddec;
    PacketQueue           *q        = d->queue;
//...
    while (!q->abort_request) {
        ret = feed_input_buffer(env, node, AMC_INPUT_TIMEOUT_US, &dequeue_count);
        if (ret != 0) {
            goto fail;
        }
    }
//...
}

點進去feed_input_buffer()/ffpipenode_android_mediacodec_vdec.c:

staticint feed_input_buffer(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int*enqueue_count)
{
    if(ffp_packet_queue_get_or_buffering(ffp, d->queue, &pkt,&d->pkt_serial, &d->finished) < 0) { //取得數據包
        //...
        input_buffer_ptr= SDL_AMediaCodec_getInputBuffer(opaque->acodec, input_buffer_index,&input_buffer_size); //得到硬解碼應該輸入的buffer的地址
        //...
        memcpy(input_buffer_ptr,d->pkt_temp.data, copy_size);
        //...
        amc_ret= SDL_AMediaCodec_queueInputBuffer(opaque->acodec, input_buffer_index, 0,copy_size, time_stamp, 0); 
//送到AMediaCodec解碼器
        //...
    }
}

OK,這一步原來就是用硬解碼器解碼。哦? 這個input_thread也就只是把每一幀數據送到解碼器去解碼而已。

再看到之前解碼線程,發現真正的解碼是重新開一個線程解碼,那麼這個線程是幹嘛的呢?我們看到後面的代碼drain_output_buffer()函數:

drain_output_buffer()
          |(調用)
drain_output_buffer_l(env, node, timeUs, dequeue_count, frame, got_frame);

這裏面代碼太多了,省略大部分:

static int drain_output_buffer_l(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs,int *dequeue_count)
{
//...
output_buffer_index= SDL_AMediaCodec_dequeueOutputBuffer(opaque->acodec, &bufferInfo,timeUs); 
//...
}

SDL_AMediaCodec_dequeueOutputBuffer():從硬解碼器中獲得解碼後的數據,

然後在func_run_sync()裏面會繼續執行ffp_queue_picture()把數據插入到顯示隊列ijkplayer->ffplayer->is->pictq中。

軟解碼流程更簡單,大家可以自己去看看。

顯示線程

前面我們分析瞭解碼線程,最終數據會放到is->pictq中,那麼我們繼續來分析顯示流程。

其實顯示的線程非常簡單,就是從is->pictq讀取數據,然後直接推送給硬件設備,完成渲染。

我們還是從入口函數video_refresh_thread()/ff_ffplayer.c開始:

video_refresh_thread(void *arg)
              |(調用)
video_refresh(ffp, &remaining_time);
              |(調用)
 video_display2(ffp);
              |(調用)
video_image_display2(ffp);


static void video_image_display2(FFPlayer *ffp)
{
    VideoState *is = ffp->is;
    Frame *vp;

    vp = frame_queue_peek(&is->pictq);
    if (vp->bmp) {
        SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
        ffp->stat.vfps = SDL_SpeedSamplerAdd(&ffp->vfps_sampler, FFP_SHOW_VFPS_FFPLAY, "vfps[ffplay]");
        if (!ffp->first_video_frame_rendered) {
            ffp->first_video_frame_rendered = 1;
            ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);
        }
    }
}
static Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

從隊列中取出數據,然後調用SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);完成渲染。
還記得我們在初始化的時候,在ijkmp_android_create()裏面調用的SDL_VoutAndroid_CreateForAndroidSurface();麼?裏面有一行代碼:

 vout->display_overlay = func_display_overlay;

現在返回剛剛的SDL_VoutDisplayYUVOverlay()函數:

int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    if (vout && overlay && vout->display_overlay)
        return vout->display_overlay(vout, overlay);

    return -1;
}

現在我們知道了,它其實調用了func_display_overlay()完成渲染。

這裏主要是從pictq列表中獲取視頻frame數據,然後再寫入nativewindows的視頻緩衝中進行渲染。

msg_loop線程

其實這個線程做的事很簡單,裏面就一個主要的函數:

inline static void post_event(JNIEnv *env, jobject weak_this, int what, int arg1, int arg2)  
{
    (*env)->CallStaticVoidMethod(env, g_clazz.clazz, g_clazz.jmid_postEventFromNative, weak_this, what, arg1, arg2, NULL );
}

這個函數直接調用java層裏面的函數,然後發送一個時狀態消息。
而在java裏面,有一個handler,這個handler初始化的時候已經說過了,然後向looper中發送消息,然後handler處理消息,然後做出相應反應。比如之前的:

videoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(IMediaPlayer mp) {
                videoView.start();
            }
        });

就是因爲這裏handler處理消息後回調的。

分析到這裏,流程大致走完了,當然還有很多細節沒有分析。由於筆者馬上期末考試了。所以等考完後,工作之餘會分析有關細節的。

** 如果大家還想了解ijkplayer的工作流程的話,可以關注下android下的ijkplayer。**

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