音视频流媒体开发【二十三】ffplay播放器-架构及数据结构分析

音视频流媒体开发-目录

1 ffplay.c的意义

ffplay.c是FFmpeg源码⾃带的播放器,调⽤FFmpeg和SDL API实现⼀个⾮常有⽤的播放器。 例如哔哩哔哩著名开源项⽬ijkplayer也是基于ffplay.c进⾏⼆次开发。

ffplay实现了播放器的主体功能,掌握其原理对于我们独⽴开发播放器⾮常有帮助。

2 FFplay框架分析

播放器初始化
  • 初始化packet queue
  • 初始化frame queue
  • 初始化clock
  • 创建数据读取线程
线程的划分
  • 数据读取线程
    • 打开媒体⽂件
    • 打开对应码流的decoder以及初始化对应的audio、video、subtitle输出
    • 创建decoder线程,audio、video和subtitle的解码线程独⽴
    • 调⽤av_read_frame读取packet,并根据steam_index放⼊不同stream对应的packet队列
  • ⾳频解码
    • 从packet queue读取packet,解出frame后放⼊frame queue
  • 视频解码
    • 从packet queue读取packet,解出frame后放⼊frame queue
  • 字幕解码
    • 从packet queue读取packet,解出frame后放⼊frame queue
  • ⾳频播放(或者回调函数)
    • 从frame queue读取frame进⾏播放
  • 视频播放(ffplay⽬前是在main主线程进⾏视频播放)
    • 从frame queue读取frame进⾏播放
  • 字幕播放(ffplay⽬前是在main主线程进⾏字幕播放)
    • 从frame queue读取frame进⾏播放
  • 控制响应(播放/暂停/快进/快退等)(ffplay⽬前是在main主线程进⾏播放控制)
packet队列的设计
  • 线程安全,⽀持互斥、等待、唤醒
  • 缓存数据⼤⼩
  • 缓存包数
  • 队列播放可持续时间
  • 进队列/出队列等
⾳视频同步
  • ⾳频同步
  • 视频同步
  • 外部时钟同步
⾳频处理
  • ⾳量调节
  • 静⾳
  • 重采样
视频处理
  • 图像格式转换YUV->RGB等
  • 图像缩放1280720->800480等
播放器控制
  • 播放
  • 暂停
  • 停⽌
  • 快进/快退
  • 逐帧
  • 静⾳

3 数据结构分析

struct VideoState 播放器封装

