现在开始debugread_thread

/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
    VideoState *is = arg;
    AVFormatContext *ic = NULL;
    int err, i, ret;
    int st_index[AVMEDIA_TYPE_NB];
    AVPacket *pkt = NULL;
    int64_t stream_start_time;
    int pkt_in_play_range = 0;
    const AVDictionaryEntry *t;
    SDL_mutex *wait_mutex = SDL_CreateMutex();
    int scan_all_pmts_set = 0;
    int64_t pkt_ts;

开局一堆变量,也不知道是啥。

    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate packet.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    ic = avformat_alloc_context();
    if (!ic) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }

进行了一些初始化,SDL的部分我就不说了,ffmpeg的话初始化分配了AVPacketAVFormatContext

ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;

static int decode_interrupt_cb(void *ctx)
{
    VideoState *is = ctx;
    return is->abort_request;
}

对ic(AVFormatContext)设置了中断处理函数decode_interrupt_cb。ffmpeg会在阻塞的时候调用该函数。

if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
    av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
    scan_all_pmts_set = 1;
}

这里format_optsAVDictionary的一个变量。

AVDictioanry是ffmpeg定义的一个字典数据类型。

av_dict_get是用来获取"scan_all_pmts"key的value的,如果没有这个key,就插入这条key,并把value设为了"1"。 这里的"scan_all_pmts"目前还不知道是什么,继续看。

err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
if (err < 0) {
    print_error(is->filename, err);
    ret = -1;
    goto fail;
}

然后是打开流。很好,这里就发现了format_opts的用处,查了一下,它是用来控制avformat_open_input对文件进行解复用是的行为。 目前设置的"scan_all_pmts"是扫描所有私有元数据的行为。

// avformat_open_input
if (options) {
    av_dict_free(options);
    *options = tmp;
}

这里avformat_open_input会对format_opts的内容copy一份,在函数返回前,会把format_opts释放掉并替换成copy的,所以format_opts在调用前后地址已经被替换掉了,而且内容也可能会有变化,不过目前只有"scan_all_pmts"所以也没什么变化,具体看ffmpeg docs去。

if (scan_all_pmts_set)
    av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
remove_avoptions(&format_opts, codec_opts);

之后是删掉刚刚的"scan_all_pmts"并且移除掉format_optscodec_opts的部分。

ret = check_avoptions(format_opts);
if (ret < 0)
    goto fail;
is->ic = ic;

这里是checkformat_opts里是否还有东西,就是看有没有清空,之后再把ic(AVFormatContext)存到is(VideoState)里。

if (genpts)
    ic->flags |= AVFMT_FLAG_GENPTS;

这个是用户输入参数来决定是否生成时间戳,具体干啥的不清楚。

if (find_stream_info) {
    AVDictionary **opts;
    int orig_nb_streams = ic->nb_streams;
    // ...

之后有一大段是来寻找流的信息的,放在audio就是采样率,时长之类,还有样本格式什么的。这里用户可以指定不打印流信息使find_stream_info为0吧,虽然我没试过。

有一个setup_find_stream_info_opts用于找stream中的一些信息并设置opts,不过我这里跑下来opts空的。

之后就是老生常谈的avformat_find_stream_info了,用于获取流的信息,调用后你就可以在AVFormatContext中查看到音频的采样率等之类的信息了。

哦对了,这里说一嘴,我是在debug的同时去用cpp复现ffplay的音频部分,但ffplay中有些使用的函数是libavutil/cmdutils.h里的,比如刚刚的setup_find_stream_info_optscheck_avoptions等,包括其使用的format_optscodec_opts都是在cmdutls.h中extern引用来的,在cmdutils.c中定义的。

标准的ffmpeg编译下来是不包含cmdutils.h的,这个只在生成ffmpeg,ffprobe以及ffplay中才会使用到。所以在复现时无法直接调用这类函数,不过我目前的功能只是单纯的音频播放,并不包括这些复杂的cmd options,所以这部分我都不用管。

if (ic->pb)
    ic->pb->eof_reached = 0; // FIXME hack, ffplay maybe should not use avio_feof() to test for the end

pbAVIOContext文件IO上下文,这里是一个bug,注释里说的是avio_feof在某些情况下检测到的eof是错误的,ffplay作者建议不应该使用avio_feof。这里用了个临时的解决办法就是把eof_reached设为0。

if (seek_by_bytes < 0)
    seek_by_bytes = !(ic->iformat->flags & AVFMT_NO_BYTE_SEEK) &&
                    !!(ic->iformat->flags & AVFMT_TS_DISCONT) &&
                    strcmp("ogg", ic->iformat->name);

check是否按bytes seek,即是否标志位没有AVFMT_NO_BYTE_SEEK,并且有AVMFT_TS_DISCONT(即时间不连续)并为ogg格式。

is->max_frame_duration = (ic->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0;

获取最大帧持续时间,时间戳不连续为10否则为3600,虽然不知道单位是什么,us ms s。

if (!window_title && (t = av_dict_get(ic->metadata, "title", NULL, 0)))
    window_title = av_asprintf("%s - %s", t->value, input_filename);

从读取到的文件中查找metadata中是否有标题。有则打印。

/* if seeking requested, we execute it */
if (start_time != AV_NOPTS_VALUE) {
    int64_t timestamp;

    timestamp = start_time;
    /* add the stream start time */
    if (ic->start_time != AV_NOPTS_VALUE)
        timestamp += ic->start_time;
    ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
    if (ret < 0) {
        av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",
                is->filename, (double)timestamp / AV_TIME_BASE);
    }
}

这一部分应该是在开始播放前指定播放位置的情况。start_time被设置的话(通过-ss选项指定开始播放时间),加上ic->start_time(第一帧的起始时间,一般是0,但某些情况下为别的值,在我的mp3中为256xx来着)。得到真正开始播放时间之后就用avformat_seek_file来寻道。ffmpeg中的时间单位为AV_TIME_BASE

is->realtime = is_realtime(ic);

这个是判定输入文件是否实时,对于单纯本地文件播放来说是不需要的。(is_realtime里面是判断文件格式是否为rtp,rtsp,sdp等等)

然后是是否打印音视频信息,用的av_dump_format

for (i = 0; i < ic->nb_streams; i++) {
    AVStream *st = ic->streams[i];
    enum AVMediaType type = st->codecpar->codec_type;
    st->discard = AVDISCARD_ALL;
    if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
        if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
            st_index[type] = i;
}
for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
   if (wanted_stream_spec[i] && st_index[i] == -1) {
        av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not match any %s stream\n", wanted_stream_spec[i], av_get_media_type_string(i));
        st_index[i] = INT_MAX;
    }
}

