资讯专栏INFORMATION COLUMN

android ijkplayer c层分析-prepare过程与读取线程

MobService / 2857人阅读

摘要:我们下面先从读取线程入手。无论这个循环前后干了什么,都是要走这一步,读取数据帧。从开始,我理解的是计算出当前数据帧的时间戳后再计算出播放的起始时间到当前时间,然后看这个时间戳是否在此范围内。

ijkplayer现在比较流行,因为工作关系,接触了他,现在做个简单的分析记录吧。我这里直接跳过java层代码,进入c层,因为大多数的工作都是通过jni调用到c层来完成的,java层的内容并不是主体功能。

先来看看线索。直接看ijkplayer_jni.c文件,在ijkmedia下。所有的c函数及java函数映射关系都在这里。java上层调用的_prepareAsync方法为native方法,在这里映射为IjkMediaPlayer_prepareAsync。经过一系列调用后会走到ijkplayer.c的ijkmp_prepare_async_l方法里面,这里看下:

    ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
    
    msg_queue_start(&mp->ffplayer->msg_queue);
    
    // released in msg_loop
    
    ijkmp_inc_ref(mp);
    
    mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop");
    
    // msg_thread is detached inside msg_loop
    
    // TODO: 9 release weak_thiz if pthread_create() failed;
    
    int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
    
    if (retval < 0) {
    
    ijkmp_change_state_l(mp, MP_STATE_ERROR);
    
    return retval;
    
    }

这里显示启动了一个队列,然后启动了一个loop消息线程,然后走了一个关键函数ffp_prepare_async_l。所谓启动队列,其实就是分配空间初始化,然后塞入个私有消息体。后面这个loop线程,真正的执行体在ijkplayer_jni.c的message_loop。这个我们暂时不去管他,理解为一个独立的线程来处理消息派发。重点看下面的ffp_prepare_async_l,这个才是进入到准备播放的阶段:

