ffmpeg ffplay.c 源码分析二:数据读取线程

本章主要是分析 数据读取线程read_thread 中的工作。如上图红色框框的部分

从ffplay框架分析我们可以看到,ffplay有专⻔的线程read_thread()读取数据,
且在调⽤av_read_frame 读取数据包之前需要做:
1.例如打开⽂件,
2.查找配置解码器,
3.初始化⾳视频输出等准备阶段,
主要包括三⼤步骤:

  • 准备⼯作
  • For循环读取数据
  • 退出线程处理

一 准备⼯作

准备⼯作主要包括以下步骤:

  1. avformat_alloc_context 创建上下⽂
  2. ic -> interrupt_callback . callback = decode_interrupt_cb;
  3. avformat_open_input打开媒体⽂件
  4. avformat_find_stream_info 读取媒体⽂件的包获取更多的stream信息
  5. 检测是否指定播放起始时间,如果指定时间则seek到指定位置 avformat_seek_file
  6. 查找 查找AVStream,讲对应的index值记录到 st_index [ AVMEDIA_TYPE_NB ];
    a. 根据⽤户指定来查找流 avformat_match_stream_specifier
    b. 使⽤av_find_best_stream查找流
  7. 通过 AVCodecParameters和 av_guess_sample_aspect_ratio 计算出显示窗⼝的宽、⾼
  8. stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参数的初始化。

1. avformat_alloc_context 创建上下⽂

调⽤ avformat_alloc_context创建解复⽤器上下⽂

// 1. 创建上下⽂结构体,这个结构体是最上层的结构体,表示输⼊上下⽂
ic = avformat_alloc_context();

最终该ic 赋值给VideoState的ic变量

is->ic = ic; // videoState的ic指向分配的ic

2 ic->interrupt_callback

ic 这里是AVFormatContext ,

    /* 2.设置中断回调函数,如果出错或者退出,就根据目前程序设置的状态选择继续check或者直接退出 */
    /* 当open出现阻塞的时候时候,会调用interrupt_callback.callback
     * 回调函数中返回1则代表ffmpeg结束阻塞可以将操纵权交给用户线程并返回错误码
     * 回调函数中返回0则代表ffmpeg继续阻塞直到ffmpeg正常工作为止,所以要退出死等则需要返回1
     */
    ic->interrupt_callback.callback = decode_interrupt_cb;
    ic->interrupt_callback.opaque = is;

那么这个 interrupt_callback 是个啥?
interrupt_callback⽤于ffmpeg内部在执⾏耗时操作时检查调⽤者是否有退出请求,避免⽤户退出请求没 有及时响应。
在什么时候触发这个 callback 呢?这个知道什么时候触发就可以了
avformat_open_input的触发
avformat_find_stream_info的触发
av_read_frame的触发
在QT中debug 是看不到什么时候触发的,测试方法如下:
怎么去测试在哪⾥触发? 在ubuntu使⽤gdb进⾏调试:我们之前讲的在ubuntu下编译ffmpeg,在 lqf@ubuntu:~/ffmpeg_sources/ffmpeg-4.2.1⽬录下有ffplay_g,我们可以通过 gdb ./ffplay_g来播 放视频,然后在decode_interrupt_cb打断点。
举例:avformat_open_input的触发

1 #0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:271
5
2 #1 0x00000000007d99b7 in ff_check_interrupt (cb=0x7fffd00014b0)
3 at libavformat/avio.c:667
4 #2 retry_transfer_wrapper (transfer_func=0x7dd950 <file_read>, size
_min=1,
5 size=32768, buf=0x7fffd0001700 "", h=0x7fffd0001480)
6 at libavformat/avio.c:374
7 #3 ffurl_read (h=0x7fffd0001480, buf=0x7fffd0001700 "", size=32768)
8 at libavformat/avio.c:411
9 #4 0x000000000068cd9c in read_packet_wrapper (size=<optimized out>,
10 buf=<optimized out>, s=0x7fffd00011c0) at libavformat/aviobuf.c:
535
11 #5 fill_buffer (s=0x7fffd00011c0) at libavformat/aviobuf.c:584
12 #6 avio_read (s=s@entry=0x7fffd00011c0, buf=0x7fffd0009710 "",
13 size=size@entry=2048) at libavformat/aviobuf.c:677
14 #7 0x00000000006b7780 in av_probe_input_buffer2 (pb=0x7fffd00011c0,
15 fmt=0x7fffd0000948,
16 filename=filename@entry=0x31d50e0 "source.200kbps.768x320.flv",
17 logctx=logctx@entry=0x7fffd0000940, offset=offset@entry=0,
18 max_probe_size=1048576) at libavformat/format.c:262
19 #8 0x00000000007b631d in init_input (options=0x7fffdd9bcb50,
20 filename=0x31d50e0 "source.200kbps.768x320.flv", s=0x7fffd000094
0)
21 at libavformat/utils.c:443
22 #9 avformat_open_input (ps=ps@entry=0x7fffdd9bcbf8,
23 filename=0x31d50e0 "source.200kbps.768x320.flv", fmt=<optimized
out>,

