音视频流媒体开发【二十四】ffplay播放器-数据读取线程

音视频流媒体开发-目录

4 数据读取线程

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

  • 准备⼯作
  • 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. 从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
  8. stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参数的初始化。
⼆ For循环读取数据
  1. 检测是否退出
  2. 检测是否暂停/继续
  3. 检测是否需要seek
  4. 检测video是否为attached_pic
  5. 检测队列是否已经有⾜够数据
  6. 检测码流是否已经播放结束
    a. 是否循环播放
    b. 是否⾃动退出
  7. 使⽤av_read_frame读取数据包
  8. 检测数据是否读取完毕
  9. 检测是否在播放范围内
  10. 到这步才将数据插⼊对应的队列
三 退出线程处理
  1. 如果解复⽤器有打开则关闭avformat_close_input
  2. 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
  3. 消耗互斥量wait_mutex

4.1 准备⼯作

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

  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 // 1\. 创建上下⽂结构体,这个结构体是最上层的结构体,表示输⼊上下⽂
2 ic = avformat_alloc_context();

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

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

2 ic->interrupt_callback

1 /* 2.设置中断回调函数,如果出错或者退出,就根据⽬前程序设置的状态选择继续check或者直接退出 */
2 /* 当执⾏耗时操作时(⼀般是在执⾏while或者for循环的数据读取时),会调⽤interrupt_callback.callback
3 * 回调函数中返回1则代表ffmpeg结束耗时操作退出当前函数的调⽤
4 * 回调函数中返回0则代表ffmpeg内部继续执⾏耗时操作,直到完成既定的任务(⽐如读取到既定的数据包)
5 */
6 ic->interrupt_callback.callback = decode_interrupt_cb;
7 ic->interrupt_callback.opaque = is;

interrupt_callback⽤于ffmpeg内部在执⾏耗时操作时检查调⽤者是否有退出请求,避免⽤户退出请求没有及时响应。

怎么去测试在哪⾥触发?

在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
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=0x7fffd0000940)
21 at libavformat/utils.c:443
22 #9 avformat_open_input (ps=ps@entry=0x7fffdd9bcbf8,
23 filename=0x31d50e0 "source.200kbps.768x320.flv", fmt=<optimizedout>,

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

avformat_find_stream_info的触发
1 #0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:2715
2 #1 0x00000000007b25bc in avformat_find_stream_info (ic=0x7fffd0000940,
3 options=0x0) at libavformat/utils.c:3693
4 #2 0x00000000004a6ea9 in read_thread (arg=0x7ffff7e36040)

从该调⽤栈可以看出来 avformat_find_stream_info也会触发ic->interrupt_callback的调⽤,具体可以看代码(libavformat/utils.c:3693⾏)

av_read_frame的触发
1 #0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:271
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=0x7fffd0009710 "FLV\001\005", h=0x7fffd0001480)
6 at libavformat/avio.c:374
7 #3 ffurl_read (h=0x7fffd0001480, buf=0x7fffd0009710 "FLV\001\005",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=0x7fffd00dbf6d "\177",size=45,
13 size@entry=90) at libavformat/aviobuf.c:677
14 #7 0x00000000007a99d5 in append_packet_chunked (s=0x7fffd00011c0,
15 pkt=pkt@entry=0x7fffdd9bca00, size=size@entry=90)
16 at libavformat/utils.c:293
17 #8 0x00000000007aa969 in av_get_packet (s=<optimized out>,
18 pkt=pkt@entry=0x7fffdd9bca00, size=size@entry=90)
19 at libavformat/utils.c:317
20 #9 0x00000000006b350a in flv_read_packet (s=0x7fffd0000940,
21 pkt=0x7fffdd9bca00) at libavformat/flvdec.c:1295
22 #10 0x00000000007aad6d in ff_read_packet (s=s@entry=0x7fffd0000940,
23 pkt=pkt@entry=0x7fffdd9bca00) at libavformat/utils.c:856
24 ---Type <return> to continue, or q <return> to quit---
25 #11 0x00000000007ae291 in read_frame_internal (s=0x7fffd0000940,
26 pkt=0x7fffdd9bcc00) at libavformat/utils.c:1582
27 #12 0x00000000007af422 in av_read_frame (s=0x7fffd0000940,
28 pkt=pkt@entry=0x7fffdd9bcc00) at libavformat/utils.c:1779
29 #13 0x00000000004a68b1 in read_thread (arg=0x7ffff7e36040)
30 at fftools/ffplay.c:3008