1 typedef struct VideoState {
2     SDL_Thread *read_tid; // 读线程句柄
3     AVInputFormat *iformat; // 指向demuxer
4     int abort_request; // =1时请求退出播放
5     int force_refresh; // =1时需要刷新画⾯,请求⽴即刷新画⾯的意思
6     int paused; // =1时暂停,=0时播放
7     int last_paused; // 暂存“暂停”/“播放”状态
8     int queue_attachments_req;
9     int seek_req; // 标识⼀次seek请求
10    int seek_flags; // seek标志,诸如AVSEEK_FLAG_BYTE等
11    int64_t seek_pos; // 请求seek的⽬标位置(当前位置+增量)
12    int64_t seek_rel; // 本次seek的位置增量
13    int read_pause_return;
14    AVFormatContext *ic; // iformat的上下⽂
15    int realtime; // =1为实时流
16
17    Clock audclk; // ⾳频时钟
18    Clock vidclk; // 视频时钟
19    Clock extclk; // 外部时钟
20
21    FrameQueue pictq; // 视频Frame队列
22    FrameQueue subpq; // 字幕Frame队列
23    FrameQueue sampq; // 采样Frame队列
24
25    Decoder auddec; // ⾳频解码器
26    Decoder viddec; // 视频解码器
27    Decoder subdec; // 字幕解码器
28
29    int audio_stream ; // ⾳频流索引
30
31    int av_sync_type; // ⾳视频同步类型, 默认audio master
32
33    double audio_clock; // 当前⾳频帧的PTS+当前帧Duration
34    int audio_clock_serial; // 播放序列,seek可改变此值
35    // 以下4个参数 ⾮audio master同步⽅式使⽤
36    double audio_diff_cum; // used for AV difference average computation
37    double audio_diff_avg_coef;
38    double audio_diff_threshold;
39    int audio_diff_avg_count;
40    // end
41
42    AVStream *audio_st; // ⾳频流
43    PacketQueue audioq; // ⾳频packet队列
44    int audio_hw_buf_size; // SDL⾳频缓冲区的⼤⼩(字节为单位)
45    // 指向待播放的⼀帧⾳频数据,指向的数据区将被拷⼊SDL⾳频缓冲区。若经过重采样则指向audio_buf1,
46    // 否则指向frame中的⾳频
47    uint8_t *audio_buf; // 指向需要重采样的数据
48    uint8_t *audio_buf1; // 指向重采样后的数据
49    unsigned int audio_buf_size; // 待播放的⼀帧⾳频数据(audio_buf指向)的⼤⼩
50    unsigned int audio_buf1_size; // 申请到的⾳频缓冲区audio_buf1的实际尺⼨
51    int audio_buf_index; // 更新拷⻉位置 当前⾳频帧中已拷⼊SDL⾳频缓冲区
52    // 的位置索引(指向第⼀个待拷⻉字节)
53    // 当前⾳频帧中尚未拷⼊SDL⾳频缓冲区的数据量:
54    // audio_buf_size = audio_buf_index + audio_write_buf_size
55    int audio_write_buf_size;
56    int audio_volume; // ⾳量
57    int muted; // =1静⾳,=0则正常
58    struct AudioParams audio_src; // ⾳频frame的参数
59 #if CONFIG_AVFILTER
60    struct AudioParams audio_filter_src;
61 #endif
62    struct AudioParams audio_tgt; // SDL⽀持的⾳频参数,重采样转换:audio_src->audio_tgt
63    struct SwrContext *swr_ctx; // ⾳频重采样context
64    int frame_drops_early; // 丢弃视频packet计数
65    int frame_drops_late; // 丢弃视频frame计数
66
67    enum ShowMode {
68       SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
69    } show_mode;
70
71    // ⾳频波形显示使⽤
72    int16_t sample_array[SAMPLE_ARRAY_SIZE];
73    int sample_array_index;
74    int last_i_start;
75    RDFTContext *rdft;
76    int rdft_bits;
77    FFTSample *rdft_data;
78
79    int xpos;
80    double last_vis_time;
81    SDL_Texture *vis_texture;
82
83    SDL_Texture *sub_texture; // 字幕显示
84    SDL_Texture *vid_texture; // 视频显示
85
86    int subtitle_stream; // 字幕流索引
87    AVStream *subtitle_st; // 字幕流
88    PacketQueue subtitleq; // 字幕packet队列
89
90    double frame_timer; // 记录最后⼀帧播放的时刻
91    double frame_last_returned_time;
92    double frame_last_filter_delay;
93
94    int video_stream; // 视频流索引
95    AVStream *video_st; // 视频流
96    PacketQueue videoq; // 视频队列
97    double max_frame_duration; // ⼀帧最⼤间隔. above this, we consider the jump a timestamp discontinuity
98    struct SwsContext *img_convert_ctx; // 视频尺⼨格式变换
99    struct SwsContext *sub_convert_ctx; // 字幕尺⼨格式变换
100   int eof; // 是否读取结束
101
102   char *filename; // ⽂件名
103   int width, height, xleft, ytop; // 宽、⾼,x起始坐标,y起始坐标
104   int step; // =1 步进播放模式, =0 其他模式
105
106 #if CONFIG_AVFILTER
107   int vfilter_idx;
108   AVFilterContext *in_video_filter; // the first filter in the video chain
109   AVFilterContext *out_video_filter; // the last filter in the video chain
110   AVFilterContext *in_audio_filter; // the first filter in the audio chain
111   AVFilterContext *out_audio_filter; // the last filter in the audio chain
112   AVFilterGraph *agraph; // audio filter graph
113 #endif
114   // 保留最近的相应audio、video、subtitle流的steam index
115   int last_video_stream, last_audio_stream, last_subtitle_stream;
116
117   SDL_cond *continue_read_thread; // 当读取数据队列满了后进⼊休眠时,可以通过该condition唤醒读线程
118 } VideoState;

struct Clock 时钟封装

1 typedef struct Clock {
2     double pts; // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上⼀帧
3     // 当前pts与当前系统时钟的差值, audio、video对于该值是独⽴的
4     double pts_drift; // clock base minus time at which we updated the clock
5     // 当前时钟(如视频时钟)最后⼀次更新时间,也可称当前时钟时间
6     double last_updated; // 最后⼀次更新的系统时钟
7     double speed; // 时钟速度控制,⽤于控制播放速度
8     // 播放序列,所谓播放序列就是⼀段连续的播放动作,⼀个seek操作会启动⼀段新的播放序列
9     int serial; // clock is based on a packet with this serial
10    int paused; // = 1 说明是暂停状态
11    // 指向packet_serial
12    int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
13 } Clock;

struct MyAVPacketList和PacketQueue队列

ffplay⽤PacketQueue保存解封装后的数据,即保存AVPacket。
ffplay⾸先定义了⼀个结构体 MyAVPacketList :

1 typedef struct MyAVPacketList {
2     AVPacket pkt; //解封装后的数据
3     struct MyAVPacketList *next; //下⼀个节点
4     int serial; //播放序列
5 } MyAVPacketList;

可以理解为是队列的⼀个节点。可以通过其 next 字段访问下⼀个节点。

serial字段主要⽤于标记当前节点的播放序列号,ffplay中多处⽤到serial的概念,主要⽤来区分是否连续数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。serial字段在我们ffplay的分析中应⽤⾮常⼴泛,谨记他是⽤来区分数据否连续先。

接着定义另⼀个结构体PacketQueue:

1 typedef struct PacketQueue {
2     MyAVPacketList *first_pkt, *last_pkt; // 队⾸,队尾指针
3     int nb_packets; // 包数量,也就是队列元素数量
4     int size; // 队列所有元素的数据⼤⼩总和
5     int64_t duration; // 队列所有元素的数据播放持续时间
6     int abort_request; // ⽤户退出请求标志
7     int serial; // 播放序列号,和MyAVPacketList的serial作⽤相同
8     SDL_mutex *mutex; // ⽤于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)
9     SDL_cond *cond; // ⽤于读、写线程相互通知(SDL_cond可以按pthread_cond_t理解)
10 } PacketQueue;