这部分是只获取指定的流,将指定了的流的index放入st_index的感觉。说是感觉因为我并没有指定流,所以这块debug就直接被跳过了。

if (!video_disable)
    st_index[AVMEDIA_TYPE_VIDEO] =
        av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                            st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
    st_index[AVMEDIA_TYPE_AUDIO] =
        av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                            st_index[AVMEDIA_TYPE_AUDIO],
                            st_index[AVMEDIA_TYPE_VIDEO],
                            NULL, 0);
if (!video_disable && !subtitle_disable)
    st_index[AVMEDIA_TYPE_SUBTITLE] =
        av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                            st_index[AVMEDIA_TYPE_SUBTITLE],
                            (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                             st_index[AVMEDIA_TYPE_AUDIO] :
                             st_index[AVMEDIA_TYPE_VIDEO]),
                            NULL, 0);

这里是找到符合参数要求的各个流,video, audio, subtitle。

is->show_mode = show_mode;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
    AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
    AVCodecParameters *codecpar = st->codecpar;
    AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
    if (codecpar->width)
        set_default_window_size(codecpar->width, codecpar->height, sar);
}

/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
    stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}

ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
    ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
}
if (is->show_mode == SHOW_MODE_NONE)
    is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
    stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
}

if (is->video_stream < 0 && is->audio_stream < 0) {
    av_log(NULL, AV_LOG_FATAL, "Failed to open file '%s' or configure filtergraph\n",
           is->filename);
    ret = -1;
    goto fail;
}

设置显示模式的enum类型,没啥说的。

第一个if是处理video流的,设置窗口大小,我不管。

第二个是音频流,

oh 嘞 shift。又一个重磅炸弹。看来今天,大抵,或许,可能,是看不完read_thread的了。

这个stream_compontent_open的内容也挺多了,上百行的。。。

总之它打开了音频流。嗯,就先这样。

不行,总觉得不把这个先看了后面的看不下去。

嘛,先把剩下两个说完,音频流打开后后两个也同理,打开视频流和字幕流,最后判断有没有流打开。 中间会有个设置显示模式的,呃,我只有音频流,它最后得到的是SHOW_MODE_RDFT

这个RDFT,就是我苦手的傅里叶变换了。

RDFT, Real Discrete Fourier Transform (实数离散傅里叶变换)。一种傅里叶变换的变体,用于处理实数信号。至于它是什么,我只想说,我不想,可能又要,学习数学了。。。


流打开之后,就开始进入循环不断去解码packet了吧,这是最后的部分了,也是很复杂的一部分,毕竟还要涉及播放暂停,寻道,音量控制等各种麻烦的东西,而且我最终还是想要实现播放暂停和切换音频时导致的杂音用淡入淡出来消除的功能,这个真是我目前最大的难点了。

嘛,这部分要等到stream_compontent_open看完再看喽。然后这个也看完后就剩最后一个主线程的事件处理了。最后就可以去实现最终boss,淡入淡出

好了,今天就到这了,顺便一提明天就离开老家了,后天就要上班。。。工作上的事情好多好烦啊。。 所以可能明天也看不完stream_compontent_open的部分了。而且从明天开始进度会慢很多很多,毕竟不能像过年这会儿时间这么充裕可以全用来看代码。日常的生活当然是周内加班赶工作进度,周内估计也就一两个小时能debug吧。周末有时间但也不可能全用在debug上,还要学日语(毕竟7月份就要jplt了来着),而且当然要推gal啦!活着就是为了Gal!所以周内实际上可能就没有时间debug了哈哈,毕竟难得有时间就像打gal嘻嘻嘻。

じゃ,下次见喽。