这⾥的触发和avformat_open_input⼀致,⼤家可以⾃⾏跟踪调⽤栈。

3 avformat_open_input()打开媒体⽂件

函数原型:

/**
* Open an input stream and read the header. The codecs are not opened.
* The stream must be closed with avformat_close_input().
*
* @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context).
* May be a pointer to NULL, in which case an AVFormatContext is allocated by this
* function and written into ps.
* Note that a user-supplied AVFormatContext will be freed on failure.
* @param url URL of the stream to open.
* @param fmt If non-NULL, this parameter forces a specific input format.
* Otherwise the format is autodetected.
* @param options A dictionary filled with AVFormatContext and demuxer-private options.
* On return this parameter will be destroyed and replaced with a dict containing
* options that were not found. May be NULL.
*
* @return 0 on success, a negative AVERROR on failure.
*
* @note If you want to use custom IO, preallocate the format context and set its pb field.
*/
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat
*fmt, AVDictionary **options);

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

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

主要代码:

1 //特定选项处理
2 if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
3     av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
4     scan_all_pmts_set = 1;
5 }
6 /* 3.打开⽂件,主要是探测协议类型,如果是⽹络⽂件则创建⽹络链接等 */
7 err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
8 if (err < 0) {
9     print_error(is->filename, err);
10    ret = -1;
11    goto fail;
12 }
13 if (scan_all_pmts_set)
14     av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
15
16 if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))){
17     av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
18     ret = AVERROR_OPTION_NOT_FOUND;
19     goto fail;
20 }

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)

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

mpegts.c

image.png

flvdec.c

image.png

4 avformat_find_stream_info()

在打开了⽂件后,就可以从AVFormatContext中读取流信息了。⼀般调⽤avformat_find_stream_info获取完整的流信息。为什么在调⽤了avformat_open_input后,仍然需要调⽤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.
* The logical file position is not changed by this function;
* examined packets may be buffered for later processing.
*
* @param ic media file handle
* @param options If non-NULL, an ic.nb_streams long array of pointers to
* dictionaries, where i-th member contains options for
* codec corresponding to i-th stream.
* On return each dictionary will be filled with options that were not found.
* @return >=0 if OK, AVERROR_xxx on error
*
* @note this function isn't guaranteed to open all the codecs, so
* options being non-empty at return is a perfectly normal behavior.
*
* @todo Let the user decide somehow what information is needed so that
* we do not waste time getting stuff the user does not need.
*/
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说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\" seconds of audio/video", "duration" },
1 /* if seeking requested, we execute it */
2 /* 5. 检测是否指定播放起始时间 */
3 if (start_time != AV_NOPTS_VALUE) {
4     int64_t timestamp;
5
6     timestamp = start_time;
7     /* add the stream start time */
8     if (ic->start_time != AV_NOPTS_VALUE)
9     timestamp += ic->start_time;
10     // seek的指定的位置开始播放
11     ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
12     if (ret < 0) {
13         av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position%0.3f\n",
14         is->filename, (double)timestamp / AV_TIME_BASE);
15     }
16 }

6 查找查找AVStream

⼀个媒体⽂件,对应有0n个⾳频流、0n个视频流、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 来选择,其原型为:

1 /**
2 * Find the "best" stream in the file.
3 * The best stream is determined according to various heuristics as the most
4 * likely to be what the user expects.
5 * If the decoder parameter is non-NULL, av_find_best_stream will find the
6 * default decoder for the stream's codec; streams for which no decoder can
7 * be found are ignored.
8 *
9 * @param ic media file handle
10 * @param type stream type: video, audio, subtitles, etc.
11 * @param wanted_stream_nb user-requested stream number,
12 * or -1 for automatic selection
13 * @param related_stream try to find a stream related (eg. in the same
14 * program) to this one, or -1 if none
15 * @param decoder_ret if non-NULL, returns the decoder for the
16 * selected stream
17 * @param flags flags; none are currently defined
18 * @return the non-negative stream number in case of success,
19 * AVERROR_STREAM_NOT_FOUND if no stream with the requested type
20 * could be found,
21 * AVERROR_DECODER_NOT_FOUND if streams were found but no decoder
22 * @note If av_find_best_stream returns successfully and decoder_ret is not
23 * NULL, then *decoder_ret is guaranteed to be set to a valid AVCodec.
24 */
25 int av_find_best_stream(AVFormatContext *ic,
26 enum AVMediaType type, //要选择的流类型
27 int wanted_stream_nb, //⽬标流索引
28 int related_stream, //相关流索引
29 AVCodec **decoder_ret,
30 int flags);

