从这里开始,我将实时记录debug ffplay的过程,也算是一种学习了吧。(第一次debug这种大项目,对我来说很大了,快4000行了欸,搞不好中途就放弃了哈哈)

/* Called from the main */
int main(int argc, char **argv)
{
    int flags, ret;
    VideoState *is;

    init_dynload();

一开始,就不知道这个VideoState是干啥的。

init_dynload也不知道欸。

debug是在vscode上,但翻阅代码就在github上了。毕竟我debug技巧没那么熟练。gdb用不好,vscode也是,vs also。

我cao了呀,这VideoState有100行的结构体,算了还是具体调试时再看吧。

第一个,init_dynload,听起来像 initialize dynamic load,初始化动态加载,分配内存用的吗。

// cmdutils.c

void init_dynload(void)
{
#if HAVE_SETDLLDIRECTORY && defined(_WIN32)
    /* Calling SetDllDirectory with the empty string (but not NULL) removes the
     * current working directory from the DLL search path as a security pre-caution. */
    SetDllDirectory("");
#endif
}

这好像是特定win平台时重置dll的路径一样。

好吧HAVE_SETDLLDIRECTORYconfig.h中为0,这说明我的平台上是没有这个func的吧。

那这个func就什么都没干喽。

ok,下一个。

av_log_set_flags(AV_LOG_SKIP_REPEATED);
parse_loglevel(argc, argv, options);

av_log_set_flags设置log flags。

AV_LOG_SKIP_PEREATED用于将av_log所打的日志中重复的log跳过。

哦,对了。我会尝试同步自己实现一遍ffplay。不过只实现本地音频播放的功能,用cpp。该项目请参考link

static const OptionDef options[] = {
    CMDUTILS_COMMON_OPTIONS
    { "x",                  OPT_TYPE_FUNC, OPT_FUNC_ARG, { .func_arg = opt_width }, "force displayed width", "width" },

optionsOptionDef的一个数组,里面有超~~多的选项,估计是为了分析args来映射使用的功能。

void parse_loglevel(int argc, char **argv, const OptionDef *options)
{
    int idx = locate_option(argc, argv, options, "loglevel");
    char *env;

    check_options(options);

    if (!idx)
        idx = locate_option(argc, argv, options, "v");
    if (idx && argv[idx + 1])
        opt_loglevel(NULL, "loglevel", argv[idx + 1]);
    idx = locate_option(argc, argv, options, "report");
    env = getenv_utf8("FFREPORT");
    if (env || idx) {
        FILE *report_file = NULL;
        init_report(env, &report_file);
        if (report_file) {
            int i;
            fprintf(report_file, "Command line:\n");
            for (i = 0; i < argc; i++) {
                dump_argument(report_file, argv[i]);
                fputc(i < argc - 1 ? ' ' : '\n', report_file);
            }
            fflush(report_file);
        }
    }
    freeenv_utf8(env);
    idx = locate_option(argc, argv, options, "hide_banner");
    if (idx)
        hide_banner = 1;
}

我勒个龟龟,直接把所有代码贴出来还是太多了,我之后还是调着贴吧。

for (i = 1; i < argc; i++) {
    const char *cur_opt = argv[i];

    if (*cur_opt++ != '-')
        continue;

靠,我不知道为啥locate_option这里cur_opt指向的是argv[1]的第二个字符。argv[1]为test.mp3,结果cur_opt被赋值为est.mp3?我也没开编译器优化选项啊。为啥

嗯,好吧,locate_option这里就是查看你参数里有没有-xxx这类的选项,我因为只传的test.mp3所以这里就直接退出了。

我只调试test.mp3的情况的代码哦,音频的部分也暂且先不看,毕竟我最初只是为了解决ffmpeg流式解码的问题。当然,视频的部分等到时候想要做视频库的时候再看哈。(对了,我调试的的代码是ffmpeg官网上下的7.1源码,并不是github上最新的(main分支的),所以不同版本的具体细节上还会不一样的)。

check_options用来检索options的定义是否正确,这个就不用管。

然后是开始检查是否第一步的locate_option检查出东西了,因为我没有传-xxx的选项,所以实际上locate_option的调用会一直是0。

欸油,你这写的也不行嘛哈哈哈。应该直接检查我是否有-xxx的选项,没有就直接不调用locate_option就行了哈哈。不过它这么写或许会有模块化或逻辑化的考量,我不知道就是了嘛。

getenv_utf8是获取环境变量的,不过我这里它获取的FFREPORT为0就是了。

很好,parse_loglevel这个func根本没有起到任何作用。

然后两行是注册所有codec, demux protocols的,和网络的初始化(用不到)。

之后是设了俩signal的处理函数,处理退出程序是返回123。

之后show_banner就打印了一串版本信息。

之后对环境进行各种设定,初始化SDL,以及有段代码是为了解决ALSA的问题。


SDL的统统跳过,好啦,终于到第一个重头戏了。

is = stream_open(input_filename, file_iformat);
if (!is) {
    av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
    do_exit(NULL);
}
static VideoState *stream_open(const char *filename,
                               const 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); // copy string
    if (!is->filename)
        goto fail;
    is->iformat = iformat;
    is->ytop    = 0;
    is->xleft   = 0;

stream_open先是给VideoState分配了内存。 然后是初始化。

/* start video display */
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;

这里调用三个frame_queue_init,第一个是初始化video的我不看,第二个是初始化subtitle(字幕)的我也不看,我只看第三个初始化audio的哈。

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last);

进来后直接俩队列FrameQueuePacketQueue,然后是队列的大小max_size,以及是否保留最后一帧keep_last

typedef struct PacketQueue {
    AVFifo *pkt_list;   // AVFifo,ffmpeg提供的一个动态队列(可切换为固定容量)。用于实际存取包数据
    int nb_packets;     // 包数量
    int size;           // 容量
    int64_t duration;   // 总包播放的时间
    int abort_request;  // 终止请求,用于暂停寻道之类
    int serial;         // 这个还不太清楚,是流的序号吗?
    SDL_mutex *mutex;   // 锁
    SDL_cond *cond;     // 条件变量
} PacketQueue;

/* Common struct for handling all types of decoded data and allocated render buffers. */
typedef struct Frame {
    AVFrame *frame; // 解码后的帧
    AVSubtitle sub; // 字幕
    int serial;     // 当前帧的序列号
    double pts;           /* presentation timestamp for the frame */
    double duration;      /* estimated duration of the frame */
    int64_t pos;          /* byte position of the frame in the input file */
    int width;      // 宽度(像素)
    int height;     // 高度(像素)
    int format;     // 像素格式
    AVRational sar; // 采样宽高比 sample aspect ratio
    int uploaded;   // 是否上传到GPU,以此来硬件加速
    int flip_v;     // 是否垂直翻转
} Frame;

typedef 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;  // 未解码的包队列
} FrameQueue;

相关的结构体的内容我都标在注释里了。

frame_queue_init就是对FrameQueue进行初始化的,该清空的清空,alloc的alloc。

f->keep_last = !!keep_last;

这句话看起来是不是挺迷惑的,其实就是将keep_last的任意整数值全转化为0或1。所以为什么不一开始就用bool嘞?(为了扩展性吗!?)

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;
}

is就是之前那个VideoState,反正把它当作全局变量就可。

continue_read_thread是条件变量,至少字面意思翻译是继续读线程。

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;

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;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

Clock,每一个包都有一个(Clockserialpacket_serial)。

Clock正常初始化。

剩下的也是对VideoState中的一些其他变量进行初始化也没什么好看的。

    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;
}

open_stream的最后一部分,是创建并执行了一个read_thread函数的线程,read_thread的内容待会儿再看(其实是明天了吧,今天已经好累了。。。)。

至此,整个open_stream结束。可以看到open_stream就干了两件事,初始化,和read_thread,这个可能就是直接从test.mp3中读数据到VideoState了吧。嘛,也算初始化的部分。

好了,明天再看read_thread的部分,今天就先到这儿。