该结构体内定义了“队列”⾃身的属性。上⾯的注释对每个字段作了简单的介绍,这⾥也看到了serial字段,MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。

⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。

接下来我们也从队列的操作函数具体分析各个字段的含义。

PacketQueue 操作提供以下⽅法:

packet_queue_init:初始化
packet_queue_destroy:销毁
packet_queue_start:启⽤
packet_queue_abort:中⽌
packet_queue_get:获取⼀个节点
packet_queue_put:存⼊⼀个节点
packet_queue_put_nullpacket:存⼊⼀个空节点
packet_queue_flush:清除队列内所有的节点
packet_queue_init()

初始化⽤于初始各个字段的值,并创建mutex和cond:

1 /* packet queue handling */
2 static int packet_queue_init(PacketQueue *q)
3 {
4     memset(q, 0, sizeof(PacketQueue));
5     q->mutex = SDL_CreateMutex();
6     if (!q->mutex) {
7         av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
8         return AVERROR(ENOMEM);
9     }
10     q->cond = SDL_CreateCond();
11     if (!q->cond) {
12           av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
13           return AVERROR(ENOMEM);
14     }
15     q->abort_request = 1; // 在packet_queue_start和packet_queue_abort时修改到该值
16     return 0;
17 }
packet_queue_destroy()

相应的,packet_queue_destroy()销毁过程负责清理mutex和cond:

1 static void packet_queue_destroy(PacketQueue *q)
2 {
3     packet_queue_flush(q); //先清除所有的节点
4     SDL_DestroyMutex(q->mutex);
5     SDL_DestroyCond(q->cond);
6 }
packet_queue_start()

启动队列

1 static void packet_queue_start(PacketQueue *q)
2 {
3       SDL_LockMutex(q->mutex);
4       q->abort_request = 0;
5       packet_queue_put_private(q, &flush_pkt); //这⾥放⼊了⼀个flush_pkt,问题:⽬的是什么
6       SDL_UnlockMutex(q->mutex);
7 }

flush_pkt定义是 static AVPacket flush_pkt; ,是⼀个特殊的packet,主要⽤来作为⾮连续的两端数据的“分界”标记:

  • 插⼊ flush_pkt 触发PacketQueue其对应的serial,加1操作
  • 触发解码器清空⾃身缓存 avcodec_flush_buffers(),以备新序列的数据进⾏新解码
packet_queue_abort()

中⽌队列:

1 static void packet_queue_abort(PacketQueue *q)
2 {
3       SDL_LockMutex(q->mutex);
4
5       q->abort_request = 1; // 请求退出
6
7       SDL_CondSignal(q->cond); //释放⼀个条件信号
8
9       SDL_UnlockMutex(q->mutex);
10 }

这⾥SDL_CondSignal的作⽤在于确保当前等待该条件的线程能被激活并继续执⾏退出流程,并唤醒者会检测abort_request标志确定⾃⼰的退出流程。

packet_queue_put()

读、写是PacketQueue的主要⽅法。

先看写——往队列中放⼊⼀个节点:

1 static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
2 {
3       int ret;
4
5       SDL_LockMutex(q->mutex);
6       ret = packet_queue_put_private(q, pkt);//主要实现
7       SDL_UnlockMutex(q->mutex);
8
9       if (pkt != &flush_pkt && ret < 0)
10           av_packet_unref(pkt); //放⼊失败,释放AVPacket
11
12     return ret;
13 }

主要实现在函数 packet_queue_put_private ,这⾥需要注意的是如果插⼊失败,则需要释放AVPacket。

我们再分析packet_queue_put_private:

1 static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
2 {
3     MyAVPacketList *pkt1;
4
5     if (q->abort_request) //如果已中⽌,则放⼊失败
6         return -1;
7
8     pkt1 = av_malloc(sizeof(MyAVPacketList)); //分配节点内存
9     if (!pkt1) //内存不⾜,则放⼊失败
10         return -1;
11     // 没有做引⽤计数,那这⾥也说明av_read_frame不会释放替⽤户释放buffer。
12     pkt1->pkt = *pkt; //拷⻉AVPacket(浅拷⻉,AVPacket.data等内存并没有拷⻉)
13     pkt1->next = NULL;
14     if (pkt == &flush_pkt)//如果放⼊的是flush_pkt,需要增加队列的播放序列号,以区分不连续的两段数据
15     {
16         q->serial++;
17         printf("q->serial = %d\n", q->serial++);
18     }
19     pkt1->serial = q->serial; //⽤队列序列号标记节点
20     /* 队列操作:如果last_pkt为空,说明队列是空的,新增节点为队头;
21     * 否则,队列有数据,则让原队尾的next为新增节点。 最后将队尾指向新增节点
22     */
23     if (!q->last_pkt)
24         q->first_pkt = pkt1;
25     else
26         q->last_pkt->next = pkt1;
27     q->last_pkt = pkt1;
28
29     //队列属性操作:增加节点数、cache⼤⼩、cache总时⻓, ⽤来控制队列的⼤⼩
30     q->nb_packets++;
31     q->size += pkt1->pkt.size + sizeof(*pkt1);
32     q->duration += pkt1->pkt.duration;
33
34     /* XXX: should duplicate packet data in DV case */
35     //发出信号,表明当前队列中有数据了,通知等待中的读线程可以取数据了
36     SDL_CondSignal(q->cond);
37     return 0;
38 }