可以看到是在libavformat/avio.c:374⾏有触发到

3.avformat_open_input()打开媒体⽂件

复制代码
Open an input stream and read the header,打开文件后读取头部。这里有个问题,有些格式其实是没有头部的,因此只调用avformat_open_input是不行的,还需要调用avformat_find_stream_info,avformat_find_stream_info方法是会读取数据的一部分,因此通常情况下 ,这两函数会一起使用。

avformat_open_input⽤于打开输⼊⽂件(对于RTMP/RTSP/HTTP⽹络流也是⼀样,在ffmpeg内部都 抽象为URLProtocol,这⾥描述为⽂件是为了⽅便与后续提到的AVStream的流作区分),读取视频⽂件 的基本信息。

需要提到的两个参数是fmt和options。通过fmt可以强制指定视频⽂件的封装,options可以传递额外参数 给封装(AVInputFormat)。
主要代码:

    //特定选项处理
    if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
        av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
        scan_all_pmts_set = 1;
    }
     /* 3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等 */
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    if (err < 0) {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }
    if (scan_all_pmts_set)
        av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);

    if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret = AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }

scan_all_pmts是mpegts的⼀个选项,表示扫描全部的ts流的"Program Map Table"表。这⾥在没有设定 该选项的时候,强制设为1。最后执⾏avformat_open_input。
参数的设置最终都是设置到对应的解复⽤器,⽐如:

scan_all_pmts是mpegts的⼀个选项,表示扫描全部的ts流的"Program Map Table"表。这⾥在没有设定 该选项的时候,强制设为1。最后执⾏avformat_open_input。

使⽤gdb跟踪options的设置,在av_opt_set打断点,,这个有啥用?

