前言
FFplay是 FFmpeg 官方提供的一个播放器的实现,支持快进快退,逐帧播放,滤镜等。本文是在VS2017的Debug x86模式下调试运行的,ffplay.c移植到VS2017工程可以参考
ffplay移植vs2017
https://mp.csdn.net/mp_blog/creation/editor/143884981
stream_open函数主要是做了些初始化工作,并创建了读文件线程read_thread。先分析函数中出现的几个结构体。
一、数据结构
1、VideoState结构体
是ffplay的全局管理器,记录各种数据状态(时钟,队列,解码器等)。先看VideoState的部分字段
1)read_tid :read_thread线程ID,由SDL_CreateThread()返回
2) AVInputFormat *iformat:媒体文件容器
3)Clock audclk:音频时钟,Clock vidclk:视频时钟,Clock extclk:外部时钟
4)FrameQueue pictq:视频帧队列,存放解码后的视频数据
FrameQueue subpq:字幕帧队列,存放解码后的字幕数据
FrameQueue sampq:音频帧队列,存放解码后的音频数据
5)Decoder auddec:音频解码器,存放音频解码器相关的参数
Decoder viddec:视频解码器
Decoder subdec:字幕解码器
6)av_sync_type:时钟同步方式,默认以音频时钟为主
7)last_video_stream:代表最后一个视频流,另外两个 last_audio_stream, last_subtitle_stream一样
8)char* filename:音视频文件名或者网络地址url。
9)width, height, xleft, ytop:分别代表SDL播放器窗口的宽,高,左偏移和上偏移。
10)PacketQueue videoq:视频AVPacket 队列
PacketQueue audioq:音频AVPacket 队列
PacketQueue subtitleq:字幕AVPacket 队列
11)continue_read_thread:条件变量,当解码队列为空或视频跳转时发送信号给read_thread,通知read_thread立即读取数据
2、MyAVPacketList结构体
typedef struct MyAVPacketList {
AVPacket *pkt;
int serial;
} MyAVPacketList;
MyAVPacketList结构体包含了AVPacket *pkt和 int serial两个字段,增加serial字段是为了给AVPacket添加序列号标识,跳转的时候serial字段会变。
3、PacketQueue结构体
typedef struct PacketQueue {
AVFifoBuffer *pkt_list; //ffmpeg实现的先进先出的队列
int nb_packets; // 队列中AVPacket数量
int size; //队列占用内存大小
int64_t duration; // 队列所有数据包的持续时间
int abort_request; //是否结束
int serial; //队列序列号
SDL_mutex *mutex; //队列锁
SDL_cond *cond;//队列条件变量
} PacketQueue;
PacketQueue 是解码前的 Packet 队列,用于保存解封装后的数据。初始化 PacketQueue 时,会对 VideoState 中的 videoq(视频包队列)、audio(音频包队列)、subtitleq(字幕包队列)依次调用packet_queue_init函数进行初始化。不同于 FrameQueue , PacketQueue 采用链表的方式实现队列。由于解码前的包大小不可控,无法明确缓冲区的最大容量,如果使用环形缓冲区,容易触发缓冲区扩容,需要移动缓冲区内的数据。因此,使用链表实现队列更加合适。其中AVFifoBuffer是ffmpeg实现的一个先进先出的队列,队列中存放的每个元素是MyAVPacketList结构体。
4、Frame结构体
/* Common struct for handling all types of decoded data and allocated render buffers. */
typedef struct Frame {
AVFrame *frame; //AVFrame的指针
AVSubtitle sub; //字幕指针
int serial; //Frame的序列号
double pts; //显示时间戳 /* presentation timestamp for the frame */
double duration;//AVFrame的持续时长 /* estimated duration of the frame */
int64_t pos; //在文件的字节位置 /* byte position of the frame in the input file */
int width;//视频宽度
int height;//视频高度
int format;//AVFrame格式
AVRational sar;//缩放比
int uploaded;//是否上传到纹理
int flip_v;
} Frame;
对AVFrame的又一次封装,补充AVFrame的相关信息(比如序列号,宽高,显示时刻等)
5、FrameQueue结构体
typedef struct FrameQueue {
Frame queue[FRAME_QUEUE_SIZE]; //Frame数组
int rindex;//队列读索引
int windex;//队列写索引
int size;//队列大小
int max_size;//队列最大值
int keep_last;//是否保留最后一帧,只有视频保留最后一帧
int rindex_shown;//配合keep_last用
SDL_mutex *mutex;//队列锁
SDL_cond *cond;//队列条件变量
PacketQueue *pktq;//对应的PacketQueue
} FrameQueue;
FrameQueue 是解码后的 Frame 队列, Frame 是解码后的数据,例如视频解码后是 YUV 或 RGB 数据,音频解码后是 PCM 数据。初始化 FrameQueue 时,会对 VideoState 中的 pictq(视频帧队列)、subpq(字幕帧队列)、sampq(音频帧队列)依次调用frame_queue_init()函数进行初始化。FrameQueue 内部是通过数组实现了一个先进先出的环形缓冲区,windex 是写指针,被解码线程使用;rindex 是读指针,被播放线程使用。使用环形缓冲区的好处是,缓冲区内的元素被移除后,其它元素不需要移动位置,适用于事先知道缓冲区最大容量的场景。
6、Clock结构体
typedef struct Clock {
double pts; //时钟校时的时刻 /* clock base */
double pts_drift; //时钟的偏移值 /* clock base minus time at which we updated the clock */
double last_updated;//最近一次更新的系统时间
double speed;//播放速度
int serial;//时钟序列号 /* clock is based on a packet with this serial */
int paused;//是否暂停状态
int *queue_serial; //指向对应PacketQueue的序列号 /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
Clock 是时钟,在音视频同步阶段,有三种同步方法:视频同步到音频,音频同步到视频,以及音频和视频同步到外部时钟。ffplay默认采用视频同步到音频的策略。初始化 Clock 时,会对 VideoState 中的 vidclk(视频时钟)、audclk(音频时钟)、extclk(外部时钟)依次调用init_clock函数进行初始化。
pts_drift是为了计算时钟校时后,再次读取时钟时,经过了多长时间用的。
二、流程图

三、源码分析
1、VideoState初始化
VideoState *is;
is = av_mallocz(sizeof(VideoState));
if (!is)
return NULL;
is->last_video_stream = is->video_stream = -1;
is->last_audio_stream = is->audio_stream = -1;
is->last_subtitle_stream = is->subtitle_stream = -1;
is->filename = av_strdup(filename);
if (!is->filename)
goto fail;
is->iformat = iformat;
is->ytop = 0;
is->xleft = 0;
VideoState 是 ffplay 中最大的结构体,所有的视频信息都定义在其中。初始化 VideoState 时,先定义 VideoState 结构体指针类型的局部变量is,分配堆内存。然后初始化结构体中的变量,例如视频流、音频流、字幕流的索引ID初始为-1,初始化了filename及媒体容器,以及初始化SDL窗口的显示位置。
av_mallocz函数除了申请内存外,还把申请的内存置为零。
2、FrameQueue初始化
调用frame_queue_init函数初始化音频,视频,字幕队列
/* start video display 第四个参数是keep_last*/
if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
goto fail;
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
goto fail;
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
goto fail;
第一个参数类型是FrameQueue,第二个参数类型是PacketQueue,第三个参数是对应FrameQueue中Frame数组的大小(对于视频是3,对于字幕是16,音频是9),第四个参数代表是否保留最后一帧,只有视频FrameQueue才保留最后一帧。
frame_queue_init函数内部代码如下:
static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
int i;
memset(f, 0, sizeof(FrameQueue));
if (!(f->mutex = SDL_CreateMutex())) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
if (!(f->cond = SDL_CreateCond())) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
f->pktq = pktq;
f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
f->keep_last = !!keep_last;
for (i = 0; i < f->max_size; i++)
if (!(f->queue[i].frame = av_frame_alloc()))
return AVERROR(ENOMEM);
return 0;
}
frame_queue_init函数创建了锁和条件变量,同时绑定对应的PacketQueue,并对Frame数组做了初始化。初始化时,只初始化了参数对应max_size大小的数组空间。
f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
还有keep_last两次取反,把非零值转成1代表true,因为C语言没有布尔型。
f->keep_last = !!keep_last;
3、PacketQueue初始化
packet_queue_init函数内部,通过av_fifo_alloc创建了AVFifoBuffer,AVFifoBuffer是FFmpeg 项目中的 fifo 的实现 ,fifo 的全称是 first in first out (先进先出),这是一个环形的buffer内存管理器,可以认为是一个环形队列,而且能动态的扩展容量。每一个元素是一个MyAVPacketList结构体元素。
初始化了锁和条件变量。
q->abort_request = 1先初始化为1,后面会初始化为0。abort_request代表是否中止请求。
4、创建条件变量continue_read_thread
if (!(is->continue_read_thread = SDL_CreateCond())) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
goto fail;
}
continue_read_thread在解码线程中会用到,当解码时PacketQueue无数据可解码了,会发送信号给read_thread线程立即解码。另外跳转时也会用到,也是通知read_thread线程立即读取数据以便跳转。
5、初始化时钟
调用init_clock函数初始化三个时钟
init_clock(&is->vidclk, &is->videoq.serial);
init_clock(&is->audclk, &is->audioq.serial);
init_clock(&is->extclk, &is->extclk.serial);
is->audio_clock_serial = -1;
static void init_clock(Clock *c, int *queue_serial)
{
c->speed = 1.0;
c->paused = 0;
c->queue_serial = queue_serial;
set_clock(c, NAN, -1);
}
在init_clock的内部函数中,时钟的queue_serial是个指针,指向PacketQueue的serial字段。
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time); //初始时 pts为NAN,serial为-1
}
在set_clock内部函数中,av_gettime_relative()函数是个相对时间,是从开机到现在的微秒数(1微妙=100 0000秒),所以time变量的单位是秒,set_clock又调用了set_clock_at函数,
//初始时 pts为NAN serial为-1
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
c->pts = pts;
c->last_updated = time;
c->pts_drift = c->pts - time;
c->serial = serial;
}
在set_clock_at()的内部函数中,此时pts初始为NAN,last_update初始为系统时间,参数serial初始为-1,queue_serial指向对应的PacketQueue的serial字段。
6、初始化音频相关
if (startup_volume < 0)
av_log(NULL, AV_LOG_WARNING, "-volume=%d < 0, setting to 0\n", startup_volume);
if (startup_volume > 100)
av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100\n", startup_volume);
startup_volume = av_clip(startup_volume, 0, 100);
startup_volume = av_clip(SDL_MIX_MAXVOLUME * startup_volume / 100, 0, SDL_MIX_MAXVOLUME);
is->audio_volume = startup_volume;
先限制音量范围在 0~100 之间,然后再根据 SDL 的音量范围作进一步限制。
startup_volume是个全局静态变量,初始值为100,通过第一次av_clip剪裁后是100,第二次调用av_clip函数后是128,然后赋值给is->audio_volume字段。av_clip(a,b,c)函数的作用是,给定数值区间[b,c],如果a在区间左边,返回最小值b,如果a在区间右边,返回最大值c,如果a在区间中,直接返回a。
is->muted = 0;
is->muted = 0; 是否混音初始为0
is->av_sync_type = av_sync_type;
av_sync_type是静态全局变量,代表时钟同步方式,默认是以音频时钟为主,其他时钟都向音频时钟同步
7、创建read_thread线程
is->read_tid = SDL_CreateThread(read_thread, "read_thread", is);
if (!is->read_tid) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
fail:
stream_close(is);
return NULL;
}
通过SDL_CreateThread创建read_thread线程,并返回线程ID。如果线程ID返回0,代表创建线程失败,并用stream_close函数关闭read_thread线程。read_thread线程详见链接:
8、返回值
将局部变量is作为函数返回值返回,用于处理各种 SDL 事件。

1011

被折叠的 条评论
为什么被折叠?