对于packet_queue_put_private主要完成3件事:

  • 计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的packet.
  • 节点⼊队列操作。
  • 队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综合去约束队列的节点的数量,具体在read_thread进⾏分析。
packet_queue_get()

从队列中取⼀个节点:

1 /**
2 * @brief packet_queue_get
3 * @param q 队列
4 * @param pkt 输出参数,即MyAVPacketList.pkt
5 * @param block 调⽤者是否需要在没节点可取的情况下阻塞等待
6 * @param serial 输出参数,即MyAVPacketList.serial
7 * @return <0: aborted; =0: no packet; >0: has packet
8 */
9 static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
10 {
11     MyAVPacketList *pkt1;
12     int ret;
13
14     SDL_LockMutex(q->mutex); // 加锁
15
16     for (;;) {
17         if (q->abort_request) {
18         ret = -1;
19         break;
20     }
21
22     pkt1 = q->first_pkt; //MyAVPacketList *pkt1; 从队头拿数据
23     if (pkt1) { //队列中有数据
24     q->first_pkt = pkt1->next; //队头移到第⼆个节点
25     if (!q->first_pkt)
26         q->last_pkt = NULL;
27         q->nb_packets--; //节点数减1
28         q->size -= pkt1->pkt.size + sizeof(*pkt1); //cache⼤⼩扣除⼀个节点
29         q->duration -= pkt1->pkt.duration; //总时⻓扣除⼀个节点
30         //返回AVPacket,这⾥发⽣⼀次AVPacket结构体拷⻉,AVPacket的data只拷⻉了指针
31         *pkt = pkt1->pkt;
32         if (serial) //如果需要输出serial,把serial输出
33             *serial = pkt1->serial;
34         av_free(pkt1); //释放节点内存,只是释放节点,⽽不是释放AVPacket
35         ret = 1;
36         break;
37       } else if (!block) { //队列中没有数据,且⾮阻塞调⽤
38          ret = 0;
39           break;
40       } else { //队列中没有数据,且阻塞调⽤
41           //这⾥没有break。for循环的另⼀个作⽤是在条件变量满⾜后重复上述代码取出节点
42           SDL_CondWait(q->cond, q->mutex);
43       }
44     }
15
45     SDL_UnlockMutex(q->mutex); // 释放锁
46     return ret;
47 }

该函数整体流程:

  • 加锁
  • 进⼊for循环,如果需要退出for循环,则break;当没有数据可读且block为1时则等待
    • ret = -1 终⽌获取packet
    • ret = 0 没有读取到packet
    • ret = 1 获取到了packet
  • 释放锁

如果有取到数据,主要分3个步骤:

  1. 队列操作:出队列操作; nb_packets相应-1; duration 的也相应减少, size也相应占⽤的字节⼤⼩(pkt1->pkt.size + sizeof(*pkt1))
  2. 给输出参数赋值:就是MyAVPacketList的成员传递给输出参数pkt和serial
  3. 释放节点内存:释放放⼊队列时申请的节点内存(注意是节点内存⽽不是AVPacket的数据的内存)
packet_queue_put_nullpacket()

放⼊“空包”(nullpacket)。放⼊空包意味着流的结束,⼀般在媒体数据读取完成的时候放⼊空包。放⼊空包,⽬的是为了冲刷解码器,将编码器⾥⾯所有frame都读取出来:

1 static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
2 {
3     AVPacket pkt1, *pkt = &pkt1;
4     av_init_packet(pkt);
5     pkt->data = NULL;
6     pkt->size = 0;
7     pkt->stream_index = stream_index;
8     return packet_queue_put(q, pkt);
9 }

⽂件数据读取完毕后刷⼊空包。

packet_queue_flush()

packet_queue_flush⽤于将packet队列中的所有节点清除,包括节点对应的AVPacket。⽐如⽤于退出播放和seek播放:

  • 退出播放,则要清空packet queue的节点
  • seek播放,要清空seek之前缓存的节点数据,以便插⼊新节点数据
1 static void packet_queue_flush(PacketQueue *q)
2 {
3     MyAVPacketList *pkt, *pkt1;
4
5     SDL_LockMutex(q->mutex);
6     for (pkt = q->first_pkt; pkt; pkt = pkt1) {
7         pkt1 = pkt->next;
8         av_packet_unref(&pkt->pkt); // 释放AVPacket的数据
9         av_freep(&pkt);
10     }
11     q->last_pkt = NULL;
12     q->first_pkt = NULL;
13     q->nb_packets = 0;
14     q->size = 0;
15     q->duration = 0;
16     SDL_UnlockMutex(q->mutex);
17 }

函数主体的for循环是队列遍历,遍历过程释放节点和AVPacket(AVpacket对应的数据也被释放掉)。最后将PacketQueue的属性恢复为空队列状态。

PacketQueue总结

