ffplay.c学习-2-数据读取线程


目录

  1. 准备⼯作

    1. avformat_alloc_context 创建上下⽂
    2. ic->interrupt_callback
    3. avformat_open_input()打开媒体⽂件
    4. avformat_find_stream_info()
    5. 检测是否指定播放起始时间
    6. 查找查找AVStream
    7. 通过AVCodecParameters和av_guess_sample_aspect_ratio计算出显示窗⼝的宽、⾼
    8. stream_component_open()
  2. For循环读取数据
    1. 检测是否退出
    2. 检测是否暂停/继续
    3. 检测是否需要seek
    4. 检测video是否为attached_pic
    5. 检测队列是否已经有⾜够数据
    6. 检测码流是否已经播放结束
    7. 使⽤av_read_frame读取数据包
    8. 检测数据是否读取完毕
    9. 检测是否在播放范围内
    10. 到这步才将数据插⼊对应的队列
  3. 退出线程处理

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

    1. 准备⼯作
    2. For循环读取数据
    3. 退出线程处理

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];
    1. 根据⽤户指定来查找流avformat_match_stream_specifier
    2. 使⽤av_find_best_stream查找流
  7. 从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
  8. stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参数的初始化

2. For循环读取数据

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

3. 退出线程处理

  1. 如果解复⽤器有打开则关闭avformat_close_input
  2. 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
  3. 消耗互斥量wait_mutex

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];
    1. 根据⽤户指定来查找流avformat_match_stream_specifier
    2. 使⽤av_find_best_stream查找流
  7. 从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
  8. stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参数的初始化

1. avformat_alloc_context 创建上下⽂

  1. 调⽤avformat_alloc_context创建解复⽤器上下⽂
// 1. 创建上下文结构体,这个结构体是最上层的结构体,表示输入上下文ic = avformat_alloc_context();
  1. 最终该ic 赋值给VideoState的ic变量
is->ic = ic;    // videoState的ic指向分配的ic

2. ic->interrupt_callback

/* 2.设置中断回调函数,如果出错或者退出,就根据目前程序设置的状态选择继续check或者直接退出 *//* 当执行耗时操作时(一般是在执行while或者for循环的数据读取时),会调用interrupt_callback.callback* 回调函数中返回1则代表ffmpeg结束耗时操作退出当前函数的调用* 回调函数中返回0则代表ffmpeg内部继续执行耗时操作,直到完成既定的任务(比如读取到既定的数据包)*/ic->interrupt_callback.callback = decode_interrupt_cb;ic->interrupt_callback.opaque = is;
  1. interrupt_callback⽤于ffmpeg内部在执⾏耗时操作时检查调⽤者是否有退出请求,避免⽤户退出请求没有及时响应.
/*** @brief 这里是设置给ffmpeg内部,当ffmpeg内部当执行耗时操作时(一般是在执行while或者for循环的数据读取时)*          就会调用该函数* @param ctx* @return 若直接退出阻塞则返回1,等待读取则返回0*/static int decode_interrupt_cb(void *ctx) {static int64_t s_pre_time = 0;int64_t cur_time = av_gettime_relative() / 1000;printf("decode_interrupt_cb interval:%lldms\n", cur_time - s_pre_time);s_pre_time = cur_time;VideoState *is = (VideoState *) ctx;return is->abort_request;
}

3. avformat_open_input()打开媒体⽂件

  1. 函数原型:
