ffplay.c源码剖析

281 阅读7分钟

工欲善其事必先利其器,如何编译调试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版本,具体怎么下载和解压缩这里就不再说了,如果是小白去网上查一下
最后得到如下图所示的目录:
image.png

编译

./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插件商城搜. 这点安装的东西相信一个程序员都会

image.png image.png

还需要安装一个调试c++的插件

image.png

我们打开fftools/ffplay.c*,找到main()函数,打上断点,就像这样:
image.png

然后我们创建launch.json文件

image.png

然后根据我们上面安装C++插件,让他自动生成调试的配置参数

image.png

image.png

然后安装我下面这样配置, ${workspaceFolder}是当前项目的目录 image.png

开始调试,点击下图所示的按钮
image.png

成功运行就像下面图这样
image.png

ffplay.c源码分析

main函数里的代码流程挺短的,主要工作都封装到其他函数里了.这里画个main函数流程图,先有个大致的认识,后面慢慢逐个分析.

image.png 最主要的核心工作都是在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的.
image.png 这里的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函数分析

该函数的流程分析图如下:
11.png

该函数主要工作就是从媒体文件或流读取AVPacket,然后放到具体的PacketQueue队列. 创建了audio_thread、video_thread用于打开音频流和视频流对应的解码器,开启解码器线程区解码.
接下来详细分析read_thread的代码

其中avformat_open_input函数的源码剖析可以看,这篇文章avformat_open_input源码剖析

audio_thread 音频解码线程

未完待续...

video_thread 视频解码线程

未完待续...