前⾯我们分析了PacketQueue的实现和主要的操作⽅法,现在总结下两个关键的点:

第⼀,PacketQueue的内存管理:

MyAVPacketList的内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。

AVPacket分两块:

  • ⼀部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList共存亡的。
  • 另⼀部分是AVPacket字段指向的内存,这部分⼀般通过 av_packet_unref 函数释放。⼀般情况下,是在get后由调⽤者负责⽤ av_packet_unref 函数释放。特殊的情况是当碰到packet_queue_flush 或put失败时,这时需要队列⾃⼰处理。
第⼆,serial的变化过程:

如上图所示,左边是队头,右边是队尾,从左往右标注了4个节点的serial,以及放⼊对应节点时queue的serial。

可以看到放⼊flush_pkt的时候后,serial增加了1.

假设,现在要从队头取出⼀个节点,那么取出的节点是serial 1,⽽PacketQueue⾃身的queue已经增⻓到了2。

PacketQueue设计思路:
  1. 设计⼀个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据⼤⼩。(这个统计数据会⽤来后续设置要缓存的数据量)
  2. 引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
  3. 设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊flush事件、放⼊null事件),我们在⾳频输出、视频输出、播放控制等模块时也会继续对flush_pkt和nullpkt的作⽤展开分析。

struct Frame 和 FrameQueue队列

1 Frame
1 typedef struct Frame {
2     AVFrame *frame; // 指向数据帧
3     AVSubtitle sub; // ⽤于字幕
4     int serial; // 播放序列,在seek的操作时serial会变化
5     double pts; // 时间戳,单位为秒
6     double duration; // 该帧持续时间,单位为秒
7     int64_t pos; // 该帧在输⼊⽂件中的字节位置
8     int width; // 图像宽度
9     int height; // 图像⾼读
10    int format; // 对于图像为(enum AVPixelFormat),
11                // 对于声⾳则为(enum AVSampleFormat)
12    AVRational sar; // 图像的宽⾼⽐,如果未知或未指定则为0/1
13    int uploaded; // ⽤来记录该帧是否已经显示过?
14    int flip_v; // =1则旋转180, = 0则正常播放
15 } Frame;

真正存储解码后⾳视频数据的结构体为AVFrame ,存储字幕则使⽤AVSubtitle,该Frame的设计是为了⾳频、视频、字幕帧通⽤,所以Frame结构体的设计类似AVFrame,部分成员变量只对不同类型有作⽤,⽐如sar只对视频有作⽤。

⾥⾯也包含了serial播放序列(每次seek时都切换serial),sar(图像的宽⾼⽐(16:9,4:3...),该值来⾃AVFrame结构体的sample_aspect_ratio变量)。

2 FrameQueue
1 typedef struct FrameQueue {
2     Frame queue[FRAME_QUEUE_SIZE]; // FRAME_QUEUE_SIZE 最⼤size, 数字太⼤时会占⽤⼤量的内存,需要注意该值的设置
3     int rindex; // 读索引。待播放时读取此帧进⾏播放,播放后此帧成为上⼀帧
4     int windex; // 写索引
5     int size; // 当前总帧数
6     int max_size; // 可存储最⼤帧数
7     int keep_last; // = 1说明要在队列⾥⾯保持最后⼀帧的数据不释放,只在销毁队列的时候才将其真正释放
8     int rindex_shown; // 初始化为0,配合keep_last=1使⽤
9     SDL_mutex *mutex; // 互斥量
10    SDL_cond *cond; // 条件变量
11    PacketQueue *pktq; // 数据包缓冲队列
12 } FrameQueue;

FrameQueue是⼀个环形缓冲区(ring buffer),是⽤数组实现的⼀个FIFO。数组⽅式的环形缓冲区适合于事先明确了缓冲区的最⼤容量的情形。

ffplay中创建了三个frame_queue:⾳频frame_queue,视频frame_queue,字幕frame_queue。每⼀个frame_queue⼀个写端⼀个读端,写端位于解码线程,读端位于播放线程。

FrameQueue的设计⽐如PacketQueue复杂,引⼊了读取节点但节点不出队列的操作、读取下⼀节点也不出队列等等的操作,FrameQueue操作提供以下⽅法:

frame_queue_unref_item:释放Frame⾥⾯的AVFrame和 AVSubtitle
frame_queue_init:初始化队列
frame_queue_destory:销毁队列
frame_queue_signal:发送唤醒信号
frame_queue_peek:获取当前Frame,调⽤之前先调⽤frame_queue_nb_remaining确保有frame可读
frame_queue_peek_next:获取当前Frame的下⼀Frame,调⽤之前先调⽤
frame_queue_nb_remaining确保⾄少有2 Frame在队列
frame_queue_peek_last:获取上⼀Frame
frame_queue_peek_writable:获取⼀个可写Frame,可以以阻塞或⾮阻塞⽅式进⾏
frame_queue_peek_readable:获取⼀个可读Frame,可以以阻塞或⾮阻塞⽅式进⾏
frame_queue_push:更新写索引,此时Frame才真正⼊队列,队列节点Frame个数加1
frame_queue_next:更新读索引,此时Frame才真正出队列,队列节点Frame个数减1,内部调⽤
frame_queue_unref_item是否对应的AVFrame和AVSubtitle
frame_queue_nb_remaining:获取队列Frame节点个数
frame_queue_last_pos:获取最近播放Frame对应数据在媒体⽂件的位置,主要在seek时使⽤
frame_queue_init() 初始化
1 static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
2 {
3     int i;
4     memset(f, 0, sizeof(FrameQueue));
5     if (!(f->mutex = SDL_CreateMutex())) {
6         av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
7         return AVERROR(ENOMEM);
8     }
9     if (!(f->cond = SDL_CreateCond())) {
10         av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
11         return AVERROR(ENOMEM);
12     }
13     f->pktq = pktq;
14     f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
15     f->keep_last = !!keep_last;
16     for (i = 0; i < f->max_size; i++)
17         if (!(f->queue[i].frame = av_frame_alloc())) // 分配AVFrame结构体
18             return AVERROR(ENOMEM);
19     return 0;
20 }