(gdb) b av_opt_set
(gdb) r
#0 av_opt_set_dict2 (obj=obj@entry=0x7fffd0000940,
options=options@entry=0x7fffdd9bcb50, search_flags=search_flags@entry=0)
at libavutil/opt.c:1588
#1 0x00000000011c6837 in av_opt_set_dict (obj=obj@entry=0x7fffd0000940,
options=options@entry=0x7fffdd9bcb50) at libavutil/opt.c:1605
#2 0x00000000007b5f8b in avformat_open_input (ps=ps@entry=0x7fffdd9bcbf8,
filename=0x31d23d0 "source.200kbps.768x320.flv", fmt=<optimized out>,
options=0x2e2d450 <format_opts>) at libavformat/utils.c:560
#3 0x00000000004a70ae in read_thread (arg=0x7ffff7e36040)
at fftools/ffplay.c:2780
......
(gdb) l
1583
1584 if (!options)
1585 return 0;
1586
1587 while ((t = av_dict_get(*options, "", t, AV_DICT_IGNORE_SUFFIX))) {
1588 ret = av_opt_set(obj, t->key, t->value, search_flags);
1589 if (ret == AVERROR_OPTION_NOT_FOUND)
1590 ret = av_dict_set(&tmp, t->key, t->value, 0);
1591 if (ret < 0) {
1592 av_log(obj, AV_LOG_ERROR, "Error setting option %s to value %s.\n", t->key, t->value);
(gdb) print **options
$3 = {count = 1, elems = 0x7fffd0001200}
(gdb) print (*options)->elems
$4 = (AVDictionaryEntry *) 0x7fffd0001200
(gdb) print *((*options)->elems)
$5 = {key = 0x7fffd0001130 "scan_all_pmts", value = 0x7fffd0001150 "1"}
(gdb)

参数的设置最终都是设置到对应的解复⽤器,⽐如:

4. avformat_find_stream_info()

在打开了⽂件后,就可以从AVFormatContext中读取流信息了。⼀般调用avformat_find_stream_info获 取完整的流信息。为什么在调⽤了avformat_open_input后,仍然需要调⽤avformat_find_stream_info 才能获取正确的流信息呢?看下avformat_find_stream_info方法的注释就知道了。

* Read packets of a media file to get stream information. This

* is useful for file formats with no headers such as MPEG. This

* function also computes the real framerate in case of MPEG-2 repeat

* frame mode.

明白的说:
该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说 MPEG(⾥应该说ts更准确)(FLV⽂件也是需要读取packet 分析流信息)。⽽被读取⽤以分析流信息的数 据可能被缓存,供av_read_frame时使⽤,在播放时并不会跳过这部分packet的读取。

5 检测是否指定播放起始时间

如果指定时间则seek到指定位置avformat_seek_file。
可以通过 ffplay -ss 设置起始时间,时间格式hh:mm:ss,
⽐如 ffplay -ss 00:00:30 test.flv 则是从30秒的起始位置开始播放。
具体调⽤流程,可以在 opt_seek 函数打断点进⾏测试

1 { "ss", HAS_ARG, { .func_arg = opt_seek }, "seek to a given position
in seconds", "pos" },
2 { "t", HAS_ARG, { .func_arg = opt_duration }, "play \"duration\" sec
onds of audio/video", "duration" }

    /* if seeking requested, we execute it */
    /* 如果需要指定起始位置 */
    if (start_time != AV_NOPTS_VALUE) {
        int64_t timestamp;

        timestamp = start_time;
        /* add the stream start time */
        if (ic->start_time != AV_NOPTS_VALUE)
            timestamp += ic->start_time;
        ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
        if (ret < 0) {
            av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",
                    is->filename, (double)timestamp / AV_TIME_BASE);
        }
    }

6 查找查找AVStream

⼀个媒体⽂件,对应有0~n个⾳频流、0~n个视频流、0~n个字幕流,⽐如这⾥我们⽤了2_audio.mp4是有 2个⾳频流,1个视频流


具体现在那个流进⾏播放我们有两种策略:

  1. 在播放起始指定对应的流
  2. 使⽤缺省的流进⾏播放
    1 在播放起始指定对应的流
    ffplay是通过通过命令可以指定
    { "ast" , OPT_STRING | HAS_ARG | OPT_EXPERT , {
    &wanted_stream_spec[ AVMEDIA_TYPE_AUDIO ] }, "select desired audio stream" ,
    "stream_specifier" },
    { "vst" , OPT_STRING | HAS_ARG | OPT_EXPERT , {
    &wanted_stream_spec[ AVMEDIA_TYPE_VIDEO ] }, "select desired video stream" ,
    "stream_specifier" },
    { "sst" , OPT_STRING | HAS_ARG | OPT_EXPERT , {
    &wanted_stream_spec[ AVMEDIA_TYPE_SUBTITLE ] }, "select desired subtitle stream" ,
    "stream_specifier" },
    可以通过
  • -ast n 指定⾳频流(⽐如我们在看电影时,有些电影可以⽀持普通话和英⽂切换,此时可以⽤该命令 进⾏选择)
  • -vst n 指定视频流
  • -vst n 指定字幕流
    讲对应的index值记录到st_index[AVMEDIA_TYPE_NB];
    2 使⽤缺省的流进⾏播放

如果我们没有指定,则ffplay主要是通过 av_find_best_stream 来选择,其原型为:

//根据用户指定来查找流
    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 && wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
                st_index[type] = i;
    }
    for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
        if (wanted_stream_spec[i] && st_index[i] == -1) {
            av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not match any %s stream\n", wanted_stream_spec[i], av_get_media_type_string(i));
            st_index[i] = INT_MAX;
        }
    }
    //利用av_find_best_stream选择流,
    if (!video_disable)
        st_index[AVMEDIA_TYPE_VIDEO] =
            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
    if (!audio_disable)
        st_index[AVMEDIA_TYPE_AUDIO] =
            av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                                st_index[AVMEDIA_TYPE_AUDIO],
                                st_index[AVMEDIA_TYPE_VIDEO],
                                NULL, 0);
    if (!video_disable && !subtitle_disable)
        st_index[AVMEDIA_TYPE_SUBTITLE] =
            av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                                st_index[AVMEDIA_TYPE_SUBTITLE],
                                (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                                 st_index[AVMEDIA_TYPE_AUDIO] :
                                 st_index[AVMEDIA_TYPE_VIDEO]),
                                NULL, 0);
  • 如果⽤户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作⽤。
  • 如果指定了正确的wanted_stream_nb,⼀般情况都是直接返回该指定流,即⽤户选择的流。
  • 如果指定了相关流,且未指定⽬标流的情况,会在相关流的同⼀个节⽬中查找所需类型的流,但⼀般结 果,都是返回该类型第1个流。

7 通过AVCodecParameters和av_guess_sample_aspect_ratio计算出显 示窗⼝的宽、⾼

具体流程如上所示,这⾥实质只是设置了 default_width、default_height 变量的⼤⼩,没有真正改变窗 ⼝的⼤⼩。真正调整窗⼝⼤⼩是在视频显示调⽤ video_open()函数进⾏设置。

 //7 从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
 if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
     AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
     AVCodecParameters *codecpar = st->codecpar;
     /*根据流和帧宽⾼⽐猜测帧的样本宽⾼⽐。
     * 由于帧宽⾼⽐由解码器设置,但流宽⾼⽐由解复⽤器设置,因此这两者可能不相等。
     * 此函数会尝试返回待显示帧应当使⽤的宽⾼⽐值。
     * 基本逻辑是优先使⽤流宽⾼⽐(前提是值是合理的),其次使⽤帧宽⾼⽐。
     * 这样,流宽⾼⽐(容器设置,易于修改)可以覆盖帧宽⾼⽐。
     */
     AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
     if (codecpar->width) {
         // 设置显示窗⼝的⼤⼩和宽⾼⽐
         set_default_window_size(codecpar->width, codecpar->height, sar);
     }
 }