具体代码流程

1 //根据⽤户指定来查找流
2 for (i = 0; i < ic->nb_streams; i++) {
3     AVStream *st = ic->streams[i];
4     enum AVMediaType type = st->codecpar->codec_type;
5     st->discard = AVDISCARD_ALL;
6     if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
7         if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
8             st_index[type] = i;
9  }
10 for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
11    if (wanted_stream_spec[i] && st_index[i] == -1) {
12         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));
13         st_index[i] = INT_MAX;
14     }
15 }
16 //利⽤av_find_best_stream选择流,
17 if (!video_disable)
18     st_index[AVMEDIA_TYPE_VIDEO] =
19     av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
20                         st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
21 if (!audio_disable)
22     st_index[AVMEDIA_TYPE_AUDIO] =
23     av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
24                         st_index[AVMEDIA_TYPE_AUDIO],
25                         st_index[AVMEDIA_TYPE_VIDEO],
26                         NULL, 0);
27 if (!video_disable && !subtitle_disable)
28     st_index[AVMEDIA_TYPE_SUBTITLE] =
29     av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
30                         st_index[AVMEDIA_TYPE_SUBTITLE],
31                         (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
32                         st_index[AVMEDIA_TYPE_AUDIO] :
33                         st_index[AVMEDIA_TYPE_VIDEO]),
34                         NULL, 0);

如果⽤户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作⽤。

如果指定了正确的wanted_stream_nb,⼀般情况都是直接返回该指定流,即⽤户选择的流。

如果指定了相关流,且未指定⽬标流的情况,会在相关流的同⼀个节⽬中查找所需类型的流,但⼀般结果,都是返回该类型第1个流。

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

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

8 stream_component_open()

经过以上步骤,⽂件打开成功,且获取了流的基本信息,并选择⾳频流、视频流、字幕流。接下来就可以所选流对应的解码器了。

1 /* open the streams */
2 /* 5.打开视频、⾳频解码器。在此会打开相应解码器,并创建相应的解码线程。 */
3 if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
4     stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
5 }
6
7 ret = -1;
8 if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
9     ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
10 }
11 if (is->show_mode == SHOW_MODE_NONE) {
12     //选择怎么显示,如果视频打开成功,就显示视频画⾯,否则,显示⾳频对应的频谱图
13     is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;
14 }
15
16 if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
17     stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
18 }

⾳频、视频、字幕等流都要调⽤stream_component_open,他们直接有共同的流程,也有差异化的流程,差异化流程使⽤switch进⾏区分。具体原型

int stream_component_open(VideoState *is, int stream_index);
stream_index

看下 stream_component_open .函数也⽐较⻓,逐步分析:

1 /* 为解码器分配⼀个编解码器上下⽂结构体 */
2 avctx = avcodec_alloc_context3(NULL);
3 if (!avctx)
4     return AVERROR(ENOMEM);
5 /* 将码流中的编解码器信息拷⻉到新分配的编解码器上下⽂结构体 */
6 ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
7 if (ret < 0)
8     goto fail;
9 // 设置pkt_timebase
10 avctx->pkt_timebase = ic->streams[stream_index]->time_base;

先是通过 avcodec_alloc_context3 分配了解码器上下⽂ AVCodecContex ,然后通过avcodec_parameters_to_context 把所选流的解码参数赋给 avctx ,最后设了 time_base .

补充:avcodec_parameters_to_context 解码时⽤,avcodec_parameters_from_context则⽤于编码。

1 /* 根据codec_id查找解码器 */
2 codec = avcodec_find_decoder(avctx->codec_id);
3
4 switch(avctx->codec_type){// 获取指定的解码器名字,如果没有设置则为NULL
5     case AVMEDIA_TYPE_AUDIO : is->last_audio_stream = stream_index;
6         forced_codec_name = audio_codec_name; break; // 获取指定的解码器名字
7     case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index;
8         forced_codec_name = subtitle_codec_name; break; // 获取指定的解码器名字
9     case AVMEDIA_TYPE_VIDEO : is->last_video_stream = stream_index;
10         forced_codec_name = video_codec_name; break; // 获取指定的解码器名字
11 }
12 }
13 if (forced_codec_name)
14     codec = avcodec_find_decoder_by_name(forced_codec_name);
15 if (!codec) {
16     if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
17         "No codec could be found with name '%s'\n", forced_codec_name);
18     else av_log(NULL, AV_LOG_WARNING,
19         "No decoder could be found for codec %s\n", avcodec_get_name(avctx->codec_id));
20     ret = AVERROR(EINVAL);
21     goto fail;
22 }

