文章目录
认识ffplay
ffplay是ffmpeg源码提供的一个C语言实现的播放器实例,其使用了ffmpeg和SDL相关的API。阅读其源码对于我们理解播放器的原理和实现自己的播放器有很重要的意义。
ffplay的总体流程
总体流程如下图:
播放器初始化
此部分主要在函数stream_open中完成,mian函数中会调用该函数
- 初始化packet队列
- 初始化frame队列
- 初始化clock
- 创建读取线程
stream_open的主要实现如下:
static VideoState *stream_open(const char *filename,
const AVInputFormat *iformat)
{
/*** 分配结构体并初始化结构体 ***/
//初始化帧队列
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;
//初始化packet队列
if (packet_queue_init(&is->videoq) < 0 ||
packet_queue_init(&is->audioq) < 0 ||
packet_queue_init(&is->subtitleq) < 0)
goto fail;
/*** 创建条件变量 ***/
//初始化时钟
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;
/*** 初始化音量 ***/
//创建读线程
is->read_tid = SDL_CreateThread(read_thread, "read_thread", is);
return is;
}
注:给出的代码删去了相对无关的代码,并使用/*** ***/的注释形式解释了其代码功能
线程的划分
- 数据读取线程
- 音频解码线程
- 视频解码线程
- 音频播放线程
- 视频播放线程(在主线程中进行)
各个线程的讲解将于其他文章中讲解
核心数据结构分析
VideoState结构体
本结构体是ffplay中最重要的结构体,起到一个类似于管家的作用,用于保存播放器中各模块的主要内容,代码如下:
typedef struct Decoder {
AVPacket *pkt;
PacketQueue *queue; //数据包队列
AVCodecContext *avctx; //解码器上下文
int pkt_serial; //包序列
int finished; //等于0, 编码器处于工作状态
int packet_pending; //等于0, 编码器处于异常状态
SDL_cond *empty_queue_cond; //检查到队列为空时, 发送信号给read_thread读取数据
int64_t start_pts;
AVRational start_pts_tb;
int64_t next_pts;
AVRational next_pts_tb;
SDL_Thread *decoder_tid; //线程句柄
} Decoder;
typedef struct VideoState {
SDL_Thread *read_tid; //读线程句柄
const AVInputFormat *iformat; //指向解复用器
int abort_request; //置为1,表示推出
int force_refresh; //刷新画面, =1时立刻刷新
int paused; //暂停, 播放
int last_paused; //暂存‘pause’状态
int queue_attachments_req;
int seek_req; //标识一次seek
int seek_flags; //seek标志, 用世间还是字节去seek
int64_t seek_pos; //请求seek的绝对位置
int64_t seek_rel; //相对位置
int read_pause_return;
AVFormatContext *ic; //解复用器上下文
int realtime; // =1时为实时流
Clock audclk; //音频时钟
Clock vidclk; //视频时钟
Clock extclk; //外部时钟
FrameQueue pictq; //视频帧队列
FrameQueue subpq; //字幕帧队列
FrameQueue sampq; //采样帧队列
Decoder auddec; //音频解码器
Decoder viddec; //视频解码器
Decoder subdec; //字幕解码器
int audio_stream; //音频索引流
int av_sync_type; //不同的音视频同步类型, 默认以audio master
double audio_clock;
int audio_clock_serial;
double audio_diff_cum; /* used for AV difference average computation */
double audio_diff_avg_coef;
double audio_diff_threshold;
int audio_diff_avg_count;
AVStream *audio_st; //音频流
PacketQueue audioq; //音频packet队列
int audio_hw_buf_size; //SDL音频缓冲区大小
// 指向待播放的⼀帧⾳频数据,指向的数据区将被拷⼊SDL⾳频缓冲区。若经过重采样则指向audio_buf1,
// 否则指向frame中的⾳频
uint8_t *audio_buf; //指向需要重采样的数据
uint8_t *audio_buf1; //指向重采样后的数据
unsigned int audio_buf_size; /* in bytes */ //待播放的一段音频数据大小
unsigned int audio_buf1_size; //申请到的音频缓冲区的实际尺寸
int audio_buf_index; /* in bytes */
int audio_write_buf_size;
int audio_volume; //音量
int muted; //=1则静音
struct AudioParams audio_src; //音频frame的参数
struct AudioParams audio_filter_src;
struct AudioParams 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;
AVTXContext *rdft;
av_tx_fn rdft_fn;
int rdft_bits;
float *real_data;
AVComplexFloat *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 *sub_convert_ctx; //字幕尺寸变化模式
int eof; //是否读取结束
char *filename; //文件名
int width, height, xleft, ytop; //宽高, 起始坐标
int step; // =1 步进模式, =0 其他模式
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
//保留最近的相应流的索引
int last_video_stream, last_audio_stream, last_subtitle_stream;
SDL_cond *continue_read_thread; //读取队列满而进入休眠后, 用过该condition唤醒
} VideoState;
VideoState是ffplay中相当主要的一个结构体,但这里并不需要将其中的成员的全部记住,了解其大致起到的管家作用和关键的成员即可继续往后阅读。
MyAvPacketList结构体
ffplay中使用MyAVpacketList结构体来保存解封装后的数据,即AVpacket,实现如下:
//队列的数据
typedef struct MyAVPacketList {
AVPacket *pkt; //解封装后的数据
int serial; //播放序列
} MyAVPacketList;
MyAVPacketList是对ffmpeg中AVPacket进行了封装,同时里面的serial被用作识别pkt是否为当前播放序列,如果不是则会丢弃。
serial字段在ffpaly十分重要,这里务必理解其含义。serial用于标记当前节点的播放序列号,主要用来区分是否是连续数据,每一次进行seek操作,都会使得serial做+1的操作,以区分不同的播放序列. 每次 seek 以后 以前的队列中的东西当然就不能用了呗 应该放弃的放弃 要free 的 free。
PacketQueue结构体
代码具体实现如下:
typedef struct PacketQueue {
AVFifo *pkt_list; //队列的数据
int nb_packets; //包的数量, 即队列的元素数量
int size; //队列所有元素的数据大小的综合
int64_t duration; //队列所有节点播放时间总和
int abort_request; //用户请求退出标志
int serial; //播放序列号
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
ffplay使用MyAVPacketList来保存解封装后的数据,而使用PacketQueue来存储MyAVpacketList数据。在函数packet_queue_put_private()会将外部传入的包存储进PacketQueuet中。AVFifo 是一个按字节存储数据的结构体。
MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。
注意:⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
Packet提供的方法很多,这里不做详细介绍,仅仅介绍其功能:
详细介绍看这篇文章:
初窥门径-ffplay-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:清除队列内所有的节点
Frame结构体
对于ffpmeg来说,音频和视频被解码后的数据都是存储于AVframe结构体中的,但字幕数据不同,其解码后数据是存储于AVsubtitle中的。为了使得所有的解码数据都能统一存在某一结构体中,ffplay重新封装了Frame结构体,也因此,Frame中的只有部分成员对当前类型起作用。
结构体的实现如下:
typedef struct Frame {
AVFrame *frame; //指向数据帧
AVSubtitle sub; //用于字幕
int serial; //播放序列
double pts; //时间戳,单位为秒
double duration; //持续时间
int64_t pos; /* byte position of the frame in the input file */
int width; //图像宽度(仅对视频)
int height;
int format; //图像:AVPixelformat, 音频:AVSampleFormat
AVRational sar; //图像的宽高比, 默认为0/1
int uploaded; //记录该帧是否已经被显示过
int flip_v; // =1则旋转180度
} Frame;
FrameQueue结构体
废话少说,先上代码:
typedef struct FrameQueue {
Frame queue[FRAME_QUEUE_SIZE];
int rindex; //读索引
int windex; //写索引
int size; //当前总帧数
int max_size; //可储存最大帧数
int keep_last; // =1说明队列最后要保持一帧的数据不释放
int rindex_shown; //配合keep_last使用
SDL_mutex *mutex;
SDL_cond *cond;
PacketQueue *pktq; //数据包缓冲队列
} FrameQueue;
由代码可以看出,FrameQueue其实是一个由数组实现的存储Frame数据的环形缓冲区。ffplay中,视频,音频,字幕有各自的解码线程和播放线程,故ffplay中其实也存在着对应的三种frameQueue,每个队列都有一个读端和写端,读端位于播放线程,而写端自然而然在解码线程接受数据。
FrameQueue提供的操作嘎嘎多,本文不做详细介绍,这里和PacketQueue一样,仅仅给出简单介绍。
详细介绍看这篇文章:
初窥门径Ⅱ-ffplay-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时使⽤
AudioParams结构体
结构体比较简单,大家直接看源码即可:
typedef struct AudioParams {
int freq; //采样率
AVChannelLayout ch_layout;
enum AVSampleFormat fmt; //采样格式
int frame_size; //一个采样单元占用的字节数
int bytes_per_sec; //一秒时间的字节数
} AudioParams;
Decoder结构体
该结构体是对解码器的一个封装,在解码线程中还会详细介绍,内容也比较简单,根据注释,相信大家就能看出该结构体作用,这里做个大致了解即可。
typedef struct Decoder {
AVPacket *pkt;
PacketQueue *queue; //数据包队列
AVCodecContext *avctx; //解码器上下文
int pkt_serial; //包序列
int finished; //等于0, 编码器处于工作状态
int packet_pending; //等于0, 编码器处于异常状态
SDL_cond *empty_queue_cond; //检查到队列为空时, 发送信号给read_thread读取数据
int64_t start_pts; //初始化是stream的start time
AVRational start_pts_tb; //初始化是stream的time base
int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts时使⽤next_pts进⾏推算
AVRational next_pts_tb; //next_pts的单位
SDL_Thread *decoder_tid; //线程句柄
} Decoder;