int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
{
    assert(ffp);
    assert(!ffp->is);
    assert(file_name);

    if (av_stristart(file_name, "rtmp", NULL) ||
        av_stristart(file_name, "rtsp", NULL)) {
        // There is total different meaning for "timeout" option in rtmp
        av_log(ffp, AV_LOG_WARNING, "remove "timeout" option for rtmp.
");
        av_dict_set(&ffp->format_opts, "timeout", NULL, 0);
    }

    /* there is a length limit in avformat */
    if (strlen(file_name) + 1 > 1024) {
        av_log(ffp, AV_LOG_ERROR, "%s too long url
", __func__);
        if (avio_find_protocol_name("ijklongurl:")) {
            av_dict_set(&ffp->format_opts, "ijklongurl-url", file_name, 0);
            file_name = "ijklongurl:";
        }
    }

    av_log(NULL, AV_LOG_INFO, "===== versions =====
");
    ffp_show_version_str(ffp, "ijkplayer",      ijk_version_info());
    ffp_show_version_str(ffp, "FFmpeg",         av_version_info());
    ffp_show_version_int(ffp, "libavutil",      avutil_version());
    ffp_show_version_int(ffp, "libavcodec",     avcodec_version());
    ffp_show_version_int(ffp, "libavformat",    avformat_version());
    ffp_show_version_int(ffp, "libswscale",     swscale_version());
    ffp_show_version_int(ffp, "libswresample",  swresample_version());
    av_log(NULL, AV_LOG_INFO, "===== options =====
");
    ffp_show_dict(ffp, "player-opts", ffp->player_opts);
    ffp_show_dict(ffp, "format-opts", ffp->format_opts);
    ffp_show_dict(ffp, "codec-opts ", ffp->codec_opts);
    ffp_show_dict(ffp, "sws-opts   ", ffp->sws_dict);
    ffp_show_dict(ffp, "swr-opts   ", ffp->swr_opts);
    av_log(NULL, AV_LOG_INFO, "===================
");

    av_opt_set_dict(ffp, &ffp->player_opts);
    if (!ffp->aout) {
        ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
        if (!ffp->aout)
            return -1;
    }

#if CONFIG_AVFILTER
    if (ffp->vfilter0) {
        GROW_ARRAY(ffp->vfilters_list, ffp->nb_vfilters);
        ffp->vfilters_list[ffp->nb_vfilters - 1] = ffp->vfilter0;
    }
#endif

    VideoState *is = stream_open(ffp, file_name, NULL);
    if (!is) {
        av_log(NULL, AV_LOG_WARNING, "ffp_prepare_async_l: stream_open failed OOM");
        return EIJK_OUT_OF_MEMORY;
    }

    ffp->is = is;
    ffp->input_filename = av_strdup(file_name);
    return 0;
}

前面判断直播视频流协议rtmp或rtsp,再后面基本是输出一对信息的,真正的核心函数是stream_open。这个才是根据地址打开视频流,代码如下:

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
    assert(!ffp->is);
    VideoState *is;

    is = av_mallocz(sizeof(VideoState));
    if (!is)
        return NULL;
    is->filename = av_strdup(filename);
    if (!is->filename)
        goto fail;
    is->iformat = iformat;
    is->ytop    = 0;
    is->xleft   = 0;

    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

    if (packet_queue_init(&is->videoq) < 0 ||
        packet_queue_init(&is->audioq) < 0 ||
        packet_queue_init(&is->subtitleq) < 0)
        goto fail;

    if (!(is->continue_read_thread = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s
", SDL_GetError());
        goto fail;
    }

    init_clock(&is->vidclk, &is->videoq.serial);
    init_clock(&is->audclk, &is->audioq.serial);
    init_clock(&is->extclk, &is->extclk.serial);
    is->audio_clock_serial = -1;
    is->audio_volume = SDL_MIX_MAXVOLUME;
    is->muted = 0;
    is->av_sync_type = ffp->av_sync_type;

    is->play_mutex = SDL_CreateMutex();
    ffp->is = is;
    is->pause_req = !ffp->start_on_prepared;

    is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
    if (!is->video_refresh_tid) {
        av_freep(&ffp->is);
        return NULL;
    }

    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s
", SDL_GetError());
fail:
        is->abort_request = true;
        if (is->video_refresh_tid)
            SDL_WaitThread(is->video_refresh_tid, NULL);
        stream_close(ffp);
        return NULL;
    }
    return is;
}

这里可以看到有3个队列初始化,分别是视频、音频和字幕,这些队列通过frame_queue_init又分别分为2个队列,原始的和解码后的。再往下看就是2个线程的创建,分别是video_refresh_thread和read_thread,从字面上理解,应当是输出刷新视频和读取线程。我们下面先从读取线程入手。整个代码很长,注意这里的ic变量。是个AVFormatContext类型,这里放的是流的信息和数据。

......
ic = avformat_alloc_context();
......
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
......
err = avformat_find_stream_info(ic, opts);
......
for (i = 0; i < ic->nb_streams; i++) {
        AVStream *st = ic->streams[i];
        enum AVMediaType type = st->codecpar->codec_type;
        st->discard = AVDISCARD_ALL;
        if (type >= 0 && ffp->wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, ffp->wanted_stream_spec[type]) > 0)
                st_index[type] = i;

        // choose first h264

        if (type == AVMEDIA_TYPE_VIDEO) {
            enum AVCodecID codec_id = st->codecpar->codec_id;
            video_stream_count++;
            if (codec_id == AV_CODEC_ID_H264) {
                h264_stream_count++;
                if (first_h264_stream < 0)
                    first_h264_stream = i;
            }
        }
    }
......
 /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    if (is->show_mode == SHOW_MODE_NONE)
        is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }
......
    /* offset should be seeked*/
    if (ffp->seek_at_start > 0) {
        ffp_seek_to_l(ffp, ffp->seek_at_start);
    }

    for (;;) {
        ......
        ret = av_read_frame(ic, pkt);
        ......
        /* check if packet is in play range specified by user, then queue, otherwise discard */
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        pkt_in_play_range = ffp->duration == AV_NOPTS_VALUE ||
                (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                av_q2d(ic->streams[pkt->stream_index]->time_base) -
                (double)(ffp->start_time != AV_NOPTS_VALUE ? ffp->start_time : 0) / 1000000
                <= ((double)ffp->duration / 1000000);
         ......@@@
         if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
            packet_queue_put(&is->audioq, pkt);
        } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))) {
            packet_queue_put(&is->videoq, pkt);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } else {
            av_packet_unref(pkt);
        }
    }
    ......