8 stream_component_open()

经过以上步骤,⽂件打开成功,且获取了流的基本信息,并选择⾳频流、视频流、字幕流。接下来就可以 所选流对应的解码器了。
⾳频、视频、字幕等流都要调⽤ stream_component_open,他们直接有共同的流程,也有差异化的流
程,差异化流程使⽤switch进⾏区分。具体原型
int stream_component_open ( VideoState * is , int stream_index );
stream_index
先是通过 avcodec_alloc_context3 分配了解码器上下⽂ AVCodecContex ,然后通过
avcodec_parameters_to_context 把所选流的解码参数赋给 avctx ,最后设了 time_base .
补充: avcodec_parameters_to_context 解码时⽤, avcodec_parameters_from_context则⽤于编

/* open a given stream. Return 0 if OK */
/**
 * @brief stream_component_open
 * @param is
 * @param stream_index 流索引
 * @return Return 0 if OK
 */
static int stream_component_open(VideoState *is, int stream_index)
{
    AVFormatContext *ic = is->ic;
    AVCodecContext *avctx;
    AVCodec *codec;
    const char *forced_codec_name = NULL;
    AVDictionary *opts = NULL;
    AVDictionaryEntry *t = NULL;
    int sample_rate, nb_channels;
    int64_t channel_layout;
    int ret = 0;
    int stream_lowres = lowres;

    if (stream_index < 0 || stream_index >= ic->nb_streams)
        return -1;
    /*  为解码器分配一个编解码器上下文结构体 */
    avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);
    /* 将码流中的编解码器信息拷贝到新分配的编解码器上下文结构体 */
    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    if (ret < 0)
        goto fail;
    // 设置pkt_timebase
    avctx->pkt_timebase = ic->streams[stream_index]->time_base;

    /* 根据codec_id查找解码器 */
    codec = avcodec_find_decoder(avctx->codec_id);

    switch(avctx->codec_type){
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index;
            forced_codec_name =    audio_codec_name; break;
        case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index;
            forced_codec_name = subtitle_codec_name; break;
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index;
            forced_codec_name =    video_codec_name; break;
    }
    if (forced_codec_name)
        codec = avcodec_find_decoder_by_name(forced_codec_name);
    if (!codec) {
        if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with name '%s'\n", forced_codec_name);
        else                   av_log(NULL, AV_LOG_WARNING,
                                      "No decoder could be found for codec %s\n", avcodec_get_name(avctx->codec_id));
        ret = AVERROR(EINVAL);
        goto fail;
    }

    avctx->codec_id = codec->id;
    if (stream_lowres > codec->max_lowres) {
        av_log(avctx, AV_LOG_WARNING, "The maximum value for lowres supported by the decoder is %d\n",
                codec->max_lowres);
        stream_lowres = codec->max_lowres;
    }
    avctx->lowres = stream_lowres;

    if (fast)
        avctx->flags2 |= AV_CODEC_FLAG2_FAST;

    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 (stream_lowres)
        av_dict_set_int(&opts, "lowres", stream_lowres, 0);
    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)
        av_dict_set(&opts, "refcounted_frames", "1", 0);
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    if ((t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret =  AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }

    is->eof = 0;
    ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
#if CONFIG_AVFILTER
        {
            AVFilterContext *sink;

            is->audio_filter_src.freq           = avctx->sample_rate;
            is->audio_filter_src.channels       = avctx->channels;
            is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);
            is->audio_filter_src.fmt            = avctx->sample_fmt;
            if ((ret = configure_audio_filters(is, afilters, 0)) < 0)
                goto fail;
            sink = is->out_audio_filter;
            sample_rate    = av_buffersink_get_sample_rate(sink);
            nb_channels    = av_buffersink_get_channels(sink);
            channel_layout = av_buffersink_get_channel_layout(sink);
        }
#else
        sample_rate    = avctx->sample_rate;
        nb_channels    = avctx->channels;
        channel_layout = avctx->channel_layout;