队列初始化函数确定了队列⼤⼩,将为队列中每⼀个节点的frame( f->queue[i].frame )分配内存,注意只是分配Frame对象本身,⽽不关注Frame中的数据缓冲区。Frame中的数据缓冲区是AVBuffer,使⽤引⽤计数机制。

f->max_size 是队列的⼤⼩,此处值为16(由FRAME_QUEUE_SIZE定义),实际分配的时候视频为3,⾳频为9,字幕为16,因为这⾥存储的是解码后的数据,不宜设置过⼤,⽐如视频当为1080p时,如果为YUV420p格式,⼀帧就有3110400字节。

#define VIDEO_PICTURE_QUEUE_SIZE 3 // 图像帧缓存数量
#define SUBPICTURE_QUEUE_SIZE 16 // 字幕帧缓存数量
#define SAMPLE_QUEUE_SIZE 9 // 采样帧缓存数量
#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE,FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))

f->keep_last 是队列中是否保留最后⼀次播放的帧的标志。 f->keep_last =!!keep_last 是将int取值的keep_last转换为boot取值(0或1)。

frame_queue_destory()销毁
1 static void frame_queue_destory(FrameQueue *f)
2 {
3     int i;
4     for (i = 0; i < f->max_size; i++) {
5         Frame *vp = &f->queue[i];
6         // 释放对vp->frame中的数据缓冲区的引⽤,注意不是释放frame对象本身
7         frame_queue_unref_item(vp);
8         // 释放vp->frame对象
9         av_frame_free(&vp->frame);
10     }
11     SDL_DestroyMutex(f->mutex);
12     SDL_DestroyCond(f->cond);
13 }

队列销毁函数对队列中的每个节点作了如下处理:

  1. frame_queue_unref_item(vp) 释放本队列对vp->frame中AVBuffer的引⽤
  2. av_frame_free(&vp->frame) 释放vp->frame对象本身
frame_queue_peek_writable()获取可写Frame
frame_queue_push()⼊队列

FrameQueue写队列的步骤和PacketQueue不同,分了3步进⾏:

  1. 调⽤frame_queue_peek_writable获取可写的Frame,如果队列已满则等待
  2. 获取到Frame后,设置Frame的成员变量
  3. 再调⽤frame_queue_push更新队列的写索引,真正将Frame⼊队列
1 Frame *frame_queue_peek_writable(FrameQueue *f); // 获取可写帧
2 void frame_queue_push(FrameQueue *f); // 更新写索引

通过实例看⼀下写队列的⽤法:

1 // video AVFrame ⼊队列
2 int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
3 {
4       Frame *vp;
5
6       if (!(vp = frame_queue_peek_writable(&is->pictq))) // 检测队列是否有可写空间
7           return -1; // Frame队列满了则返回-1
8       // 执⾏到这步说已经获取到了可写⼊的Frame
9       vp->sar = src_frame->sample_aspect_ratio;
10      vp->uploaded = 0;
11
12      vp->width = src_frame->width;
13      vp->height = src_frame->height;
14      vp->format = src_frame->format;
15
16      vp->pts = pts;
17      vp->duration = duration;
18      vp->pos = pos;
19      vp->serial = serial;
20
21      set_default_window_size(vp->width, vp->height, vp->sar);
22
23      av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据拷⻉到dst中,并复位src。
24      frame_queue_push(&is->pictq); // 更新写索引位置
25      return 0;
26 }

上⾯⼀段代码是视频解码线程向视频frame_queue中写⼊⼀帧的代码,步骤如下:

  1. frame_queue_peek_writable(&is->pictq) 向队列尾部申请⼀个可写的帧空间,若队列已满⽆空间可写,则等待(由SDL_cond *cond控制,由frame_queue_next或frame_queue_signal触发唤醒)
  2. av_frame_move_ref(vp->frame, src_frame) 将src_frame中所有数据拷⻉到vp->frame并复位src_frame,vp->frame中AVBuffer使⽤引⽤计数机制,不会执⾏AVBuffer的拷⻉动作,仅是修改指针指向值。为避免内存泄漏,在 av_frame_move_ref(dst, src) 之前应先调⽤ av_frame_unref(dst) ,这⾥没有调⽤,是因为frame_queue在删除⼀个节点时,已经释放了frame及frame中的AVBuffer。
  3. frame_queue_push(&is->pictq) 此步仅将frame_queue中的写索引加1,实际的数据写⼊在此步之前已经完成。