avformat_alloc_context是分配空间初始化。然后在avformat_open_input里面填充ic,这个函数里要读取网络数据包的信息,并判定格式等等操作。avformat_find_stream_info这个函数内部看的不是很明白,但是感觉是在找解码器,后面再分析这个吧,先看大流程。这个for循环取出每一帧,然后进行判断类型,音视频字幕归类,并分别设置个数。再来进入到核心的一个函数,就是stream_component_open,这里根据不同类型的帧是否有来进行流的读取及解码工作。这里本来有个疑问,如果在解码之前就处理帧数据,例如优化丢帧处理之类的,是否会加快效率呢?不行,如果不解码就无法分辨出gop里的帧哪个是关键帧i帧,对于后面的处理带来很大不便(特此注明)。真正的读取在av_read_frame,读取解码后的数据,读取到pkt里。这个过程是在一个for无限循环内部进行的,在seek的处理之后进行。这个for不小心会漏看的......那么这个av_read_frame读取到的是什么数据呢?看结构AVPacket,这个的说明已经挺清晰了,是一个压缩的数据帧,是否是关键帧已经在flags里标记了。无论这个循环前后干了什么,都是要走这一步,读取数据帧。下面有一大段是对错误的处理,暂时略过。下面有一段检查数据包是否在用户指定的播放范围内,并根据时间戳排队,否则丢弃的过程。从stream_start_time=...开始,我理解的是计算出当前数据帧的时间戳后再计算出播放的起始时间到当前时间,然后看这个时间戳是否在此范围内。范围内的就put到队列中,否则丢弃。这里插一句,在@@@这里可以加入直播中的丢帧处理,详情网上有资料可查。最后循环前做了缓冲的检查工作。后面就是错误处理了,整个流程就是如此。
这里每个跳过地方其实都挺复杂,后面我捡一些关键的做下分析。本人刚接触此类技术,只做笔记。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76406.html

相关文章

  • android ijkplayer c分析-prepare过程读取线程(续2-读取输入源)

    摘要:下面是,读取头信息头信息。猜测网络部分至少在一开始就应当初始化好的,因此在的过程里面找,在中找到了。就先暂时分析到此吧。 这章要简单分析下ijkplayer是如何从文件或网络读取数据源的。还是read_thread函数中的关键点avformat_open_input函数: int avformat_open_input(AVFormatContext **ps, const char ...

    kevin 评论0 收藏0
  • android ijkplayer c分析-prepare过程读取线程(续1-解码粗略分析)

    摘要:分别为音频视频和字母进行相关处理。向下跟踪两层,会发现,核心函数是。至此解码算完了。整个过程真是粗略分析啊,对自己也很抱歉,暂时先这样吧。 上文中说到在read_thread线程中有个关键函数:avformat_open_input(utils.c),应当是读取视频文件的,这个函数属于ffmpeg层。这回进入到其中去看下: int avformat_open_input(AVForma...

    zhonghanwen 评论0 收藏0
  • android ijkplayer c分析-prepare过程读取线程(续3-解码核心video

    摘要:基本上就是对一个数据帧的描述。我理解的是一个未解码的压缩数据帧。 read_thread这个最关键的读取线程中,逐步跟踪,可以明确stream_component_open---> decoder_start---> video_thread--->ffplay_video_thread。这个调用过程,在解码开始后的异步解码线程中,调用的是ffplay_video_thread。具体可...

    _Suqin 评论0 收藏0
  • android ijkplayer c分析-渲染显示线程

    摘要:在的过程中,不仅会启动,而且会启动。获取队列最后一帧,如果帧中的图像正常,继续走渲染画面。最后通过消息通知开始渲染。这个返回的偏差值就是后面进行是否抛帧或的判断依据。 在prepare的stream_open过程中,不仅会启动read_thread,而且会启动video_refresh_thread。今天就来看看这个video_refresh_thread干了什么。 static in...

    henry14 评论0 收藏0
  • android ijkplayer c分析-初始化(续1 javac衔接)

    摘要:初始化的过程上一篇其实并未完全分析完,这回接着来。层的函数中,最后还有的调用,走的是层的。结构体如下的和,以及,其余是状态及的内容。整个过程是个异步的过程,并不阻塞。至于的东西,都是在层创建并填充的。 初始化的过程上一篇其实并未完全分析完,这回接着来。java层的initPlayer函数中,最后还有native_setup的调用,走的是c层的IjkMediaPlayer_native_...

    Olivia 评论0 收藏0

发表评论

0条评论

MobService

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<