#endif

        /* prepare audio output 准备音频输出*/
        if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
        is->audio_hw_buf_size = ret;
        is->audio_src = is->audio_tgt;
        is->audio_buf_size  = 0;
        is->audio_buf_index = 0;

        /* init averaging filter 初始化averaging滤镜, 非audio master时使用 */
        is->audio_diff_avg_coef  = exp(log(0.01) / AUDIO_DIFF_AVG_NB);
        is->audio_diff_avg_count = 0;
        /* 由于我们没有精确的音频数据填充FIFO,故只有在大于该阈值时才进行校正音频同步*/
        is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;

        is->audio_stream = stream_index; 获取audio的stream索引
        is->audio_st = ic->streams[stream_index];// 获取audio的stream指针

        // 初始化ffplay封装的⾳频解码器
        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
        if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }
        // 启动⾳频解码线程
        if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);
        break;
    case AVMEDIA_TYPE_VIDEO:
        is->video_stream = stream_index;// 获取video的stream索引
        is->video_st = ic->streams[stream_index];// 获取video的stream

        // 初始化ffplay封装的视频解码器
        decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
        // 启动视频频解码线程
        if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
            goto out;
        is->queue_attachments_req = 1;// 使能请求mp3、aac等⾳频⽂件的封⾯
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        is->subtitle_stream = stream_index;
        is->subtitle_st = ic->streams[stream_index];

        decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
        if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
            goto out;
        break;
    default:
        break;
    }
    goto out;

fail:
    avcodec_free_context(&avctx);
out:
    av_dict_free(&opts);

    return ret;
}

即根据具体的流类型,作特定的初始化。但不论哪种流,基本步骤都包括了ffplay封装的解码器的初始化和 启动解码器线程:

decoder_init 初始化解码器

  • d->avctx = avctx; 绑定对应的解码器上下⽂
  • d->queue = queue; 绑定对应的packet队列
  • d->empty_queue_cond = empty_queue_cond; 绑定VideoState的continue_read_thread,当
  • 解码线程没有packet可读时唤醒read_thread赶紧读取数据
  • d->start_pts = AV_NOPTS_VALUE; 初始化start_pts
  • d->pkt_serial = -1; 初始化pkt_serial
    decoder_start启动解码器
  • packet_queue_start 启⽤对应的packet 队列
  • SDL_CreateThread 创建对应的解码线程
    以上是准备的⼯作,我们再来看for循环。

二 For循环读取数据

主要包括以下步骤:

  1. 检测是否退出
  2. 检测是否暂停/继续
  3. 检测是否需要seek
  4. 检测video是否为attached_pic
  5. 检测队列是否已经有⾜够数据
  6. 检测码流是否已经播放结束
  • a. 是否循环播放
  • b. 是否⾃动退出
  1. 使⽤av_read_frame读取数据包
  2. 检测数据是否读取完毕
  3. 检测是否在播放范围内
  4. 到这步才将数据插⼊对应的队列

1. 检测是否退出

        ///1.检测是否退出
        if (is->abort_request)
            break;

2. 检测是否暂停/继续

        ///2. 检查是否暂停,这⾥的暂停、继续只是对⽹络流有意义
        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);
        }
复制代码
 av_read_pause(ic);方法和 av_read_play(ic);方法 的内部实现都是会调用真正的 解码器的方法
int av_read_pause(AVFormatContext *s)
{
    if (s->iformat->read_pause)
        return s->iformat->read_pause(s);
    if (s->pb)
        return avio_pause(s->pb, 1);
    return AVERROR(ENOSYS);
}

例如:⽐如rtsp rtsp_read_pause方法 和 rtsp_read_play方法

/* pause the stream */
static int rtsp_read_pause(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader reply1, *reply = &reply1;

    if (rt->state != RTSP_STATE_STREAMING)
        return 0;
    else if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {
        ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, NULL);
        if (reply->status_code != RTSP_STATUS_OK) {
            return ff_rtsp_averror(reply->status_code, -1);
        }
    }
    rt->state = RTSP_STATE_PAUSED;
    return 0;
}

