工欲善其事必先利其器,如何编译调试ffplay.c (Ubuntu)
1. 安装SDL
下载sdl2
wget https://www.libsdl.org/release/SDL2-2.0.8.tar.gz
解压缩
tar -zxvf SDL2-2.0.8.tar.gz
进入解压后的sdl2文件夹
cd SDL2-2.0.8
编译、安装
./configure
make -j16 & make install
2. 编译ffplay
我使用了的是ffmpeg4.4版本,具体怎么下载和解压缩这里就不再说了,如果是小白去网上查一下
最后得到如下图所示的目录:
编译
./configure \
--enable-gpl \
--enable-nonfree \
--enable-debug=3 \
--disable-optimizations \
--disable-asm \
--disable-stripping \
--enable-sdl
make -j16 & make install
如果不出错的情况下,就会在目录生成ffmpeg、ffplay、ffprobeg这几个应用程序
3. 使用vscode调试代码
我用的wsl环境,直接vscode安装wsl插件就可以打开了.如果你的是vmware或者virtualbox虚拟机的话,那需要安装SSH remote插件,直接在vscode插件商城搜. 这点安装的东西相信一个程序员都会
还需要安装一个调试c++的插件
我们打开fftools/ffplay.c*,找到main()函数,打上断点,就像这样:
然后我们创建launch.json文件
然后根据我们上面安装C++插件,让他自动生成调试的配置参数
然后安装我下面这样配置, ${workspaceFolder}是当前项目的目录
开始调试,点击下图所示的按钮
成功运行就像下面图这样
ffplay.c源码分析
main函数里的代码流程挺短的,主要工作都封装到其他函数里了.这里画个main函数流程图,先有个大致的认识,后面慢慢逐个分析.
最主要的核心工作都是在
stream_open()函数中做的,把这个函数搞明白以及对应的数据结构搞明白了. 那就ok了.下面直接从这个函数讲起
stream_open()函数分析
在此之前,我们先介绍一下用到的几个数据结构:
VideoState: 存储音频和视频的状态还有数据
struct VideoState
{
SDL_Thread* read_tid; // 存储read_thread的线程标识
AVInputFormat* iformat; // 用于存储容器相关信息(比如mp4、flv)
int abort_request; //
Clock audclkt; // 音频时钟
Clock vidclk; // 视频时钟
CLock extclk; // 外部时钟
FrameQueue pictq; // 视频的AVFrame队列
FrameQueue subpq; // 字幕的AVFrame队列
FrameQueue sampq; // 音频的AVFrame队列
char* filename; // 需要播放文件路径
int width; // 播放器窗口宽
int height; // 播放器窗口高
int xtop; // 窗口位置
int xleft; // 窗口位置
PacketQueue videoq; // 视频的AVPacket队列
PacketQueue audioq; // 音频的AVPacket队列
int audio_volume; // 音频声音大小
int muted; // 是否静音
int av_sync_type; // 音视频同步方式,一共3种: 音频为准、视频为准、外部时钟为准
};
上面又用到了几个数据结构: Clock、FrameQueue、PacketQueue.我们再看一下这几个是怎么定义的
Clock
struct Clock
{
double pts; // 当前时钟的播放时间戳
double pts_drift; // pts偏移量,用于矫正时钟. 计算方式: 当前pts减去初始化时间
double last_updated; // 上一次更新时钟的系统时间,通过av_gettime_relative()获取
double speed; // 时钟速度,默认1.0
int serial; // 时钟所属的流的序列号,变化时标识需要重置时钟
int paused; // 暂停, 1表示暂停
// 指向当前流包序列的序列号,用于检测时钟同步的流是否发生变化
int* queue_serial;
};
Frame,FrameQueue中用到的数据结构
struct Frame
{
AVFrame* frame; // 解码后的原始数据
AVSubtitle* sub; // 字幕
int serial; // 当前帧的流序列号
double pts; // 当前帧的播放时间戳
double duration; // 持续时间
int64_t pos; // 在媒体文件中的位置(偏移量)
int width; // 帧的宽度(单位像素)
int height; // 帧的高度(单位像素)
int format; // 像素格式(比如YUV420、RGBA)
AVRational sar; // 采样纵横比
int uploaded; // 是否已经上传到GPU
int flip_v; // 是否需要垂直翻转. 因为有的0坐标不一致
};
FrameQueue
struct FrameQueue
{
Frame queue[FRAME_QUEUE_SIZE]; // 存储帧数据的数组,队列的实际存储
int rindex; // 队列读取索引,指示当前正在读取的帧
int windex; // 队列写入索引,指示当前正在写入的帧
int size; // 当前队列中帧的数量
int max_size; // 队列的最大容量
int keep_last; // 标记是否保持队列中的最后一帧(用于避免丢帧)
int rindex_shown; // 最后一帧的读取索引,用于显示同步
SDL_mutex* mutex; // 互斥锁,用于多线程访问队列时同步
SDL_cond* cond; // 条件变量,用于同步队列操作
PacketQueue* pktq; // 一个指向数据包队列的指针(用于解码或获取帧)
};
AVFifoBuffer
struct AVFifoBuffer
{
uint8_t* buffer; // 缓冲区起始地址
uint8_t* rptr, *wptr, *end; // 读取指针,写入指针,缓冲区末尾
uint32_t rndx, wndx; // 读取、写入操作索引
};
PacketQueue
struct PacketQueue
{
AVFifoBuffer* pkt_list;
int nb_packets; // 队列中数据包的数量
int size; // 数据大小 + AVPacket结构体占用的数据大小
int64_t duration; // 队列中所有数据包的总持续时间
int abort_request; // 是否请求中止队列的标志
// 当前队列的序列号,通常与流同步,每次跳转播放时间点
// 通过比较匹配来丢弃无效的缓存帧. 比如缓存了5分钟帧,但是跳转到8分钟后
// 就需要丢弃这些帧
int serial;
SDL_mutex* mutex; // 互斥锁,用于多线程同步访问队列
SDL_cond* cond; // 条件变量,用于线程之间的同步
};
然后我再看看stream_open函数做了哪些内容,其实也没做啥.
就是初始化队列和时钟,然后创建一个线程,线程函数是read_thread.
在main函数中是这样调用stream_open的.
这里的
file_iformat内容为nullptr
VideoState *stream_open(const char *filename, AVInputFormat *iformat)
{
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;
// 初始化AVFrame队列,参数还传入了AVPacket队列,两者关联起来
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;
// 初始化AVPacket队列
if (packet_queue_init(&is->videoq) < 0 ||
packet_queue_init(&is->audioq) < 0 ||
packet_queue_init(&is->subtitleq) < 0)
goto fail;
// 创建条件变量
if (!(is->continue_read_thread = SDL_CreateCond()))
{
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
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;
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;
is->muted = 0; // 不静音,设置为0
is->av_sync_type = av_sync_type; // 设置音视频同步方式: 音频为准
//////////////////////////////////////////////////////////////////////
// 这里创建一个线程,然后执行 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;
}
return is;
}
上面向read_thread函数传入is参数,也就是VideoState数据结构类型的,接下来分析下read_thread函数怎么做的.
read_thread函数分析
该函数的流程分析图如下:
该函数主要工作就是从媒体文件或流读取AVPacket,然后放到具体的PacketQueue队列. 创建了audio_thread、video_thread用于打开音频流和视频流对应的解码器,开启解码器线程区解码.
接下来详细分析read_thread的代码
其中avformat_open_input函数的源码剖析可以看,这篇文章avformat_open_input源码剖析
audio_thread 音频解码线程
未完待续...
video_thread 视频解码线程
未完待续...