zoukankan      html  css  js  c++  java
  • Ffplay视频播放流程

    主框架流程

    下图是一个使用“gcc+eygpt+graphviz+手工调整”生成的一个ffplay函数基本调用关系图,其中只保留了视频部分,去除了音频处理、字幕处理以及一些细节处理部分。

    ffplay主流程

    注:图中的数字表示了播放中的一次基本调用流程,X?序号表示退出流程。

    从上图中我们可以了解到以下几种信息:

    • 三个线程:主流程用于视频图像显示和刷新、read_thread用于读取数据、video_thread用于解码处理;
    • 视频 数据处理:由read_thread读取原始数据解复用后,按照packet的方式放入到队列中;由video_thread从packet队列中读取 packet解码后,按照picture的方式放入到队列中;由主流程从picture队列中依次取picture进行显示;
    • 启动流程:启动流程如上图中的数字部分
    • 退出流程:退出流程如上图中的X?序号部分

    下面将对三个线程分别加以详细描述。

    read_thread线程

    从read_thread开始说起而不是从main线程,主要原因是考虑按照视频数据转换的方式比较好理解。

    read_thread的创建是在main-->stream_open函数中:

        is->read_tid     = SDL_CreateThread(read_thread, is);

    read_thread线程主要分为三部分:

    • 初始化部分:主要包括SDL_mutex信号量创建、AVFormatContext创建、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。对应ffplay.c文件中的2693-2810行代码;
    • 循环读取数据部分:主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。对应ffplay.c文件中的2812-2946行代码;
    • 反初始化部分:主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。对应ffplay.c文件中的2947-2972行代码;

    初始化部分

    主要包括SDL_mutex信号量创建、创建avformat上下文、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。

    创建wait_mutex互斥量

        SDL_mutex *wait_mutex = SDL_CreateMutex();

    该互斥量主要用于在对(VideoState *)is->continue_read_thread操作时加保护,如2887行和2925行:

    //代码段一

    /* if the queue are full, no need to read more */

    if (infinite_buffer<1 &&

          ……) {

        /* wait 10 ms */

        SDL_LockMutex(wait_mutex);

        SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);  <-- line 2887

        SDL_UnlockMutex(wait_mutex);

        continue;

    }

    //代码段二

    ret = av_read_frame(ic, pkt);

    if (ret < 0) {

        if (ret == AVERROR_EOF || url_feof(ic->pb))

            eof = 1;

        if (ic->pb && ic->pb->error)

            break;

        SDL_LockMutex(wait_mutex);

        SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);  <-- line 2925

        SDL_UnlockMutex(wait_mutex);

        continue;

    }

    而continue_read_thread从其名字上来看,是一个控制read_thread线程是否继续阻塞的信号量,上面两次阻塞的地方分别 是:packet队列已满,需要等待一会(即超时10ms)或者收到信号重新循环;读数据失败,但是并不是IO错误 (ic->pb->error),如读取网络实时数据时取不到数据,此时也需要等待或者收到信号重新循环。

    注:seek操作时(L1216)和音频队列为空(L2327)时,会发送continue_read_thread信号。

    AVFormatContext创建

    (AVFormatContext *)ic = avformat_alloc_context();

             此处创建的avformat上下文,类似于一个句柄,后续所有avformat相关的函数调用第一个参数都是该上下文指针,如 avformat_open_input、avformat_find_stream_info以及一些和av相关的函数接口第一个参数也是该指针,如 av_find_best_stream、av_read_frame等等。

    打开输入文件

    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);

             创建好avformat上下文后,就打开is->filename指定的文件(或流),其中第三个和第四个参数可以传NULL,由ffmpeg自动 侦测待输入流的文件格式,也可以通过is->iformat手动指定,format_opts参数表示设置的特殊属性。

    通过调用avformat_open_input函数,我们可以得到输入流的一个基本信息。我们可以通过调用av_dump_format(ic, 0, is->filename, 0);来输出解析后的码流信息,可以得到如下数据:

    Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0  

      Duration: N/A, bitrate: N/A

      Program 1

        Stream #0:0[0x68]:Video:h264 ([27][0][0][0] / 0x001B), 90k tbn

        Stream #0:1[0x67]:Audio:aac([15][0][0][0] / 0x000F), 0 channels

    即,可以解析出

    ²  封装格式是mpegts,包含两路数据流

    ²  流1的PID是0x68,类型是视频,编码格式是H264

    ²  流2的PID是0x67,类型是音频,编码格式是AAC

    但是只有这些信息可定无法解码,比如视频的宽高比、图像编码格式(YUV or RGB …)、音频采样率、音频声道数量等等,以及Duration、bitrate等信息。这些信息都需要通过其他函数来解析。

    解析码流信息

    err = avformat_find_stream_info(ic, opts);

    因为avformat_open_input函数只能解析出一些基本的码流信息,不足以满足解码的要求,因此我们调用avformat_find_stream_info函数来尽量的解析出所有的和输入流相关的信息。

    解析码流的内部实现我们不在此处讨论,先看一看调用后该函数后解析出来的信息(同样采用av_dump_format来输出):

    Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0  

      Duration: 00:02:53.73, start: 2051.276989, bitrate: 1983 kb/s

      Program 1

        Stream #0:0[0x68]: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuv420p, 1280x720, 30 tbr, 90k tbn, 180k tbc

        Stream #0:1[0x67]: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp,72 kb/s

    对比上一步获取的信息,我们可以看到新解析出来的信息:

    ²  码流信息;节目时长00:02:53.73,开始播放时间2051.276989,码率1983 kb/s

    ²  视频信息:色彩空间YUV420p,分辨率1280x720,帧率30,文件层的时间精度90k,视频层的时间精度180K

    ²  音频信息:采样率48000,立体声stereo,音频采样格式fltp(float, planar),音频比特率72 kb/s

    需要注意的是,该函数是一个阻塞操作,即默认情况下会在该函数中阻塞5s。具体的实现是在avformat_open_input函数中有一个for(;;) 循环,其中的一个break条件如下:

    if (t >= ic->max_analyze_duration) {

        av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %d reached at %"PRId64" microseconds ", ic->max_analyze_duration, t);

        break;

    }

    而ic->max_analyze_duration的默认值定义在options_table.h文件中,即默认的参数表:

    {"analyzeduration", "specify how many microseconds are analyzed to probe the input", OFFSET(max_analyze_duration), AV_OPT_TYPE_INT, {.i64 = 5*AV_TIME_BASE }, 0, INT_MAX, D},

    #define AV_TIME_BASE            1000000            <--file: avutil.h, line: 229

    如果觉得这个默认的5s阻塞时间太长,或者甚至觉得完全没有必要,即我们可以手动的设置各种解码的参数,那么可以通过下面的方法将ic->max_analyze_duration的值修改为1s:

    ic = avformat_alloc_context();

    ic->interrupt_callback.callback = decode_interrupt_cb;

    ic->interrupt_callback.opaque = is;

    //add by Nfer

    ic->max_analyze_duration =1*1000*1000;

    av_log(NULL, AV_LOG_ERROR, "ic->max_analyze_duration %d. ", ic->max_analyze_duration);

    err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);

    注:红色部分为添加的代码

    查找音视频数据流

    if (!video_disable)

        st_index[AVMEDIA_TYPE_VIDEO] =

            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,

                                wanted_stream[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);

    av_find_best_stream函数主要就做了一件事:找符合条件的数据流。其简单实现可以参考ffmpeg-tutorial项目中tutorial01.c的代码:

    // Find the first video stream

    videoStream=-1;

    for(i=0; i<pFormatCtx->nb_streams; i++)

      if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {

        videoStream=i;

        break;

      }

    if(videoStream==-1)

      return -1; // Didn't find a video stream

    注:ffmpeg-tutorial项目是对Stephen Dranger写的7个ffmpeg tutorial做的一个update。

    打开对应的数据流

    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {

        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);

    }

    通过最开始的主框架流程图,我们可以大概的看到stream_component_open函数中最主要的动作就是调用packet_queue_start和创建video_thread线程。当然在这之前还有一些处理,其中包括:

    查找解码器

        avctx = ic->streams[stream_index]->codec;

        codec = avcodec_find_decoder(avctx->codec_id);

    如果启动ffplay时通过vcodec参数指定了解码器名称,那么在通过codec_id查找到解码器后,再使用 forced_codec_name查找解码avcodec_find_decoder_by_name。但是注意,如果通过解码器名称查找后会覆盖之前 通过codec_id查找到解码器,即如果在参数中指定了错误的解码器会导致无法正常播放的。

    设置解码参数

    opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec);

    if (!av_dict_get(opts, "threads", NULL, 0))

        av_dict_set(&opts, "threads", "auto", 0);

    if (avctx->lowres)

        av_dict_set(&opts, "lowres", av_asprintf("%d", avctx->lowres), AV_DICT_DONT_STRDUP_VAL);

    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)

        av_dict_set(&opts, "refcounted_frames", "1", 0);

    打开解码器

        if (avcodec_open2(avctx, codec, &opts) < 0)

        return -1;

    启动packet队列

    packet_queue_start(&is->videoq);

    启动packet队列时,会向队列中先放置一个flush_pkt,其中详细缘由后面再讲。

    创建video_thread线程

    is->video_stream = stream_index;

    is->video_st = ic->streams[stream_index];

    is->video_tid = SDL_CreateThread(video_thread, is);

    is->queue_attachments_req = 1;

    注:上述分析过程中没有考虑音频和字幕处理的部分,后续有机会再详解。

    循环读取数据部分

    该部分是一个for (;;)循环,循环中主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。

    for循环跳出条件

    有两处是break处理的:

    //代码段一

    if (is->abort_request)

        break;                                <-- Line 2814

    //代码段二

    ret = av_read_frame(ic, pkt);

    if (ret < 0) {

        if (ic->pb && ic->pb->error)

            break;                            <-- Line 2923

    }

    其中条件一是调用do_exit --> stream_close中将is->abort_request置为1的,代码中有多个地方是判断该条件进行exit处理的;条件二很清晰,就是当遇到读数据失败并且是IO错误时,会退出。

    pause和resume操作处理

    if (is->paused != is->last_paused) {

        is->last_paused = is->paused;

        if (is->paused)

            is->read_pause_return = av_read_pause(ic);

        else

            av_read_play(ic);

    }

    在ffplay中暂停和恢复的按键操作时p键(SDLK_p)和space键(SDLK_SPACE),会调用 toggle_pause--> stream_toggle_pause来修改is->paused标记变量,然后在read_thread线程中通过对 is->paused标记变量的判断进行pause和resum(play)的处理。

    seek操作处理

    if (is->seek_req) {

        ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);

             if (is->video_stream >= 0) {

                       packet_queue_flush(&is->videoq);

                       packet_queue_put(&is->videoq, &flush_pkt);

             }

             is->seek_req = 0;

    }

    注:上述代码有所删减,只保留了和视频相关的部分

    同上面pause和resume的处理,is->seek_req是在按键操作(SDLK_PAGEUP、SDLK_PAGEDOWN、 SDLK_LEFT、SDLK_RIGHT、SDLK_UP和SDLK_DOWN)时,调用stream_seek函数来修改 is->seek_req标记变量,然后在read_thread线程中根据is->seek_req标记变量来进行处理。

    具体处理除了调用ffmpeg的avformat_seek_file接口外,还向packet队列中放置了一个flush_pkt,这个在video_thread中的处理中会解决seek操作的花屏效果。

    packet队列写入失败处理

    /* if the queue are full, no need to read more */

    if (infinite_buffer<1 &&

          (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE

        || (   (is->audioq   .nb_packets > MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)

            && (is->videoq   .nb_packets > MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request

                || (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))

            && (is->subtitleq.nb_packets > MIN_FRAMES || is->subtitle_stream < 0 || is->subtitleq.abort_request)))) {

        /* wait 10 ms */

        SDL_LockMutex(wait_mutex);

        SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);

        SDL_UnlockMutex(wait_mutex);

        continue;

    }

    此处的各种判断条件不详细解释,重点是在播放器处理中,写数据失败时需要wait and continue的处理。

    读数据结束处理

    if (eof) {

        if (is->video_stream >= 0) {

            av_init_packet(pkt);

            pkt->data = NULL;

            pkt->size = 0;

            pkt->stream_index = is->video_stream;

            packet_queue_put(&is->videoq, pkt);

        }

        SDL_Delay(10);

        if (is->audioq.size + is->videoq.size + is->subtitleq.size == 0) {

            if (loop != 1 && (!loop || --loop)) {

                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);

            } else if (autoexit) {

                ret = AVERROR_EOF;

                goto fail;

            }

        }

        eof=0;

        continue;

    }

    当遇到eof,即end of file时,做一下几个步骤:

    • 向packet队列中放置一个null packet,此处用于loop时使用
    • 判断是否是loop操作,如果是就seek到开始位置重新播放
    • 如果是autoexit模式,就goto fail退出

    注意,在读数据eof时,读数据部分还有些滞后,即if (is->audioq.size + is->videoq.size + is->subtitleq.size== 0)判断不一定为true,引起在判断前先delay了10ms(SDL_Delay(10););但是仍然不一定为true,因此需要 continue。当然下一步av_read_frame失败也会返回AVERROR_EOF,eof会重新赋值为1。即,eof退出会wait到真正的 播放完毕。

    读数据并写入到对应的音视频队列

    ret = av_read_frame(ic, pkt);

    if (pkt->stream_index == is->video_stream && pkt_in_play_range

               && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {

        packet_queue_put(&is->videoq, pkt);

    }

    注:上述代码有所删减,只保留了和视频相关的部分

    此处的处理实际上比较简单,就是av_read_frame和packet_queue_put,不详解。

    反初始化部分

    主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。

    退出前的等待

    /* wait until the end */

    while (!is->abort_request) {

        SDL_Delay(100);

    }

    因为之前for循环跳出条件中说明了只有两种情况下才会break出来,其一就是is->abort_request为true,其二直接就goto到fail了,因此两种情况下该while循环都不会判断为true,直接略过。具体代码原因不明。

    关闭音视频流

    if (is->video_stream >= 0)

        stream_component_close(is, is->video_stream);

    注:上述代码有所删减,只保留了和视频相关的部分

    其中stream_component_close关闭视频流做了以下处理:

    • 终止packet队列:packet_queue_abort(&is->videoq);
    • 发送信号给video_thread,避免继续解码阻塞:SDL_CondSignal(is->pictq_cond);
    • 等待vide_thread线程退出:SDL_WaitThread(is->video_tid, NULL);
    • 清空packet队列:packet_queue_flush(&is->videoq);

    给主线程发送FF_QUIT_EVENT

    if (ret != 0) {

        SDL_Event event;

        event.type = FF_QUIT_EVENT;

        event.user.data1 = is;

        SDL_PushEvent(&event);

    }

    在主线程会接收到FF_QUIT_EVENT消息,从而会调用do_exit函数来做退出处理。

    销毁SDL_mutex信号量

    SDL_DestroyMutex(wait_mutex);

    read_thread基本就分析到这里,下面描述以下video_thread。

    video_thread线程

    从主框架流程中可以看出,video_thread线程是在read_thread--> stream_component_open中创建的,负责从packet队列中读取packet并解码为picture,然后存储到picture队列 中供主线程读取并刷新显示。

    video_thread的创建是在read_thread --> stream_component_open函数中:

    is->video_tid = SDL_CreateThread(video_thread, is);

    read_thread线程同样分为三部分:

    • 初始化部分:主要包括AVFrame创建和AVFilterGraph创建。对应ffplay.c文件中的1881-1895行代码;
    • 循环解码部分:主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。对应ffplay.c文件中的1897-1966行代码;
    • 反初始化部分:主要包括刷新codec中的数据、释放AVFilterGraph、释放AVPacket以及释放AVFrame。对应ffplay.c文件中的1972-1978行代码;

    初始化部分

    该线程的初始化就是创建了AVFrame和AVFilterGraph,其中AVFilterGraph还是和编译宏包含,如果没有打开CONFIG_AVFILTER可以直接省略。

    is->video_tid = SDL_CreateThread(video_thread, is);

    … …

    AVFrame *frame = av_frame_alloc();

    #if CONFIG_AVFILTER

        AVFilterGraph *graph = avfilter_graph_alloc();

    #endif

    循环解码部分

    主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。

    pause和resume操作处理

    video_thread中的关于pause和resume的处理比较简单,就是如果是pause状态就delay(线程sleep):

    while (is->paused && !is->videoq.abort_request)

        SDL_Delay(10);

    读取packet处理

    avcodec_get_frame_defaults(frame);

    av_free_packet(&pkt);

    ret = get_video_frame(is, frame, &pkt, &serial);

    //关于frame的一些处理

    av_frame_unref(frame);

    从上述代码中可以看出,一个frame(和packet)的完整生命流程。

    ffmpeg-tutorial项 目中tutorial01.c中的例子是使用avcodec_alloc_frame()来申请并设置default value的操作,但是在这里就分成了两步:av_frame_alloc()然后 avcodec_get_frame_defaults(frame)。

    av_free_packet实际上清空上一次get_video_frame中获取的packet数据,函数本身是有异常处理的,所以连续调用两次av_free_packet是没有问题的。

    get_video_frame函数中主要部分是packet_queue_get然后avcodec_decode_video2,即从packet队列中读取数据然后进行解码,具体内容有机会另开文章进行讲解。

    AVFILTER处理

    AVFILTER处理是一个比较模块化很高的处理部分,大致流程包括以下几步:

    1. 释放旧的AVFilterGraph并创建一个新的:avfilter_graph_free()和avfilter_graph_alloc()
    2. 配置video filters:configure_video_filters
    3. 向buffersrc中添加frame:av_buffersrc_add_frame
    4. 情况原有的frame和packet:av_frame_unref、avcodec_get_frame_defaults和av_free_packet
    5. 从buffersink中读取处理后的frame:av_buffersink_get_frame_flags

    简单的理解就是:

    AVFILTER使用简要流程

    将picture写入视频队列

    如果需要avfilter处理,那么处理完后或者不需要avfilter处理,解码完成后的frame会调用queue_picture写入到picture队列中。具体细节不详解。

    解码后的清理动作

    使用完packet后,必须从frame中释放出来:av_frame_unref。如api说明:Unreference allthe buffers referenced by frame and reset the frame fields.

    for循环跳出条件

    有以下几种情况下会break出for循环:

    • get_video_frame读数据失败,并且返回<0:该函数失败条件和read_thread其实是一致的,即当q->abort_request为true时;
    • configure_video_filters 配置filter失败:该函数失败的情况下,我遇到的一种就是avfilter_graph_create_filter创建crop filter时失败,原因在于在configureffmpeg时没有把filter配置打开,导致只有默认的几个filter,其他一些特性 filter都没有添加进行;
    • av_buffersrc_add_frame添加frame失败:该函数属于api,不详解;
    • queue_picture保存picture失败:该函数的失败条件是当is->videoq.abort_request为true时;

    即正常情况下,有两种退出模式:

    1. 正常播放完成后退出,此时会通过get_video_frame读数据失败退出
    2. 如果是按ESCAPE和Q键退出,会直接退出,则不会等到,直接在queue_picture函数失败

    反初始化部分

    反初始化部分比较简单,就是先通知avcodec进行flush数据,然后依次释放AVFilterGraph、AVPacket和AVFrame。

    video_thread讲解的比较粗糙,主要原因还是由于个人了解的知识有所欠缺,后续有机会会补上。

    主线程

    主流程用于视频图像显示和刷新,实际上还主线程是一个事件驱动的,就是一个wait_event然后switch处理,然后继续for循环。

    refresh_loop_wait_event处理

    该函数会从event队列中读取出event,SDL_PumpEvents、SDL_PeepEvents。同时会调用video_refresh来进行视频刷新和显示。此处会有大量和SDL API相关的操作,由于个人能力有限暂不分析。

    event的switch处理

    该event的处理分为以下几类:

      • SDL_KEYDOWN键盘按键事件
      • SDL_VIDEOEXPOSE屏幕重画事件
      • SDL_MOUSEBUTTONDOWN鼠标按下事件,如果启动ffplay时有exitonmousedown参数,会相应鼠标按下事件,然后退出播放;
      • SDL_MOUSEMOTION鼠标移动事件,主要seek操作
      • SDL_VIDEORESIZE视频大小变化事件,比如视频中间会出现大小变化,会触发该事件
      • SDL_QUIT、FF_QUIT_EVENT退出事件,如read_thread中出现各种异常会发送该消息
      • FF_ALLOC_EVENT 事件比较特殊,如代码中的注释“ifthe queue is aborted, we have to pop the pending ALLOC event or wait for theallocation to complete”,该消息是video_thread中的发出的消息
  • 相关阅读:
    萌新向Python数据分析及数据挖掘 第三章 机器学习常用算法 第三节 梯度下降法 (上)理解篇
    萌新向Python数据分析及数据挖掘 第三章 机器学习常用算法 第二节 线性回归算法 (下)实操篇
    萌新向Python数据分析及数据挖掘 第三章 机器学习常用算法 第二节 线性回归算法 (上)理解篇
    萌新向Python数据分析及数据挖掘 第三章 机器学习常用算法 第一节 KNN算法 (下)实操篇
    萌新向Python数据分析及数据挖掘 第三章 机器学习常用算法 第一节 KNN算法 (上)理解篇
    萌新向Python数据分析及数据挖掘 第二章 pandas 第五节 Getting Started with pandas
    Oracle数据库安装和授权
    c# 如何获取JSON文件以及如何获取Config文件(framework 和 net .Core)
    C#Core查询数据库存储EXCEL文件
    如何在WINDOW系统下编译P12证书制作
  • 原文地址:https://www.cnblogs.com/shakin/p/4522302.html
Copyright © 2011-2022 走看看