static int rtsp_read_play(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader reply1, *reply = &reply1;
    int i;
    char cmd[MAX_URL_SIZE];

    av_log(s, AV_LOG_DEBUG, "hello state=%d\n", rt->state);
    rt->nb_byes = 0;

    if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP) {
        for (i = 0; i < rt->nb_rtsp_streams; i++) {
            RTSPStream *rtsp_st = rt->rtsp_streams[i];
            /* Try to initialize the connection state in a
             * potential NAT router by sending dummy packets.
             * RTP/RTCP dummy packets are used for RDT, too.
             */
            if (rtsp_st->rtp_handle &&
                !(rt->server_type == RTSP_SERVER_WMS && i > 1))
                ff_rtp_send_punch_packets(rtsp_st->rtp_handle);
        }
    }
    if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {
        if (rt->transport == RTSP_TRANSPORT_RTP) {
            for (i = 0; i < rt->nb_rtsp_streams; i++) {
                RTSPStream *rtsp_st = rt->rtsp_streams[i];
                RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
                if (!rtpctx)
                    continue;
                ff_rtp_reset_packet_queue(rtpctx);
                rtpctx->last_rtcp_ntp_time  = AV_NOPTS_VALUE;
                rtpctx->first_rtcp_ntp_time = AV_NOPTS_VALUE;
                rtpctx->base_timestamp      = 0;
                rtpctx->timestamp           = 0;
                rtpctx->unwrapped_timestamp = 0;
                rtpctx->rtcp_ts_offset      = 0;
            }
        }
        if (rt->state == RTSP_STATE_PAUSED) {
            cmd[0] = 0;
        } else {
            snprintf(cmd, sizeof(cmd),
                     "Range: npt=%"PRId64".%03"PRId64"-\r\n",
                     rt->seek_timestamp / AV_TIME_BASE,
                     rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
        }
        ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
        if (reply->status_code != RTSP_STATUS_OK) {
            return ff_rtsp_averror(reply->status_code, -1);
        }
        if (rt->transport == RTSP_TRANSPORT_RTP &&
            reply->range_start != AV_NOPTS_VALUE) {
            for (i = 0; i < rt->nb_rtsp_streams; i++) {
                RTSPStream *rtsp_st = rt->rtsp_streams[i];
                RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
                AVStream *st = NULL;
                if (!rtpctx || rtsp_st->stream_index < 0)
                    continue;

                st = s->streams[rtsp_st->stream_index];
                rtpctx->range_start_offset =
                    av_rescale_q(reply->range_start, AV_TIME_BASE_Q,
                                 st->time_base);
            }
        }
    }
    rt->state = RTSP_STATE_STREAMING;
    return 0;
}

3. 检测是否需要seek

主要的seek操作通过avformat_seek_file完成(该函数的具体使⽤在播放控制seek时做详解)。根据 avformat_seek_file的返回值,如果seek成功,需要:

  1. 清除PacketQueue的缓存,并放⼊⼀个flush_pkt。放⼊的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据(PacketQueue函数的分析 0 ),该flush_pkt也会触发解码器重新刷新解码 器缓存avcodec_flush_buffers(),以避免解码时使⽤了原来的buffer作为参考⽽出现⻢赛克。

  2. 同步外部时钟。在后续⾳视频同步的课程中再具体分析。
    这⾥还要注意:如果播放器本身是pause的状态,则
    if (is->paused)
    step_to_next_frame(is); // 如果本身是pause状态的则显示⼀帧继续暂停

         if (is->seek_req) { // 是否有seek请求
             int64_t seek_target = is->seek_pos;
             int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
             int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
     // FIXME the +-2 is due to rounding being not done in the correct direction in generation
     //      of the seek_pos/seek_rel variables
             // 修复由于四舍五入,没有再seek_pos/seek_rel变量的正确方向上进行
             ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
             if (ret < 0) {
                 av_log(NULL, AV_LOG_ERROR,
                        "%s: error while seeking\n", is->ic->url);
             } else {
                 /* seek的时候,要把原先的数据情况,并重启解码器,put flush_pkt的目的是告知解码线程需要
                  * reset decoder
                  */
                 if (is->audio_stream >= 0) { // 如果有音频流
                     packet_queue_flush(&is->audioq);    // 清空packet队列数据
                     // 放入flush pkt, 用来开起新的一个播放序列, 解码器读取到flush_pkt也清空解码器
                     packet_queue_put(&is->audioq, &flush_pkt);
                 }
                 if (is->subtitle_stream >= 0) { // 如果有字幕流
                     packet_queue_flush(&is->subtitleq); // 和上同理
                     packet_queue_put(&is->subtitleq, &flush_pkt);
                 }
                 if (is->video_stream >= 0) {    // 如果有视频流
                     packet_queue_flush(&is->videoq);    // 和上同理
                     packet_queue_put(&is->videoq, &flush_pkt);
                 }
                 if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                    set_clock(&is->extclk, NAN, 0);
                 } else {
                    set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                 }
             }
             is->seek_req = 0;
             is->queue_attachments_req = 1;
             is->eof = 0;
             if (is->paused)
                 step_to_next_frame(is);// 如果本身是pause状态的则显示⼀帧继续暂停
         }
    

4. 检测video是否为attached_pic

AV_DISPOSITION_ATTACHED_PIC 是⼀个标志。如果⼀个流中含有这个标志的话,那么就是说这个流 是 *.mp3等 ⽂件中的⼀个 Video Stream 。并且该流只有⼀个 AVPacket ,也就是
attached_pic 。这个 AVPacket 中所存储的内容就是这个 *.mp3等 ⽂件的封⾯图⽚。
因此,也可以很好的解释了⽂章开头提到的为什么 st->disposition &
AV_DISPOSITION_ATTACHED_PIC 这个操作可以决定是否可以继续向缓冲区中添加 AVPacket 。

        if (is->queue_attachments_req) {
            // attached_pic 附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面,所以需要注意的是音频文件不一定只存在音频流本身
            if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                AVPacket copy = { 0 };
                if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
                    goto fail;
                packet_queue_put(&is->videoq, &copy);
                packet_queue_put_nullpacket(&is->videoq, is->video_stream);
            }
            is->queue_attachments_req = 0;
        }

