​前言:​

大家好,我是小涂,今天继续给大家分享ffplay源码解析,今天也是最后一篇关于read_thread线程的解析,分享完这个之后,会接着分享视频和音频解码线程以及音频输出、视频输出模块,大概率每个礼拜一篇,很快就会进入到实战篇写一个播放器,前期解析ffplay源码,主要是要先了解这个优秀的播放器框架,后期我们就可以在这个基础上借鉴前人的优秀思想,来做一个自己的播放器。

今天主要继续分享read_thread线程里面的for循环读取数据这部分的源码:

ffplay之read_thread线程里的for循环读取数据源码解读_数据for循环读取队列里面的数据

这部分代码主要分为下面几个部分介绍:


  • 检测是否退出
  • 检测是否暂停/继续
  • 检测是否需要seek
  • 检测video是否为attached_pic
  • 检测队列是否已经有⾜够数据
  • 检测码流是否已经播放结束


  1. 是否循环播放
  2. 是否⾃动退出

  • 使⽤av_read_frame读取数据包
  • 检测数据是否读取完毕
  • 检测是否在播放范围内
  • 将数据插⼊对应的队列

​for循环读取数据源码解析:​

1、检测是否退出:

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

这里的意思,当如果我们退出一个正在播放的媒体流文件的时候,会把abort_request赋值为1,并退出for循环,并退出read_thread线程;那么它是在哪里被赋值为1了呢,通过在ffplay.c里面搜索,我们可以发现在stream_close这个接口里面进行了赋值:

ffplay之read_thread线程里的for循环读取数据源码解读_数据_02

2、检测是否暂停/继续:

// 检测是否暂停/继续
// paused=1时暂停,paused=0时播放
//last_paused=1时暂存“暂停”,last_paused=0时“播放”状态
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);
}

这里说的网络流,指的是rtsp流:

/**
* Pause a network-based stream (e.g. RTSP stream).
*
* Use av_read_play() to resume it.
*/
int av_read_pause(AVFormatContext *s);

ffplay之read_thread线程里的for循环读取数据源码解读_sed_03ffplay之read_thread线程里的for循环读取数据源码解读_数据_04

3、检测是否需要seek:

// 检测是否seek
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);
}

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


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

这⾥还要注意:如果播放器本身是pause的状态,则:

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

4、检测video是否为attached_pic:

//检测video是否为attached_pic
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;
}

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

/**
* For streams with AV_DISPOSITION_ATTACHED_PIC disposition, this packet
* will contain the attached picture.
*
* decoding: set by libavformat, must not be modified by the caller.
* encoding: unused
*/
AVPacket attached_pic;

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;

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

同时缓冲区满有两种可能:

  • audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M,为什么 是15M?这⾥只是⼀个经验计算值,⽐如4K视频的码率以50Mbps计算,则15MB可以缓存2.4秒,从 这么计算实际上如果我们真的是播放4K⽚源,15MB是偏⼩的数值,有些⽚源⽐较坑 同⼀个⽂件位置 附近的pts差值超过5秒,此时如果视频要缓存5秒才能做同步,那15MB的缓存⼤⼩就不够了);这里MAX_QUEUE_SIZE在ffplay默认设置成15M:
#define MAX_QUEUE_SIZE (15 * 1024 * 1024)
  • ⾳频、视频、字幕流都已有够⽤的包(stream_has_enough_packets),注意:3者要同时成⽴
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
}

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


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

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 // 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;
}
}

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


  • 不在暂停状态
  • ⾳频未打开;或者打开了,但是解码已解完所有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 对应解码后的数据也播放完毕
  • 视频未打开;或者打开了,但是解码已解完所有packet,⾃定义的解码器(decoder)serial等于 PacketQueue的serial,并且FrameQueue中没有数据帧。

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


  • loop: 控制播放次数(当前这次也算在内,也就是最⼩就是1次了),0表示⽆限次
  • autoexit:⾃动退出,也就是播放完成后⾃动退出。

loop条件简化的⾮常不友好,其意思是:如果loop==1,那么已经播了1次了,⽆需再seek重新播放;如果 loop不是1,==0,随意,⽆限次循环;减1后还⼤于0(--loop),也允许循环。

是否循环播放:

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

是否⾃动退出:如果播放完毕⾃动退出

7、使⽤av_read_frame读取数据包:

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

//读取媒体数据,得到的是音视频分离后、解码前的数据
ret = av_read_frame(ic, pkt); // 调用不会释放pkt的数据,需要我们自己去释放packet的数据

ffplay之read_thread线程里的for循环读取数据源码解读_数据_05

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;
}

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

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; // 获取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);

从流获取的参数:


  • 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、将数据插⼊对应的队列

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

这里主要是把对应的音频、视频、字幕数据分别插入到对应的音频、视频、字幕队列中去。

最后,我们再来总结一下,通过上面的一系列操作,我们的数据终于送到对应的数据队列了,接下来就是就行从队列里面取数据,来进行解码:

ffplay之read_thread线程里的for循环读取数据源码解读_sed_06

​总结:​

好了,本期就总结分享到这里,我们下期继续!