ijkplayer系列(五) —— ijkplayer视频解码与播放

写在前面

前几篇文章从播放器初始化一直分析到了音频播放,其实从中基本能发现作者写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。**

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 写在前面 前几篇文章大概分析到了数据的读取,接下来就该解码和播放了。那么ijkplayer解码和播放又是怎么做的呢...
    尸情化异阅读 18,078评论 8 12
  • 前言我是一名打算走音视频路线的android开发者。以此系列文章开始,记录我的音视频开发学习之路ijkplayer...
    HWilliamgo阅读 5,886评论 5 5
  • 随着互联网技术的飞速发展,移动端播放视频的需求如日中天,由此也催生了一批开源/闭源的播放器,但是无论这个播放器功能...
    金山视频云阅读 46,124评论 28 170
  • 问题 主流程上的区别 缓冲区的设计 内存管理的逻辑 音视频播放方式 音视频同步 seek的问题:缓冲区flush、...
    FindCrt阅读 5,551评论 4 25
  • 笔记可能微乱,但大致清晰,可能会对他人有所帮助,故分享出来。 ×××××××××××××××××××目录×××××...
    王英豪阅读 5,968评论 0 8