/*** 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);
  1. avformat_open_input⽤于打开输⼊⽂件(对于RTMP/RTSP/HTTP⽹络流也是⼀样,在ffmpeg内部都抽象为URLProtocol,这⾥描述为⽂件是为了⽅便与后续提到的AVStream的流作区分),读取视频⽂件的基本信息
  2. 需要提到的两个参数是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;}
  1. scan_all_pmts是mpegts的⼀个选项,表示扫描全部的ts流的"Program Map Table"表。这⾥在没有设定该选项的时候,强制设为1。最后执⾏avformat_open_input。
  2. 参数的设置最终都是设置到对应的解复⽤器,⽐如:
static const AVOption options[] = {MPEGTS_OPTIONS,{"fix_teletext_pts", "try to fix pts values of dvb teletext streams", offsetof(MpegTSContext, fix_teletext_pts), AV_OPT_TYPE_BOOL,{.i64 = 1}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },{"ts_packetsize", "output option carrying the raw packet size", offsetof(MpegTSContext, raw_packet_size), AV_OPT_TYPE_INT,{.i64 = 0}, 0, 0, AV_OPT_FLAG_DECODING_PARAM | AV_OPT_FLAG_EXPORT | AV_OPT_FLAG_READONLY },{"scan_all_pmts", "scan and combine all PMTs", offsetof(MpegTSContext, scan_all_pmts), AV_OPT_TYPE_BOOL,{.i64 = -1}, -1, 1, AV_OPT_FLAG_DECODING_PARAM },{"skip_unknown_pmt", "skip PMTs for programs not advertised in the PAT", offsetof(MpegTSContext, skip_unknown_pmt), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },{"merge_pmt_versions", "re-use streams when PMT's version/pids change", offsetof(MpegTSContext, merge_pmt_versions), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1,  AV_OPT_FLAG_DECODING_PARAM },{"skip_changes", "skip changing / adding streams / programs", offsetof(MpegTSContext, skip_changes), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, 0 },{"skip_clear", "skip clearing programs", offsetof(MpegTSContext, skip_clear), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, 0 },{ NULL },
};

4. avformat_find_stream_info()

  1. 在打开了⽂件后,就可以从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);
  1. 该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说MPEG(⾥应该说ts更准确)(FLV⽂件也是需要读取packet 分析流信息)。⽽被读取⽤以分析流信息的数据可能被缓存,供av_read_frame时使⽤,在播放时并不会跳过这部分packet的读取。

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

  1. 如果指定时间则seek到指定位置avformat_seek_file。
  2. 可以通过 ffplay -ss 设置起始时间,时间格式hh:mm:ss,⽐如
  3. ffplay -ss 00:00:30 test.flv 则是从30秒的起始位置开始播放。
{"ss", HAS_ARG, {.func_arg = opt_seek}, "seek to a given position in seconds", "pos"},
{"t", HAS_ARG, {.func_arg = opt_duration}, "play  \"duration\" seconds of audio/video", "duration"},
/* if seeking requested, we execute it *//* 5. 检测是否指定播放起始时间 */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;// seek的指定的位置开始播放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

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

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

    1. 在播放起始指定对应的流
    2. 使⽤缺省的流进⾏播放
1 在播放起始指定对应的流
{"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"},
  1. 可以通过

    1. -ast n 指定⾳频流(⽐如我们在看电影时,有些电影可以⽀持普通话和英⽂切换,此时可以⽤该命令进⾏选择)
    2. -vst n 指定视频流
    3. -vst n 指定字幕流
  2. 将对应的index值记录到st_index[AVMEDIA_TYPE_NB];

2. 使⽤缺省的流进⾏播放
  1. 如果我们没有指定,则ffplay主要是通过 av_find_best_stream 来选择,其原型为:
/*** Find the "best" stream in the file.* The best stream is determined according to various heuristics as the most* likely to be what the user expects.* If the decoder parameter is non-NULL, av_find_best_stream will find the* default decoder for the stream's codec; streams for which no decoder can* be found are ignored.** @param ic                media file handle* @param type              stream type: video, audio, subtitles, etc.* @param wanted_stream_nb  user-requested stream number,*                          or -1 for automatic selection* @param related_stream    try to find a stream related (eg. in the same*                          program) to this one, or -1 if none* @param decoder_ret       if non-NULL, returns the decoder for the*                          selected stream* @param flags             flags; none are currently defined* @return  the non-negative stream number in case of success,*          AVERROR_STREAM_NOT_FOUND if no stream with the requested type*          could be found,*          AVERROR_DECODER_NOT_FOUND if streams were found but no decoder* @note  If av_find_best_stream returns successfully and decoder_ret is not*        NULL, then *decoder_ret is guaranteed to be set to a valid AVCodec.*/
int av_find_best_stream(AVFormatContext *ic,enum AVMediaType type,int wanted_stream_nb,int related_stream,AVCodec **decoder_ret,int flags);
// 6.2 利用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);
  1. 如果⽤户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作⽤。
  2. 如果指定了正确的wanted_stream_nb,⼀般情况都是直接返回该指定流,即⽤户选择的流。
  3. 如果指定了相关流,且未指定⽬标流的情况,会在相关流的同⼀个节⽬中查找所需类型的流,但⼀般结果,都是返回该类型第1个流。

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

//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);}}
  1. 具体流程如上所示,这⾥实质只是设置了default_width、default_height变量的⼤⼩,没有真正改变窗⼝的⼤⼩。真正调整窗⼝⼤⼩是在视频显示调⽤video_open()函数进⾏设置。

