IJKPlayer ffplay流程详解

ffplay流程图.jpg

上图摘自 雷霄骅 雷神的博客 是FFmpeg中 ffplay.c的结构流程图

ijkplayer的ff_ffplay.c是建立在FFmpeg的ffplay.c的基础之上的。

其核心:

read_thread线程负责接收服务器的数据 
video_thread线程负责video decode  
audio_thread负责audio decode
video_refresh_thread线程负责video 的渲染

audio的播放暂时还没有细细的研究 audio_open是关键函数 SDL_OpenAudio():SDL中打开音频设备的函数。注意它是根据SDL_AudioSpec参数打开音频设备。SDL_AudioSpec中的callback字段指定了音频播放的回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。

ffp_prepare_async_l 函数开启read_thread和video_refresh_thread线程。 
read_thread开启audio_thread和video_thread解码线程

ffp_prepare_async_l函数主要调用 ffpipeline_open_audio_output函数和stream_open函数。

ffpipeline_open_audio_output函数调用SDL_AoutIos_CreateForAudioUnit函数

SDL_Aout *SDL_AoutIos_CreateForAudioUnit()
{
    SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque));
    if (!aout)
        return NULL;

    // SDL_Aout_Opaque *opaque = aout->opaque;

    aout->free_l = aout_free_l;
    aout->open_audio  = aout_open_audio;
    aout->pause_audio = aout_pause_audio;
    aout->flush_audio = aout_flush_audio;
    aout->close_audio = aout_close_audio;

    aout->func_set_playback_rate = aout_set_playback_rate;

    return aout;
}
  

SDL_AoutIos_CreateForAudioUnit函数 实现了函数指针的赋值,用于ffplay回调控制audio output的方法。

stream_open函数进行初始化frame队列 packet队列还有音视频同步相关的clock时钟并且同时开启了read_thread线程和video_refresh_thread线程。

video_refresh_thread:这个线程负责 video 的渲染。
read_thread:

这个线程先进行FFmpeg初始化操作,然后获取视频流信息

avformat_open_input():打开媒体。

avformat_find_stream_info():获得媒体信息。这个函数有时候会影响首画的时间,看这个函数的源码发现:这个函数会一直分析视频流的信息,当一直获取不到信息的时候就会一直在这个函数中,一直到它最大的限制,才会出来,会有好几秒的时间。如果网络卡的话,时间会更长。但是我们可以自己给他添加限制,到达我们自己的限制条件的时候就会出来,不会一直在这个函数里面。

ic->probesize这个控制分析视频流的大小,当读的视频流大小达到这个size大小的时候,即使没有获取到信息 也会出来

ic->max_analyze_duration这个事控制视频流信息的大小,同理 当没有获取到信息,读取的视频流的时长达到这个duration的时候 也会出来。

av_dump_format():输出媒体信息到控制台。

stream_component_open():分别打开视频/音频/字幕解码线程。(在下面详细讲解该函数)

av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。

packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。
stream_component_open该函数:

1.如果是video:

ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);在这个函数中选择硬解码还是软解码,在iOS中实现如下

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipenode* node = NULL;
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    if (ffp->videotoolbox) {
        node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
    }
    if (node == NULL) {
        ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \n");
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
        ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
        opaque->is_videotoolbox_open = false;
    } else {
        ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX;
        opaque->is_videotoolbox_open = true;
    }
    ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open);
    return node;
}

在android中实现如下:

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

选择完解码器,然后开始解码

decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")创建一个解码线程
static int video_thread(void *arg)
{
    FFPlayer *ffp = (FFPlayer *)arg;
    int       ret = 0;

    if (ffp->node_vdec) {
        ret = ffpipenode_run_sync(ffp->node_vdec);
    }
    return ret;
}

不管是iOS还是android中:

ffpipenode_create_video_decoder_from_ffplay
ffpipenode_create_video_decoder_from_android_mediacodec
ffpipenode_create_video_decoder_from_ios_videotoolbox
这三个方法都是返回node,并node这个结构体中的函数指针赋值。我们解码的时候 调用的就是node->func_run_sync该方法,使用选择的解码器去解码video。

软解码 该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque *opaque = node->opaque;

    return ffp_video_thread(opaque->ffp);
}

使用videotoolbox 该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)

{

    IJKFF_Pipenode_Opaque *opaque = node->opaque;

    int ret = videotoolbox_video_thread(opaque);

    if (opaque->context) {

        dealloc_videotoolbox(opaque->context);

        free(opaque->context);

        opaque->context = NULL;

    }
    return ret;
} 