这段主要是通过 avcodec_find_decoder 找到所需解码器(AVCodec)。如果⽤户有指定解码器,则设置 forced_codec_name ,并通过 avcodec_find_decoder_by_name 查找解码器。找到解码器

后,就可以通过 avcodec_open2 打开解码器了。(forced_codec_name对应到⾳频、视频、字幕不同的传⼊的解码器名字,如果有设置,⽐如ffplay -acodec aac xx.flv, 此时audio_codec_name设置为"aac",则相应的forced_codec_name为“aac”)

最后,是⼀个⼤的switch-case:

1 switch (avctx->codec_type) {
2     case AVMEDIA_TYPE_AUDIO:
3          sample_rate = avctx->sample_rate;
4          nb_channels = avctx->channels;
5          channel_layout = avctx->channel_layout;
6
7          /* prepare audio output 准备⾳频输出*/
8          if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
9             goto fail;
10         is->audio_hw_buf_size = ret;
11         is->audio_src = is->audio_tgt;
12         is->audio_buf_size = 0;
13         is->audio_buf_index = 0;
14
15         /* init averaging filter 初始化averaging滤镜, ⾮audio master时使⽤ */
16         is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB);
17         is->audio_diff_avg_count = 0;
18         /* 由于我们没有精确的⾳频数据填充FIFO,故只有在⼤于该阈值时才进⾏校正⾳频同步*/
19         is->audio_diff_threshold = (double)(is->audio_hw_buf_size) /is->audio_tgt.bytes_per_sec;
20
19
21         is->audio_stream = stream_index; // 获取audio的stream索引
22         is->audio_st = ic->streams[stream_index]; // 获取audio的stream指针
23         // 初始化ffplay封装的⾳频解码器
24         decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
25         if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
26             is->auddec.start_pts = is->audio_st->start_time;
27             is->auddec.start_pts_tb = is->audio_st->time_base;
28         }
29         // 启动⾳频解码线程
30         if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
31             goto out;
32         SDL_PauseAudioDevice(audio_dev, 0);
33         break;
34     case AVMEDIA_TYPE_VIDEO:
35         is->video_stream = stream_index; // 获取video的stream索引
36         is->video_st = ic->streams[stream_index];// 获取video的stream指针
37         // 初始化ffplay封装的视频解码器
38         decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
39         // 启动视频频解码线程
40         if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
41             goto out;
42         is->queue_attachments_req = 1; // 使能请求mp3、aac等⾳频⽂件的封⾯
43         break;
44     case AVMEDIA_TYPE_SUBTITLE: // 视频是类似逻辑处理
45         is->subtitle_stream = stream_index;
46         is->subtitle_st = ic->streams[stream_index];
47
48         decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
49         if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
50             goto out;
20
51         break;
52     default:
53         break;
54 }

即根据具体的流类型,作特定的初始化。但不论哪种流,基本步骤都包括了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循环。

4.2 For循环读取数据

主要包括以下步骤:

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

当退出事件发⽣时,调⽤do_exit() -> stream_close() -> 将is->abort_request置为1。退出该for循环,并最终退出该线程。

2. 检测是否暂停/继续

这⾥的暂停、继续只是对⽹络流有意义

1 // 2 检测是否暂停/继续
2 if (is->paused != is->last_paused) {
3     is->last_paused = is->paused;
4     if (is->paused)
5         is->read_pause_return = av_read_pause(ic); // ⽹络流的时候有⽤
6     else
7         av_read_play(ic);
8 }

⽐如rtsp
av_read_pause

1 /* pause the stream */
2 static int rtsp_read_pause(AVFormatContext *s)
3 {
4     RTSPState *rt = s->priv_data;
5     RTSPMessageHeader reply1, *reply = &reply1;
6
7     if (rt->state != RTSP_STATE_STREAMING)
8         return 0;
9     else if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {
10         ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, NULL);
11         if (reply->status_code != RTSP_STATUS_OK) {
12             return ff_rtsp_averror(reply->status_code, -1);
13         }
14     }
15     rt->state = RTSP_STATE_PAUSED;
16     return 0;
17 }

av_read_play