frame_queue_peek_writable获取可写Frame指针
1 // 获取可写指针
2 static Frame *frame_queue_peek_writable(FrameQueue *f)
3 {
4     /* wait until we have space to put a new frame */
5     SDL_LockMutex(f->mutex);
6     while (f->size >= f->max_size &&
7         !f->pktq->abort_request) { /* 检查是否需要退出 */
8         SDL_CondWait(f->cond, f->mutex);
9     }
10    SDL_UnlockMutex(f->mutex);
11
12    if (f->pktq->abort_request) /* 检查是不是要退出 */
13       return NULL;
14
15    return &f->queue[f->windex];
16 }

向队列尾部申请⼀个可写的帧空间,若⽆空间可写,则等待。

这⾥最需要体会到的是abort_request的使⽤,在等待时如果播放器需要退出则将abort_request = 1,那frame_queue_peek_writable函数可以知道是正常frame可写唤醒,还是其他唤醒。

frame_queue_push()
1 // 更新写索引
2 static void frame_queue_push(FrameQueue *f)
3 {
4     if (++f->windex == f->max_size)
5         f->windex = 0;
6     SDL_LockMutex(f->mutex);
7     f->size++;
8     SDL_CondSignal(f->cond); // 当frame_queue_peek_readable在等待时则可以唤醒
9     SDL_UnlockMutex(f->mutex);
10 }

向队列尾部压⼊⼀帧,只更新计数与写指针,因此调⽤此函数前应将帧数据写⼊队列相应位置。SDL_CondSignal(f->cond);可以唤醒读frame_queue_peek_readable。

frame_queue_peek_readable() 获取可读Frame
frame_queue_next()出队列

写队列中,应⽤程序写⼊⼀个新帧后通常总是将写索引加1。⽽读队列中,“读取”和“更新读索引(同时删除旧帧)”⼆者是独⽴的,可以只读取⽽不更新读索引,也可以只更新读索引(只删除)⽽不读取(只有更新读索引的时候才真正释放对应的Frame数据)。⽽且读队列引⼊了是否保留已显示的最后⼀帧的机制,导致读队列⽐写队列要复杂很多。

读队列和写队列步骤是类似的,基本步骤如下:

  1. 调⽤frame_queue_peek_readable获取可读Frame;
  2. 如果需要更新读索引(出队列该节点)则调⽤frame_queue_peek_next;

读队列涉及如下函数:

1 Frame *frame_queue_peek_readable(FrameQueue *f); // 获取可读Frame指针(若读空则等待)
2 Frame *frame_queue_peek(FrameQueue *f); // 获取当前Frame指针
3 Frame *frame_queue_peek_next(FrameQueue *f); // 获取下⼀Frame指针
4 Frame *frame_queue_peek_last(FrameQueue *f); // 获取上⼀Frame指针
5 void frame_queue_next(FrameQueue *f); // 更新读索引(同时删除旧frame)

通过实例看⼀下读队列的⽤法:

1 static void video_refresh(void *opaque, double *remaining_time)
2 {
3     ......
4     if (frame_queue_nb_remaining(&is->pictq) == 0) { // 所有帧已显示
5         // nothing to do, no picture to display in the queue
6     } else {
7         Frame *vp, *lastvp;
8         lastvp = frame_queue_peek_last(&is->pictq); // 上⼀帧:上次已显示的帧
9         vp = frame_queue_peek(&is->pictq); // 当前帧:当前待显示的帧
10        frame_queue_next(&is->pictq); // 出队列,并更新rindex
11        video_display(is)-->video_image_display()-->frame_queue_peek_last();
12     }
13     ......
14 }

上⾯⼀段代码是视频播放线程从视频frame_queue中读取视频帧进⾏显示的基本步骤,其他代码已省略,只保留了读队列部分。记lastvp为上⼀次已播放的帧,vp为本次待播放的帧,下图中⽅框中的数字表示显示序列中帧的序号:

在启⽤keep_last机制后,rindex_shown值总是为1,rindex_shown确保了最后播放的⼀帧总保留在队列中。

假设某次进⼊ video_refresh() 的时刻为T0,下次进⼊的时刻为T1。在T0时刻,读队列的步骤如下:

  1. rindex表示上⼀次播放的帧lastvp,本次调⽤ video_refresh() 中,lastvp会被删除,rindex会加1,即是当调⽤frame_queue_next删除的是lastvp,⽽不是当前的vp,当前的vp转为lastvp。
  2. rindex+rindex_shown表示本次待播放的帧vp,本次调⽤ video_refresh() 中,vp会被读出播放图中已播放的帧是灰⾊⽅框,本次待播放的帧是红⾊⽅框,其他未播放的帧是绿⾊⽅框,队列中空位置为⽩⾊⽅框。
  3. rindex+rindex_shown+1表示下⼀帧nextvp
frame_queue_nb_remaining()获取队列的size
1 /* return the number of undisplayed frames in the queue */
2 static int frame_queue_nb_remaining(FrameQueue *f)
3 {
4     return f->size - f->rindex_shown;
5 }

