ffplay研究分析意义
ffplay.c是FFmpeg源码⾃带的播放器,调⽤FFmpeg和SDL API实现⼀个⾮常有⽤的播放器。 例如哔哩哔哩著名开源项⽬ijkplayer也是基于ffplay.c进⾏⼆次开发。
ffplay实现了播放器的主体功能,掌握其原理对于我们独⽴开发播放器⾮常有帮助。
FFplay框架分析
内容涉及:
1. 队列设计与管理
- Packet队列设计
- 线程安全(支持互斥、等待、唤醒)
- 缓存数据大小
- 缓存包数
- 队列播放可持续时间
- 进队列/出队列操作
- Frame队列设计
- 线程安全(支持互斥、等待、唤醒)
- 缓存帧数
- 支持读取数据而不移除队列中的元素
- 进队列/出队列操作
2. 音视频同步
- 音频同步
- 视频同步
- 外部时钟同步
3. 音频处理
- 音量调节
- 静音
- 重采样
4. 视频处理
- 图像格式转换(如YUV到RGB)
- 图像缩放(如从1280*720到800*480)
5. 播放器控制
- 播放
- 暂停
- 停止
- 快进/快退
- 逐帧播放
- 静音
数据结构分析
VideoState 播放器封装
typedef struct VideoState {
SDL_Thread *read_tid; // 读线程句柄
AVInputFormat *iformat; // 输入指定格式 指向demuxer
int abort_request; // =1时请求退出播放
int force_refresh; // =1时需要刷新画⾯,请求⽴即刷新画⾯的意思
int paused; // =1时暂停,=0时播放
int last_paused; // 暂存“暂停”/“播放”状态
int queue_attachments_req;
int seek_req; // 标识1次seek请求
int seek_flags; // seek标志,诸如AVSEEK_FLAG_BYTE等
int64_t seek_pos; // 请求seek的目标位置(当前位置+增量)
int64_t seek_rel; // 本次seek的位置增量
int read_pause_return;
AVFormatContext *ic; // iformat的上下文
int realtime; // =1为实时流
Clock audclk; // 音频时钟
Clock vidclk; // 视频时钟
Clock extclk; // 外部时钟
FrameQueue pictq; // 视频Frame队列
FrameQueue subpq; // 字幕Frame队列
FrameQueue sampq; // 采样Frame队列
Decoder auddec; // 音频解码器
Decoder viddec; // 视频解码器
Decoder subdec; // 字幕解码器
int audio_stream; // 音频流索引
int av_sync_type; // 音视频同步类型, 默认audio master
double audio_clock; // 当前音频帧的PTS+当前帧Du
int audio_clock_serial; // 播放序列,seek可改变此值
// 以下4个参数 非audio master同步方式使用
double audio_diff_cum; /* used for AV difference average computation */
double audio_diff_avg_coef;
double audio_diff_threshold;
int audio_diff_avg_count;
// end
AVStream *audio_st; // 音频流
PacketQueue audioq; // 音频packet队列
int audio_hw_buf_size; // SDL音频缓冲区的大小(字节为单位)
// 指向待播放的1帧音频数据,指向的数据区将被拷贝SDL音频缓冲区。若经过重采样则指向audio_buf1,否则指向frame中的音频
uint8_t *audio_buf; // 指向需要重采样的数据
uint8_t *audio_buf1; // 指向重采样后的数据
unsigned int audio_buf_size; /* 待播放的1帧音频数据(audio_buf指向)的大小 in bytes */
unsigned int audio_buf1_size; // 申请到的音频缓冲区audio_buf1的实际尺寸
int audio_buf_index; /* 更新拷贝位置 当前音频帧中已拷入SDL音频缓冲区 in bytes */
// 当前音频帧中尚未拷入SDL音频缓冲区的数据量:
// audio_buf_size = audio_buf_index + audio_write_buf_size
int audio_write_buf_size;
int audio_volume; // 音量
int muted; // =1静音,=0则正常
struct AudioParams audio_src; // 音频frame的参数
#if CONFIG_AVFILTER
struct AudioParams audio_filter_src;
#endif
struct AudioParams audio_tgt; // SDL支持的音频参数,重采样转换:audio_src->audio_tgt
struct SwrContext *swr_ctx; // 音频重采样context
int frame_drops_early; // 丢弃视频packet计数
int frame_drops_late; // 丢弃视频frame计数
enum ShowMode {
SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
} show_mode;
// 音频波形显示使用
int16_t sample_array[SAMPLE_ARRAY_SIZE];
int sample_array_index;
int last_i_start;
RDFTContext *rdft;
int rdft_bits;
FFTSample *rdft_data;
int xpos;
double last_vis_time;
SDL_Texture *vis_texture;
SDL_Texture *sub_texture; // 字幕显示
SDL_Texture *vid_texture; // 视频显示
int subtitle_stream; // 字幕流索引
AVStream *subtitle_st; // 字幕流
PacketQueue subtitleq; // 字幕packet队列
double frame_timer; // 记录最后一帧播放的时刻
double frame_last_returned_time;
double frame_last_filter_delay;
int video_stream; // 视频流索引
AVStream *video_st; // 视频流
PacketQueue videoq; // 视频队列
double max_frame_duration; // ⼀帧最⼤间隔. maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
struct SwsContext *img_convert_ctx; // 视频尺寸格式变换
struct SwsContext *sub_convert_ctx; // 字幕尺寸格式变换
int eof; // 是否读取结束
char *filename; // 文件名
int width, height, xleft, ytop; // 宽、高,x起始坐标,y起始坐标
int step; // =1 步进播放模式, =0 其他模式
#if CONFIG_AVFILTER
int vfilter_idx;
AVFilterContext *in_video_filter; // the first filter in the video chain
AVFilterContext *out_video_filter; // the last filter in the video chain
AVFilterContext *in_audio_filter; // the first filter in the audio chain
AVFilterContext *out_audio_filter; // the last filter in the audio chain
AVFilterGraph *agraph; // audio filter graph
#endif
// 保留最近的相应audio、video、subtitle流的steam index
int last_video_stream, last_audio_stream, last_subtitle_stream;
// 当读取数据队列满了后进入休眠时,可以通过该condition唤醒读线程
SDL_cond *continue_read_thread;
} VideoState;
Clock 时钟封装
typedef struct Clock {
double pts; /* 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上⼀帧 */
double pts_drift; /* 当前pts与当前系统时钟的差值, audio、video对于该值是独⽴的 */
// 当前时钟(如视频时钟)最后⼀次更新时间,也可称当前时钟时间
double last_updated; // 最后⼀次更新的系统时钟
double speed; // 时钟速度控制,⽤于控制播放速度
// 播放序列,所谓播放序列就是⼀段连续的播放动作,⼀个seek操作会启动⼀段新的播放序列
int serial; /* clock is based on a packet with this serial */
int paused; // = 1 说明是暂停状态
// 指向packet_serial
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
MyAVPacketList和PacketQueue队列
注意:
⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
这⾥也看到了serial字段,MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。
typedef struct MyAVPacketList {
AVPacket pkt; //解封装后的数据
struct MyAVPacketList *next; //下⼀个节点
int serial; //播放序列
} MyAVPacketList;
serial字段主要⽤于标记当前节点的播放序列号,
ffplay中多处⽤到serial的概念,主要⽤来区分是否连续数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。类似二维数组比如PacketQueue.ser=1 则MyAVPacketList.ser=2 ... MyAVPacketList.ser=4
PacketQueue.ser=2 则MyAVPacketList.ser=2 ... MyAVPacketList.ser=4
该结构体内定义了“队列”⾃身的属性
typedef struct PacketQueue {
MyAVPacketList *first_pkt, *last_pkt;// 队⾸,队尾指针
int nb_packets;// 包数量,也就是队列元素数量
int size;// 队列所有元素的数据⼤⼩总和
int64_t duration;// 队列所有元素的数据播放持续时间
int abort_request;// ⽤户退出请求标志
int serial;// 播放序列号,和MyAVPacketList的serial作⽤相同
SDL_mutex *mutex;// ⽤于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)
SDL_cond *cond;// ⽤于读、写线程相互通知(SDL_c