使用mediacodec,该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
    JNIEnv                *env      = NULL;
    IJKFF_Pipenode_Opaque *opaque   = node->opaque;
    FFPlayer              *ffp      = opaque->ffp;
    VideoState            *is       = ffp->is;
    Decoder               *d        = &is->viddec;
    PacketQueue           *q        = d->queue;
    int                    ret      = 0;
    int                    dequeue_count = 0;
    AVFrame               *frame    = NULL;
    int                    got_frame = 0;
    AVRational             tb         = is->video_st->time_base;
    AVRational             frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
    double                 duration;
    double                 pts;
    // int64_t                decode_time = 0;
    if (!opaque->acodec) {
        return ffp_video_thread(ffp);
    }

    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s: SetupThreadEnv failed\n", __func__);
        return -1;
    }

    frame = av_frame_alloc();
    if (!frame)
        goto fail;

    if (opaque->frame_rotate_degrees == 90 || opaque->frame_rotate_degrees == 270) {
        opaque->frame_width  = opaque->avctx->height;
        opaque->frame_height = opaque->avctx->width;
    } else {
        opaque->frame_width  = opaque->avctx->width;
        opaque->frame_height = opaque->avctx->height;
    }

    opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");
    if (!opaque->enqueue_thread) {
        ALOGE("%s: SDL_CreateThreadEx failed\n", __func__);
        ret = -1;
        goto fail;
    }

    while (!q->abort_request) {
        /*spb add
        if(ffp_frame_queue_nb_remaining(&is->pictq) == 2)
            ffp_frame_queue_next(&is->pictq);
        if (is->videoq.nb_packets == 0)
        {
           av_log(NULL, AV_LOG_INFO, "spb_log videoq is empty\n");
        }
         end*/
        Frame *vp = ffp_frame_queue_peek_writable(&is->pictq);
        int64_t timeUs = opaque->acodec_first_dequeue_output_request ? 0 : AMC_OUTPUT_TIMEOUT_US;
        got_frame = 0;
        // int64_t start_drain = av_gettime()/1000;
        ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &vp->dest_frame, &got_frame);
        // int64_t end_drain = av_gettime()/1000;
        if (opaque->acodec_first_dequeue_output_request) {
            SDL_LockMutex(opaque->acodec_first_dequeue_output_mutex);
            opaque->acodec_first_dequeue_output_request = false;
            SDL_CondSignal(opaque->acodec_first_dequeue_output_cond);
            SDL_UnlockMutex(opaque->acodec_first_dequeue_output_mutex);
        }
        if (ret != 0) {
            ret = -1;
            if (got_frame && frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
            goto fail;
        }
        if (got_frame) {
            // av_log(NULL, AV_LOG_INFO, "spb_log drain_output_buffer success need time:  %lld,    time : %lld\n", end_drain - start_drain, end_drain - decode_time);
            // decode_time = end_drain;
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
            if (ret) {
                if (frame->opaque)
                    SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
            }
            av_frame_unref(frame);
        }
    }

fail:
    av_frame_free(&frame);
    SDL_AMediaCodecFake_abort(opaque->acodec);
    if (opaque->n_buf_out) {
        free(opaque->amc_buf_out);
        opaque->n_buf_out = 0;
        opaque->amc_buf_out = NULL;
        opaque->off_buf_out = 0;
        opaque->last_queued_pts = AV_NOPTS_VALUE;
    }
    if (opaque->acodec) {
        SDL_VoutAndroid_invalidateAllBuffers(opaque->weak_vout);
        SDL_LockMutex(opaque->acodec_mutex);
        SDL_AMediaCodec_stop(opaque->acodec);
        SDL_UnlockMutex(opaque->acodec_mutex);
    }
    SDL_WaitThread(opaque->enqueue_thread, NULL);
    SDL_AMediaCodec_decreaseReferenceP(&opaque->acodec);
    ALOGI("MediaCodec: %s: exit: %d", __func__, ret);
    return ret;
#if 0
fallback_to_ffplay:
    ALOGW("fallback to ffplay decoder\n");
    return ffp_video_thread(opaque->ffp);
#endif
}
这个方法代码较多,但是我们能看到 其核心就做了两件事
第一件事 opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread"); 创建一个新的线程,在packet queue中不停的取数据 放入硬解码芯片。
第二件事 在下面的while循环中 调用drain_output_buffer这个函数 获取解码后的数据信息。

2.如果是audio:

先执行ret = audio_open(ffp, channel_layout, nb_channels, sample_rate, &is->audio_tgt),audio_open方法。
并没有详细的研究audio的每一步,audio_open也是在根据获取到的流媒体信息进行audio 解码和播放的初始化操作。
其中 wanted_spec.callback = sdl_audio_callback 是有关audio播放的一个函数指针的赋值。

然后

(ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec") 开启线程解码

audio解码使用软解,audio数据量小,算法相对简单没有video复杂,解码对CPU资源占用极小。

ijk销毁函数:iOS调用shutdown,android调用release。
这两个函数本质都是要调用ffplay的 ffp_stop_l和ffp_wait_stop_l函数,
ijk的释放都是在ffp_wait_stop_l函数中,这个函数主要通过调用stream_close函数来释放掉ijk。
stream_close会销毁在ijk初始化时候创建的 队列 流 解码器 线程锁和线程条件变量。

未完待续。

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

推荐阅读更多精彩内容