rindex_shown为1时,队列中总是保留了最后⼀帧lastvp(灰⾊⽅框)。需要注意的时候rindex_shown的值就是0或1,不存在变为2,3等的可能。在计算队列当前Frame数量是不包含lastvp。

rindex_shown的引⼊增加了读队列操作的理解难度。⼤多数读操作函数都会⽤到这个变量。

通过 FrameQueue.keep_last 和 FrameQueue.rindex_shown 两个变量实现了保留最后⼀次播放帧的机制。

是否启⽤keep_last机制是由全局变量 keep_last 值决定的,在队列初始化函数frame_queue_init() 中有 f->keep_last = !!keep_last; ,⽽在更新读指针函数frame_queue_next() 中如果启⽤keep_last机制,则 f->rindex_shown 值为1。

我们具体分析下 frame_queue_next() 函数:

1 /* 释放当前frame,并更新读索引rindex,
2 * 当keep_last为1, rindex_show为0时不去更新rindex,也不释放当前frame */
3 static void frame_queue_next(FrameQueue *f)
4 {
5     if (f->keep_last && !f->rindex_shown) {
6         f->rindex_shown = 1; // 第⼀次调⽤置为1
7         return;
8     }
9     frame_queue_unref_item(&f->queue[f->rindex]);
10    if (++f->rindex == f->max_size)
11    f->rindex = 0;
12    SDL_LockMutex(f->mutex);
13    f->size--;
14    SDL_CondSignal(f->cond);
15    SDL_UnlockMutex(f->mutex);
16 }

主要步骤:

  • 在启⽤keeplast时,如果rindex_shown为0则将其设置为1,并返回。此时并不会更新读索引。也就是说keeplast机制实质上也会占⽤着队列Frame的size,当调⽤frame_queue_nb_remaining()获取size时并不能将其计算⼊size;
  • 释放Frame对应的数据(⽐如AVFrame的数据),但不释放Frame本身
  • 更新读索引
  • 释放唤醒信号,以唤醒正在等待写⼊的线程。

frame_queue_peek_readable()的具体实现

1 static Frame *frame_queue_peek_readable(FrameQueue *f)
2 {
3     /* wait until we have a readable a new frame */
4     SDL_LockMutex(f->mutex);
5     while (f->size - f->rindex_shown <= 0 &&
6            !f->pktq->abort_request) {
7         SDL_CondWait(f->cond, f->mutex);
8     }
9     SDL_UnlockMutex(f->mutex);
10
11    if (f->pktq->abort_request)
12    return NULL;
13
14    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
15 }

从队列头部读取⼀帧(vp),只读取不删除,若⽆帧可读则等待。这个函数和 frame_queue_peek() 的区别仅仅是多了不可读时等待的操作。

frame_queue_peek()获取当前帧
1 /* 获取队列当前Frame, 在调⽤该函数前先调⽤frame_queue_nb_remaining确保有frame可读 */
2 static Frame *frame_queue_peek(FrameQueue *f)
3 {
4     return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
5 }
frame_queue_peek_next()获取下⼀帧
1 /* 获取当前Frame的下⼀Frame, 此时要确保queue⾥⾯⾄少有2个Frame */
2 static Frame *frame_queue_peek_next(FrameQueue *f)
3 {
4     return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
5 }
frame_queue_peek_last()获取上⼀帧
1 /* 获取last Frame:
2 * 当rindex_shown=0时,和frame_queue_peek效果⼀样
3 * 当rindex_shown=1时,读取的是已经显示过的frame
4 */
5 static Frame *frame_queue_peek_last(FrameQueue *f)
6 {
7     return &f->queue[f->rindex];
8 }
struct AudioParams ⾳频参数
1 typedef struct AudioParams {
2     int freq; // 采样率
3     int channels; // 通道数
4     int64_t channel_layout; // 通道布局,⽐如2.1声道,5.1声道等
5     enum AVSampleFormat fmt; // ⾳频采样格式,⽐如AV_SAMPLE_FMT_S16表示为有符号16bit深度,交错排列模式。
6     int frame_size; // ⼀个采样单元占⽤的字节数(⽐如2通道时,则左右通道各采样⼀次合成⼀个采样单元)
7     int bytes_per_sec; // ⼀秒时间的字节数,⽐如采样率48Khz,2channel,16bit,则⼀秒48000*2*16/8=192000
8 } AudioParams;
struct Decoder解码器封装
1 /**
2 * 解码器封装
3 */
4 typedef struct Decoder {
5     AVPacket pkt;
6     PacketQueue *queue; // 数据包队列
7     AVCodecContext *avctx; // 解码器上下⽂
8     int pkt_serial; // 包序列
9     int finished; // =0,解码器处于⼯作状态;=⾮0,解码器处于空闲状态
10    int packet_pending; // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状态
11    SDL_cond *empty_queue_cond; // 检查到packet队列空时发送 signal缓存read_thread读取数据
12    int64_t start_pts; // 初始化时是stream的start time
13    AVRational start_pts_tb; // 初始化时是stream的time_base
14    int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts时则使⽤next_pts进⾏推算
15    AVRational next_pts_tb; // next_pts的单位
16    SDL_Thread *decoder_tid; // 线程句柄
17 } Decoder;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容