1 static int rtsp_read_play(AVFormatContext *s)
2 {
3     RTSPState *rt = s->priv_data;
4     RTSPMessageHeader reply1, *reply = &reply1;
5     ......
6     ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
7     ....
8     rt->state = RTSP_STATE_STREAMING;
9     return 0;
10 }
3. 检测是否需要seek
1 // 3 检测是否seek
2 if (is->seek_req) { // 是否有seek请求
3      int64_t seek_target = is->seek_pos;
4      int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
5      int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
6      // FIXME the +-2 is due to rounding being not done in the correct direction in generation
7      // of the seek_pos/seek_rel variables
8      // 修复由于四舍五⼊,没有再seek_pos/seek_rel变量的正确⽅向上进⾏
9      ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
10     if (ret < 0) {
11         av_log(NULL, AV_LOG_ERROR,
12         "%s: error while seeking\n", is->ic->url);
13     } else {
14         /* seek的时候,要把原先的数据情况,并重启解码器,put flush_pkt的⽬的是告知解码线程需要
15         * reset decoder
16         */
17         if (is->audio_stream >= 0) { // 如果有⾳频流
18             packet_queue_flush(&is->audioq); // 清空packet队列数据
19             // 放⼊flush pkt, ⽤来开起新的⼀个播放序列, 解码器读取到flush_pkt也清空解码器
20             packet_queue_put(&is->audioq, &flush_pkt);
21         }
22         if (is->subtitle_stream >= 0) { // 如果有字幕流
23             packet_queue_flush(&is->subtitleq); // 和上同理
24             packet_queue_put(&is->subtitleq, &flush_pkt);
25         }
26         if (is->video_stream >= 0) { // 如果有视频流
27             packet_queue_flush(&is->videoq); // 和上同理
28             packet_queue_put(&is->videoq, &flush_pkt);
29         }
30         if (is->seek_flags & AVSEEK_FLAG_BYTE) {
31             set_clock(&is->extclk, NAN, 0);
32         } else {
33             set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
34         }
35     }
36     is->seek_req = 0;
37     is->queue_attachments_req = 1;
38     is->eof = 0;
39     if (is->paused)
40     step_to_next_frame(is); // 如果本身是pause状态的则显示⼀帧继续暂停
41 }

主要的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状态的则显示⼀帧继续暂停
4. 检测video是否为attached_pic
1 // 4 检测video是否为attached_pic
2 if (is->queue_attachments_req) {
3      // attached_pic 附带的图⽚。⽐如说⼀些MP3,AAC⾳频⽂件附带的专辑封⾯,所以需要注意的是⾳频⽂件不⼀定只存在⾳频流本身
4      if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
5          AVPacket copy = { 0 };
6         if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
7             goto fail;
8         packet_queue_put(&is->videoq, &copy);
9         packet_queue_put_nullpacket(&is->videoq, is->video_stream);
10     }
11     is->queue_attachments_req = 0;
12 }

AV_DISPOSITION_ATTACHED_PIC 是⼀个标志。如果⼀个流中含有这个标志的话,那么就是说这个流是 *.mp3等 ⽂件中的⼀个 Video Stream 。并且该流只有⼀个 AVPacket ,也就是attached_pic 。这个 AVPacket 中所存储的内容就是这个 *.mp3等 ⽂件的封⾯图⽚。

因此,也可以很好的解释了⽂章开头提到的为什么 st->disposition & AV_DISPOSITION_ATTACHED_PIC 这个操作可以决定是否可以继续向缓冲区中添加 AVPacket 。

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

⾳频、视频、字幕队列都不是⽆限⼤的,如果不加以限制⼀直往队列放⼊packet,那将导致队列占⽤⼤量的内存空间,影响系统的性能,所以必须对队列的缓存⼤⼩进⾏控制。

PacketQueue默认情况下会有⼤⼩限制,达到这个⼤⼩后,就需要等待10ms,以让消费者——解码线程能有时间消耗。