8. stream_component_open()

  1. 经过以上步骤,⽂件打开成功,且获取了流的基本信息,并选择⾳频流、视频流、字幕流。接下来就可以所选流对应的解码器了。
    /* open the streams *//* 8. 打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。 */if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {// 如果有音频流则打开音频流stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);}ret = -1;if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { // 如果有视频流则打开视频流ret = stream_component_open(is, 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(is, st_index[AVMEDIA_TYPE_SUBTITLE]);}
  1. ⾳频、视频、字幕等流都要调⽤stream_component_open,他们直接有共同的流程,也有差异化的流程,差异化流程使⽤switch进⾏区分。具体原型
/* 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)
  1. 看下 stream_component_open .函数也⽐较⻓,逐步分析:
    /*  为解码器分配一个编解码器上下文结构体 */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_timebaseavctx->pkt_timebase = ic->streams[stream_index]->time_base;
  1. 先是通过 avcodec_alloc_context3 分配了解码器上下⽂ AVCodecContex ,然后通过avcodec_parameters_to_context 把所选流的解码参数赋给 avctx ,最后设了 time_base .
  2. 补充:avcodec_parameters_to_context 解码时⽤,avcodec_parameters_from_context则⽤于编码。
    /* 根据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);elseav_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;}
  1. 这段主要是通过 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”)
  2. 最后,是⼀个⼤的switch-case:
    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);}
#elsesample_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;}
  1. 即根据具体的流类型,作特定的初始化。但不论哪种流,基本步骤都包括了ffplay封装的解码器的初始化和启动解码器线程:

    1. 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
    
    1. decoder_start启动解码器
    packet_queue_start 启⽤对应的packet 队列
    SDL_CreateThread 创建对应的解码线程
    
  2. 需要注意的是,对应⾳频⽽⾔,这⾥还初始化了输出参数,这块在讲⾳频输出的时候再重点展开

  3. 以上是准备的⼯作,我们再来看for循环。


2. For循环读取数据

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

1. 检测是否退出

// 1 检测是否退出if (is->abort_request)break;
  1. 当退出事件发⽣时,调⽤do_exit() -> stream_close() -> 将is->abort_request置为1。退出该for循环,并最终退出该线程

2. 检测是否暂停/继续

  1. 这⾥的暂停、继续只是对⽹络流有意义
// 2 检测是否暂停/继续if (is->paused != is->last_paused) {is->last_paused = is->paused;if (is->paused)is->read_pause_return = av_read_pause(ic); // 网络流的时候有用elseav_read_play(ic);}
  1. ⽐如rtsp
1. av_read_pause
/* 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;
}
2. av_read_play
static int rtsp_read_play(AVFormatContext *s)
{RTSPState *rt = s->priv_data;RTSPMessageHeader reply1, *reply = &reply1;...ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);...rt->state = RTSP_STATE_STREAMING;return 0;

3. 检测是否需要seek

        //  3 检测是否seekif (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);}
  1. 主要的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. 同步外部时钟。在后续⾳视频同步的课程中再具体分析。
  2. 这⾥还要注意:如果播放器本身是pause的状态,则

if (is->paused)
step_to_next_frame(is); // 如果本身是pause状态的则显示⼀帧继续暂停

4. 检测video是否为attached_pic (专辑封面)

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

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

  1. ⾳频、视频、字幕队列都不是⽆限⼤的,如果不加以限制⼀直往队列放⼊packet,那将导致队列占⽤⼤量的内存空间,影响系统的性能,所以必须对队列的缓存⼤⼩进⾏控制。
  2. PacketQueue默认情况下会有⼤⼩限制,达到这个⼤⼩后,就需要等待10ms,以让消费者——解码线程能有时间消耗。
        // 5 检测队列是否已经有足够数据/* 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. 缓冲区满有两种可能:

    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者要同时成⽴
  2. 第⼀种好理解,看下第⼆种中的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_PICqueue->nb_packets > MIN_FRAMES // packet数>25&& (!queue->duration ||     // 满足PacketQueue总时长为0av_q2d(st->time_base) * queue->duration > 1.0); //或总时长超过1s
}
  1. 有这么⼏种情况包是够⽤的:

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

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

  1. ⾮暂停状态才进⼀步检测码流是否已经播放完毕(注意:数据播放完毕和码流数据读取完毕是两个概念。)
  2. PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕
        // 6 检测码流是否已经播放结束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           // a 是否循环播放&& (!loop || --loop)) {// stream_seek不是ffmpeg的函数,是ffplay封装的,每次seek的时候会调用stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);} else if (autoexit) {  // b 是否自动退出ret = AVERROR_EOF;goto fail;}}
  1. 这⾥判断播放已完成的条件需要同时满⾜满⾜:

    1. 不在暂停状态
    2. ⾳频未打开;或者打开了,但是解码已解完所有packet,⾃定义的解码器(decoder)serial等于PacketQueue的serial,并且FrameQueue中没有数据帧
    PacketQueue.serial -> packet.serail -> decoder.pkt_serial(赋值路径)
    decoder.finished = decoder.pkt_serial
    
    1. is->auddec.finished == is->audioq.serial 最新的播放序列的packet都解码完毕
    2. frame_queue_nb_remaining(&is->sampq) == 0 对应解码后的数据也播放完毕
    3. 视频未打开;或者打开了,但是解码已解完所有packet,⾃定义的解码器(decoder)serial等于PacketQueue的serial,并且FrameQueue中没有数据帧
  2. 在确认⽬前码流已播放结束的情况下,⽤户有两个变量可以控制播放器⾏为:

    1. loop: 控制播放次数(当前这次也算在内,也就是最⼩就是1次了),0表示⽆限次
    2. autoexit:⾃动退出,也就是播放完成后⾃动退出。
  3. loop条件简化的⾮常不友好,其意思是:如果loop==1,那么已经播了1次了,⽆需再seek重新播放;如果loop不是1,==0,随意,⽆限次循环;减1后还⼤于0(–loop),也允许循环

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

7. 使⽤av_read_frame读取数据包

  1. 读取数据包很简单,但要注意传⼊的packet,av_read_frame不会释放其数据,⽽是每次都重新申请数据
        // 7.读取媒体数据,得到的是音视频分离后、解码前的数据ret = av_read_frame(ic, pkt); // 调用不会释放pkt的数据,需要我们自己去释放packet的数据

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

        // 8 检测数据是否读取完毕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;        // 继续循环} else {is->eof = 0;}
  1. 数据读取完毕后,放对应⾳频、视频、字幕队列插⼊“空包”,以通知解码器冲刷buffer,将缓存的所有数据都解出来frame并去出来。
  2. 然后继续在for{}循环,直到收到退出命令,或者loop播放,或者seek等操作。

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

  1. 播放器可以设置:-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; // 获取packet的时间戳// 这里的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);
  1. 从流获取的参数

    1. stream_start_time:是从当前流AVStream->start_time获取到的时间,如果没有定义具体的值则默认为AV_NOPTS_VALUE,即该值是⽆效的;那stream_start_time有意义的就是0值;
    2. pkt_ts:当前packet的时间戳,pts有效就⽤pts的,pts⽆效就⽤dts的;ffplay播放的参数
  2. ffplay播放的参数
    1. duration: 使⽤"-t value"指定的播放时⻓,默认值AV_NOPTS_VALUE,即该值⽆效不⽤参考
    2. start_time:使⽤“-ss value”指定播放的起始位置,默认AV_NOPTS_VALUE,即该值⽆效不⽤参考
  3. pkt_in_play_range的值为0或1。
    1. 当没有指定duration播放时⻓时,很显然duration == AV_NOPTS_VALUE的逻辑值为1,所以pkt_in_play_range为1;
    2. 当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);
  1. 实质就是当前时间戳 pkt_ts - start_time 是否 < duration,这⾥分为:

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

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

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);// // 不入队列则直接释放数据}

3. 退出线程处理

  1. 主要包括以下步骤:

    1. 如果解复⽤器有打开则关闭avformat_close_input
    2. 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
      a. 发送的FF_QUIT_EVENT退出播放事件由event_loop()函数相应,收到FF_QUIT_EVENT后调⽤
      do_exit()做退出操作。
    3. 消耗互斥量wait_mutex
    ret = 0;fail:if (ic && !is->ic)avformat_close_input(&ic);if (ret != 0) {SDL_Event event;event.type = FF_QUIT_EVENT;event.user.data1 = is;SDL_PushEvent(&event);}SDL_DestroyMutex(wait_mutex);

ffplay.c学习-2-数据读取线程相关推荐

  1. TF学习——TF数据读取:TensorFlow中数据读这三张图片的5个epoch +把读取的结果重新存到read 文件夹中

    TF学习--TF数据读取:TensorFlow中数据读这三张图片的5个epoch +把读取的结果重新存到read 文件夹中 目录 实验展示 代码实现 实验展示 代码实现 1.如果设置shuffle为T ...

  2. 【深度学习】数据读取与数据扩增方法

    转载自:Datawhale,作者:樊亮.黄星源.Datawhale优秀学习者 本文对图像数据读取及图像数据扩增方法进行了总结,并以阿里天池零基础入门CV赛事为实践,利用Pytorch对数据进行了读取和 ...

  3. TensorFlow-谷歌深度学习库 数据读取器

    先介绍一下TensorFlow自带的数据格式: TensorFlow自带一种数据格式叫做tfrecords. 你可以把你的输入转成专属与TensorFlow的tfrecords格式并保存在本地. -关 ...

  4. Spark学习笔记:数据读取和保存

    spark所支持的文件格式 1.文本文件 在 Spark 中读写文本文件很容易. 当我们将一个文本文件读取为 RDD 时,输入的每一行 都会成为 RDD 的 一个元素. 也可以将多个完整的文本文件一次 ...

  5. R语言学习:数据读取以及简单运算

    这是去年业余时间学到的R语言基础知识,打算入门的朋友可以进来看看,大佬请绕道. 这一章记录的是关于R语言数据的读取和一些简单的运算方法. 如有疑问,欢迎留言说明. 数组的基础知识 判断是否是数组is. ...

  6. 深度学习入门笔记(十二):深度学习数据读取

    欢迎关注WX公众号:[程序员管小亮] 专栏--深度学习入门笔记 声明 1)该文章整理自网上的大牛和机器学习专家无私奉献的资料,具体引用的资料请看参考文献. 2)本文仅供学术交流,非商用.所以每一部分具 ...

  7. ffplay.c学习-1-框架及数据结构

    ffplay.c学习-1-框架及数据结构 目录 ffplay.c的意义 FFplay框架分析 数据结构分析 struct VideoState 播放器封装 struct Clock 时钟封装 stru ...

  8. 期货数据读取python从新浪财经

    用途:读取股票A股,期货,股指期货历史数据 版本3: 更新时间:2022-11-20 说明:类封装Sina其他: 本人是小白,没有钱购买数据,推荐几个免费的数据读取. 掘金的期货数据相对来说比较多,支 ...

  9. ffplay.c学习-3-音视频解码线程

    ffplay.c学习-3-音视频解码线程 目录 解码线程 视频解码线程 video_thread() get_video_frame() 同⼀播放序列流连续的情况下,不断调⽤avcodec_recei ...

最新文章

  1. java银行柜面发起授权功能_java银行自主柜员程序设计
  2. 干货下载 | 高效金融客户分析体系如何搭建?
  3. Android源码下载(ubuntu12.04(amd64))
  4. 解读设计模式----简单工厂模式(SimpleFactory Pattern),你要什么我就给你什么
  5. 6 个步骤,教你在Ubuntu虚拟机环境下,用Docker自带的DNS配置Hadoop | 附代码
  6. Vue动态传值与接收步骤
  7. linux c++ 实现http请求
  8. 随想录(校园招聘记)
  9. Oracle中shrink space命令详解
  10. java trunc函数_TRUNC函数的用法
  11. 机器学习实战 Tricks
  12. Android 拦截TextView中超链接点击事件
  13. 设置Kafka集群的方法
  14. foobar2000播放dff格式音乐的解决办法
  15. Postman安装与简单使用
  16. 智能终端代替传统设备 手机APP离线识别车牌号
  17. 并发编程之四:并发之共享问题、线程安全、synchronized关键字
  18. 使用vue-admin-template搭建简单增删改查导入导出项目及CentOs服务器部署
  19. php 自定义图片排版,照片拼图在线制作 自带多种照片拼图模板,自由排版将多张照片合成一张...
  20. netsh interface portproxy listenaddress

热门文章

  1. 江苏:5G先行,智慧江苏再进一步
  2. 如何取消 DiscuzX 帖子被系统自动隐?
  3. Linux下C编程实战
  4. 对个人站长职业前景的探讨之路在何方?
  5. 用Linux命令行生成随机密码的十种方法
  6. 反编译工具Reflector下载(集成FileGenerator和FileDisassembler)
  7. NOD32客户端更新文件
  8. 教你移除IE 7.0浏览器的默认搜索框
  9. PHP与MySQL开发中页面乱码的产生与解决
  10. CodeForces - 715A Plus and Square Root(思维+构造)