5. 检测队列是否已经有⾜够数据

⾳频、视频、字幕队列都不是⽆限⼤的,如果不加以限制⼀直往队列放⼊packet,那将导致队列占⽤⼤量 的内存空间,影响系统的性能,所以必须对队列的缓存⼤⼩进⾏控制。
PacketQueue默认情况下会有⼤⼩限制,达到这个⼤⼩后,就需要等待10ms,以让消费者------解码线程 能有时间消耗。

        /* 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
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            // 如果没有唤醒则超时10ms退出,比如在seek操作时这里会被唤醒
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }

缓冲区满有两种可能:

  1. audioq,videoq,subtitleq三个PacketQueue的总字节数达到了 MAX_QUEUE_SIZE (15M,为什么 是15M?这⾥只是⼀个经验计算值,⽐如4K视频的码率以50Mbps计算,则15MB可以缓存2.4秒,从 这么计算实际上如果我们真的是播放4K⽚源,15MB是偏⼩的数值,有些⽚源⽐较坑 同⼀个⽂件位置 附近的pts差值超过5秒,此时如果视频要缓存5秒才能做同步,那15MB的缓存⼤⼩就不够了)

  2. ⾳频、视频、字幕流都已有够⽤的包(stream_has_enough_packets), 注意:3者要同时成⽴
    第⼀种好理解,看下第⼆种中的stream_has_enough_packets:

    static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {
    return stream_id < 0 || ///没有该流
    queue->abort_request || /// 请求退出
    (st->disposition & AV_DISPOSITION_ATTACHED_PIC) || /// 是ATTACHED_PIC
    queue->nb_packets > MIN_FRAMES ///packet数量大于25
    && (!queue->duration || /// 满⾜PacketQueue总时⻓为0
    av_q2d(st->time_base) * queue->duration > 1.0);///或总时⻓超过1s
    }

有这么⼏种情况包是够⽤的:

    1. 流没有打开(stream_id < 0),没有相应的流返回逻辑true
    1. 有退出请求(queue->abort_request)
    1. 配置了AV_DISPOSITION_ATTACHED_PIC
    1. packet队列内包个数⼤于MIN_FRAMES(>25),并满⾜PacketQueue总时⻓为0或总时⻓超过1s

思路:

  • 总数据⼤⼩
  • 每个packet队列的情况。

6. 检测码流是否已经播放结

⾮暂停状态才进⼀步检测码流是否已经播放完毕(注意: 数据播放完毕 和码流 数据读取完毕 是两个概 念。)
PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕

        if (!is->paused &&	 // 非暂停
                // 这里的执行是因为码流读取完毕后 插入空包所致
                (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
                (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 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;
            }
        }

这⾥判断播放已完成的条件需要同时满⾜满⾜:

  1. 不在暂停状态
  2. ⾳频未打开;或者打开了,但是解码已解完所有packet,⾃定义的解码器(decoder)serial等于
    PacketQueue的serial,并且FrameQueue中没有数据帧
    PacketQueue.serial -> packet.serail -> decoder.pkt_serial
    decoder.finished = decoder.pkt_serial
    is->auddec.finished == is->audioq.serial 最新的播放序列的packet都解码完毕
    frame_queue_nb_remaining(&is->sampq) == 0 对应解码后的数据也播放完毕
  3. 视频未打开;或者打开了,但是解码已解完所有packet,⾃定义的解码器(decoder)serial等于
    PacketQueue的serial,并且FrameQueue中没有数据帧。
    在确认⽬前码流已播放结束的情况下 ,⽤户有两个变量可以控制播放器⾏为:
  4. loop: 控制播放次数(当前这次也算在内,也就是最⼩就是1次了),0表示⽆限次
  5. autoexit:⾃动退出,也就是播放完成后⾃动退出。
    loop条件简化的⾮常不友好,其意思是:如果loop==1,那么已经播了1次了,⽆需再seek重新播放;如果 loop不是1,==0,随意,⽆限次循环;减1后还⼤于0(--loop),也允许循环

a. 是否循环播放
如果循环播放,即是将⽂件seek到起始位置 stream_seek(is, start_time != AV_NOPTS_VALUE ?
start_time : 0, 0, 0); ,这⾥讲的的起始位置不⼀定是从头开始,具体也要看⽤户是否指定了起始播放位 置
b. 是否⾃动退出
如果播放完毕⾃动退出

7. 使⽤av_read_frame读取数据包

读取数据包很简单,但要注意传⼊的packet,av_read_frame不会释放其数据,⽽是每次都重新申请数据。

        /* 7.读取媒体数据,得到的是音视频分离后、解码前的数据 */
        ret = av_read_frame(ic, pkt); // 调用不会释放pkt的数据,都是自己重新申请