1 // 5 检测队列是否已经有⾜够数据
2 /* if the queue are full, no need to read more */
3 /* 缓存队列有⾜够的包,不需要继续读取数据 */
4 if (infinite_buffer<1 && // 缓冲区不是⽆限⼤
5      (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
6      || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
7      stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
8      stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
9      /* wait 10 ms */
10     SDL_LockMutex(wait_mutex);
11     // 如果没有唤醒则超时10ms退出,⽐如在seek操作时这⾥会被唤醒
12     SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
13     SDL_UnlockMutex(wait_mutex);
14     continue;
15 }

缓冲区满有两种可能:

  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:

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

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

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

思路:
1.总数据⼤⼩
2.每个packet队列的情况。

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

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

PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕

1 // 6 检测码流是否已经播放结束
2 if (!is->paused // ⾮暂停
3     && // 这⾥的执⾏是因为码流读取完毕后 插⼊空包所致
4     (!is->audio_st // 没有⾳频流
5     || (is->auddec.finished == is->audioq.serial // 或者⾳频播放完毕
6     && frame_queue_nb_remaining(&is->sampq) == 0))
7     && (!is->video_st // 没有视频流
8     || (is->viddec.finished == is->videoq.serial // 或者视频播放完毕
9     && frame_queue_nb_remaining(&is->pictq) == 0))) {
10       if (loop != 1 // a 是否循环播放
11           && (!loop || --loop)) {
12             stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time :0, 0, 0);
13         } else if (autoexit) { // b 是否⾃动退出
14             ret = AVERROR_EOF;
15             goto fail;
16         }
17 }

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

  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中没有数据帧。

在确认⽬前码流已播放结束的情况下,⽤户有两个变量可以控制播放器⾏为:

  1. loop: 控制播放次数(当前这次也算在内,也就是最⼩就是1次了),0表示⽆限次
  2. 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不会释放其数据,⽽是每次都重新申请数据。

1 // 7.读取媒体数据,得到的是⾳视频分离后、解码前的数据
2 ret = av_read_frame(ic, pkt); // 调⽤不会释放pkt的数据,都是要⾃⼰去释放
8. 检测数据是否读取完毕
1 // 8 检测数据是否读取完毕
2 if (ret < 0) {
3     if ((ret == AVERROR_EOF || avio_feof(ic->pb))
4          && !is->eof)
5 {
6         // 插⼊空包说明码流数据读取完毕了,之前讲解码的时候说过刷空包是为了从解码器把所有帧都读出来
7         if (is->video_stream >= 0)
8             packet_queue_put_nullpacket(&is->videoq, is->video_stream);
9         if (is->audio_stream >= 0)
10           packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
11         if (is->subtitle_stream >= 0)
12             packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
13         is->eof = 1; // ⽂件读取完毕
14     }
15     if (ic->pb && ic->pb->error)
16         break;
17     SDL_LockMutex(wait_mutex);
18     SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
19     SDL_UnlockMutex(wait_mutex);
20     continue; // 继续循环 保证线程的运⾏,⽐如要seek到某个位置播放可以继续响应
21 } else {
22     is->eof = 0;
23 }

数据读取完毕后,放对应⾳频、视频、字幕队列插⼊“空包”,以通知解码器冲刷buffer,将缓存的所有数据都解出来frame并去出来。

然后继续在for{}循环,直到收到退出命令,或者loop播放,或者seek等操作。

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

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

1 // 9 检测是否在播放范围内
2 /* check if packet is in play range specified by user, then queue, otherwise discard */
3 stream_start_time = ic->streams[pkt->stream_index]->start_time;// 获取流的起始时间
4 pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;// 获取packet的时间戳
5 // 这⾥的duration是在命令⾏时⽤来指定播放⻓度
6 pkt_in_play_range = duration == AV_NOPTS_VALUE ||
7 (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
8 av_q2d(ic->streams[pkt->stream_index]->time_base) -
9 (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
10 <= ((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)且有效时,主要判断

1 (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time :0)) *
2 av_q2d(ic->streams[pkt->stream_index]->time_base) -
3 (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
4 <= ((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中
2 if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
3     packet_queue_put(&is->audioq, pkt);
4 } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
5 && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
6     //printf("pkt pts:%ld, dts:%ld\n", pkt->pts, pkt->dts);
7     packet_queue_put(&is->videoq, pkt);
8 } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
9     packet_queue_put(&is->subtitleq, pkt);
10 } else {
11     av_packet_unref(pkt);// 不⼊队列则直接释放数据
12 }

这⾥的代码就很直⽩了,将packet放⼊到对应的PacketQueue

4.3 退出线程处理

主要包括以下步骤:

  1. 如果解复⽤器有打开则关闭avformat_close_input
  2. 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
    a. 发送的FF_QUIT_EVENT退出播放事件由event_loop()函数相应,收到FF_QUIT_EVENT后调⽤do_exit()做退出操作。
  3. 消耗互斥量wait_mutex
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容