8. 检测数据是否读取完毕

数据读取完毕后,放对应⾳频、视频、字幕队列插⼊"空包",以通知解码器冲刷buffer,将缓存的所有数 据都解出来frame并去出来。
然后继续在for{}循环,直到收到退出命令,或者loop播放,或者seek等操作

        if (ret < 0) {
            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                // 插入空包说明码流数据读取完毕了,之前讲解码的时候说过刷空包是为了从解码器把所有帧都读出来
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                is->eof = 1;        // 文件读取完毕
            }
            if (ic->pb && ic->pb->error)
                break;
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;		// 继续循环,保证线程的运行,比如要seek到某个位置播放可以继续响应
        } else {
            is->eof = 0;
        }

9. 检测是否在播放范围内

播放器可以设置:-ss 起始位置,以及 -t 播放时⻓

        // 9 检测是否在播放范围内
        /* 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;
        // 这里的duration是在命令行时用来指定播放长度
        pkt_in_play_range = 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)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                <= ((double)duration / 1000000);

从流获取的参数:
stream_start_time:是从当前流AVStream->start_time获取到的时间,如果没有定义具体的值则默
认为AV_NOPTS_VALUE,即该值是⽆效的;那stream_start_time有意义的就是0值;
pkt_ts:当前packet的时间戳,pts有效就⽤pts的,pts⽆效就⽤dts的;

ffplay播放的参数
duration: 使⽤"-t value"指定的播放时⻓,默认值 AV_NOPTS_VALUE,即该值⽆效不⽤参考
start_time:使⽤"-ss value"指定播放的起始位置,默认 AV_NOPTS_VALUE,即该值⽆效不⽤参考
pkt_in_play_range的值为0或1

当没有指定duration播放时⻓时,很显然duration == AV_NOPTS_VALUE的逻辑值为1,所以
pkt_in_play_range为1;
当duration被指定(-t value)且有效时,主要判断

(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time :
0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
<= ((double)duration / 1000000);

实质就是当前时间戳 pkt_ts - start_time 是否 < duration,这⾥分为:
stream_start_time是否有效:有效就⽤实际值,⽆效就是从0开始
start_time 是否有效,有效就⽤实际值,⽆效就是从0开始
即是pkt_ts - stream_start_time - start_time < duration (为了简单,这⾥没有考虑时间单位)

10. 到这步才将数据插⼊对应的队列

1 // 10 将⾳视频数据分别送⼊相应的 queue 中
这⾥的代码就很直⽩了,将packet放⼊到对应的PacketQueue

// 10 将⾳视频数据分别送⼊相应的queue中        
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->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
            //printf("pkt pts:%ld, dts:%ld\n", pkt->pts, pkt->dts);
            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);// C++ share_ptr
        }

三 退出线程处理

主要包括以下步骤:

  1. 如果解复⽤器有打开则关闭avformat_close_input
  2. 调⽤SDL_PushEvent发送退出事件 FF_QUIT_EVENT
    a. 发送的 FF_QUIT_EVENT退出播放事件由 event_loop()函数相应,收到 FF_QUIT_EVENT后调⽤
    do_exit()做退出操作。
  3. 消耗互斥量 wait_mutex
相关推荐
hunandede3 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三:将AVFrame转换成AVPacket。视频编码,AVPacket 重要函数,结构体成员学习
c++·ffmpeg·音视频
橘子味的茶二3 小时前
SDL读取PCM音频
ffmpeg·音视频·pcm
EasyCVR20 小时前
EHOME视频平台EasyCVR视频融合平台使用OBS进行RTMP推流,WebRTC播放出现抖动、卡顿如何解决?
人工智能·算法·ffmpeg·音视频·webrtc·监控视频接入
简鹿办公21 小时前
使用 FFmpeg 进行音视频转换的相关命令行参数解释
ffmpeg·简鹿视频格式转换器·ffmpeg视频转换
EasyCVR1 天前
萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?
运维·服务器·网络·人工智能·ffmpeg·音视频
runing_an_min1 天前
ffmpeg 视频滤镜:屏蔽边框杂色- fillborders
ffmpeg·音视频·fillborders
岁月小龙1 天前
如何让ffmpeg运行时从当前目录加载库,而不是从/lib64
ffmpeg·origin·ffprobe·rpath
行者记3 天前
ffmpeg命令——从wireshark包中的rtp包中分离h264
测试工具·ffmpeg·wireshark
EasyCVR3 天前
国标GB28181视频平台EasyCVR私有化视频平台工地防盗视频监控系统方案
运维·科技·ffmpeg·音视频·1024程序员节·监控视频接入
hypoqqq3 天前
使用ffmpeg播放rtsp视频流
ffmpeg