ffplay read线程分析

转自: https://zhuanlan.zhihu.com/p/43672062

ffplay中有一个线程专门处理数据读取,即read_thread

read_thread主要按以下步骤执行:

  1. 准备阶段:打开文件,检测Stream信息,打开解码器
  2. 主循环读数据,解封装:读取Packet,存入PacketQueue

read_thread的函数比较长,这里不贴完整代码,直接根据其功能分步分析。

准备阶段

准备阶段,主要包括以下步骤:

  1. avformat_open_input
  2. avformat_find_stream_info
  3. av_find_best_stream
  4. stream_component_open

avformat_open_input用于打开输入文件(对于网络流也是一样,在ffmpeg内部都抽象为URLProtocol,这里描述为文件是为了方便与后续提到的AVStream的流作区分),读取视频文件的基本信息。

avformat_open_input声明如下:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

具体说明参考源码注释。需要提到的两个参数是fmt和options。通过fmt可以强制指定视频文件的封装,options可以传递额外参数给封装(AVInputFormat).

看下步骤1的主要代码:

//创建一个以默认值初始化的AVFormatContext
    ic = avformat_alloc_context();
    if (!ic) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    //设置interrupt_callback
    ic->interrupt_callback.callback = decode_interrupt_cb;
    ic->interrupt_callback.opaque = is;

    //特定选项处理
    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;
    }

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

根据注释不难看懂代码。avformat_alloc_context主要malloc了一个AVFormatContext,并填充了默认值;interrupt_callback用于ffmpeg内部在执行耗时操作时检查是否有退出请求,并提前中断,避免用户退出请求没有及时响应;scan_all_pmts是mpegts的一个选项,这里在没有设定该选项的时候,强制设为1。最后执行avformat_open_input。

scan_all_pmts的特殊处理为基于ffplay开发的软件提供了一个“很好的错误示范”,导致经常看到针对特定编码或封装的特殊选项、特殊处理充满了read_thread,影响代码可读性!

在打开了文件后,就可以从AVFormatContext中读取流信息了。一般调用avformat_find_stream_info获取完整的流信息。为什么在调用了avformat_open_input后,仍然需要调用avformat_find_stream_info才能获取正确的流信息呢?看下注释:

Read packets of a media file to get stream information. This
is useful for file formats with no headers such as MPEG. This
function also computes the real framerate in case of MPEG-2 repeat
rame mode.
The logical file position is not changed by this function;
examined packets may be buffered for later processing.

该函数是通过读取媒体文件的部分数据来分析流信息。在一些缺少头信息的封装下特别有用,比如说MPEG(我猜测这里应该说ts更准确)。而被读取用以分析流信息的数据可能被缓存,总之文件偏移位置相比调用前是没有变化的,对使用者透明。

接下来就可以选取用于播放的视频流、音频流和字幕流了。实际操作中,选择的策略很多,一般根据具体需求来定——比如可以是选择最高清的视频流;选择本地语言的音频流;直接选择第一条视频、音频轨道;等等。

ffplay主要是通过av_find_best_stream来选择:

//根据用户指定来查找流
    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;
        }
    }

    //利用av_find_best_stream选择流
    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);

wanted_stream_spec通过main函数传参设定,格式可以有很多种,参考官方文档:https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1

如果用户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作用。

int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,//要选择的流类型
                        int wanted_stream_nb,//目标流索引
                        int related_stream,//参考流索引
                        AVCodec **decoder_ret,
                        int flags);

fffplay主要通过上述注释中的3个参数找到“最佳流”。

如果指定了正确的wanted_stream_nb,一般情况都是直接返回该指定流,即用户选择的流。如果指定了参考流,且未指定目标流的情况,会在参考流的同一个节目中查找所需类型的流,但一般结果,都是返回该类型第一个流。

经过以上步骤,文件打开成功,且获取了流的基本信息,并选择音频流、视频流、字幕流。接下来就可以所选流对应的解码器了。

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

看下stream_component_open.函数也比较长,逐步分析:

//static int stream_component_open(VideoState *is, int stream_index)

    avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);

    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    if (ret < 0)
        goto fail;
    av_codec_set_pkt_timebase(avctx, ic->streams[stream_index]->time_base);

先是通过avcodec_alloc_context3分配了解码器上下文AVCodecContex,然后通过avcodec_parameters_to_context把所选流的解码参数赋给avctx,最后设了time_base.

codec = avcodec_find_decoder(avctx->codec_id);

    switch(avctx->codec_type){
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index; forced_codec_name =    audio_codec_name; break;
        case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index; forced_codec_name = subtitle_codec_name; break;
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index; forced_codec_name =    video_codec_name; break;
    }
    if (forced_codec_name)
        codec = avcodec_find_decoder_by_name(forced_codec_name);
    if (!codec) {
        if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with name '%s'\n", forced_codec_name);
        else                   av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with id %d\n", avctx->codec_id);
        ret = AVERROR(EINVAL);
        goto fail;
    }

这段主要是通过avcodec_find_decoder找到所需解码器(AVCodec)。如果用户有指定解码器,则设置forced_codec_name,并通过avcodec_find_decoder_by_name查找解码器。找到解码器后,就可以通过avcodec_open2打开解码器了。(avcodec_open2附近还有一些选项设置,基本是旧API的兼容,略过不看)

最后,是一个大的switch-case:

switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        ……
    case AVMEDIA_TYPE_VIDEO:
        ……
    case AVMEDIA_TYPE_SUBTITLE:
        ……
    default:
        break;
}

即根据具体的流类型,作特定的初始化。但不论哪种流,基本步骤都包括类似以下代码(节选自AVMEDIA_TYPE_VIDEO分支):

decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
if ((ret = decoder_start(&is->viddec, video_thread, is)) < 0)
    goto out;

这两个函数定义如下:

static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->avctx = avctx;
    d->queue = queue;
    d->empty_queue_cond = empty_queue_cond;
    d->start_pts = AV_NOPTS_VALUE;
}

static int decoder_start(Decoder *d, int (*fn)(void *), void *arg)
{
    packet_queue_start(d->queue);
    d->decoder_tid = SDL_CreateThread(fn, "decoder", arg);
    if (!d->decoder_tid) {
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    return 0;
}

decoder_init比较简单,看decoder_start。decoder_start中“启动”了PacketQueue,并创建了一个名为"decoder"的线程专门用于解码,具体的解码流程由传入参数fn决定。比如对于视频,是video_thread

除了decoder_init和decoder_start,对于AVMEDIA_TYPE_AUDIO,还有一个重要工作是audio_open,我们将在分析音频输出时再看。

至此,该准备的都准备好了,接下来就可以真正读取并解码媒体文件了。

以上流程虽然在read_thread中,但基本处于“准备阶段”,还未进入读线程的主循环。所以在IjkPlayer项目中,将上述流程封装到了prepare阶段,对应Android MediaPlayer的准备阶段。

主循环读数据

主循环的代码一如既往的长,先看简化后的伪代码:

for (;;) {
        if (is->abort_request)
            break;//处理退出请求
        if (is->paused != is->last_paused) {
            //处理暂停/恢复
        }

        if (is->seek_req) {
            //处理seek请求
            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
        }

        //控制缓冲区大小
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            continue;
        }

        //播放完成,循环播放
        if (!is->paused &&
            (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
            (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
            if (loop != 1 && (!loop || --loop)) {
                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
            } else if (autoexit) {
                ret = AVERROR_EOF;
                goto fail;
            }
        }

        //读取一个Packet(解封装后的数据)
        ret = av_read_frame(ic, pkt);

        //放入PacketQueue
        packet_queue_put();
    }

主要的代码就av_read_framepacket_queue_putav_read_frame从文件中读取视频数据,并获取一个AVPacket,packet_queue_put把它放入到对应的PacketQueue中。

当然,读取过程还会有seek、pause、resume、abort等可能,所以有专门的分支处理这些请求。

PacketQueue默认情况下会有大小限制,达到这个大小后,就需要等待10ms,以让消费者——解码线程能有时间消耗。

播放完成后,会根据loop的设置决定是否循环。

接下来,我们从简化后的流程出发,详细看下具体代码。

暂停/恢复的处理:

if (is->paused != is->last_paused) {//如果paused变量改变,说明暂停状态改变
    is->last_paused = is->paused;
    if (is->paused)//如果暂停调用av_read_pause
        is->read_pause_return = av_read_pause(ic);
    else//如果恢复播放调用av_read_play
        av_read_play(ic);
}

ffmpeg有专门针对暂停和恢复的函数,所以直接调用就可以了。

av_read_pause和av_read_play对于URLProtocol,会调用其url_read_pause,通过参数区分是要暂停还是恢复。对于AVInputFormat会调用其read_pause和read_play.
一般情况下URLProtocol和AVInputFormat都不需要专门处理暂停和恢复,但对于像rtsp/rtmp这种在通讯协议上支持(需要)暂停、恢复的就特别有用了。

对于seek的处理,会比暂停/恢复略微复杂一些:

if (is->seek_req) {
    ……

    ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR,
               "%s: error while seeking\n", is->ic->filename);
    } else {
        if (is->audio_stream >= 0) {
            packet_queue_flush(&is->audioq);
            packet_queue_put(&is->audioq, &flush_pkt);
        }
        if (is->subtitle_stream >= 0) {
            packet_queue_flush(&is->subtitleq);
            packet_queue_put(&is->subtitleq, &flush_pkt);
        }
        if (is->video_stream >= 0) {
            packet_queue_flush(&is->videoq);
            packet_queue_put(&is->videoq, &flush_pkt);
        }
        if (is->seek_flags & AVSEEK_FLAG_BYTE) {
           set_clock(&is->extclk, NAN, 0);
        } else {
           set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
        }
    }
    is->seek_req = 0;
    is->queue_attachments_req = 1;
    is->eof = 0;
    if (is->paused)
        step_to_next_frame(is);
}

主要的seek操作通过avformat_seek_file完成。根据avformat_seek_file的返回值,如果seek成功,需要:

  1. 清除PacketQueue的缓存,并放入一个flush_pkt。放入的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据(PacketQueue函数的分析,可以参考:https://zhuanlan.zhihu.com/p/43295650
  2. 同步外部时钟。在后续音视频同步的文章中再具体分析。

最后清理一些变量,并:

  1. 设置queue_attachments_req以显示attachment画面
  2. 如果当前是暂停状态,就跳到seek后的下一帧,以直观体现seek成功了

关于attachment后面有研究了再分析。这里看下step_to_next_frame。

static void step_to_next_frame(VideoState *is)
{
    /* if the stream is paused unpause it, then step */
    if (is->paused)
        stream_toggle_pause(is);
    is->step = 1;
}

原代码的注释比较清晰了——先取消暂停,然后执行step。当设置step为1后,显示线程会显示出一帧画面,然后再次进入暂停:

//in video_refresh
if (is->step && !is->paused)
    stream_toggle_pause(is);

这样seek的处理就完成了。

前面seek、暂停、恢复都可以通过调用ffmpeg的函数,辅助一些流程控制完成封装。

而读取缓冲区的控制可以说是ffplay原生的特性了。

是否需要控制缓冲区大小由变量infinite_buffer决定。infinite_buffer为1表示当前buffer无限大,不需要使用缓冲区限制策略。

infinite_buffer是可选选项,但在文件是实时协议时,且用户未指定时,这个值会被强制为1:

static int is_realtime(AVFormatContext *s)
{
    if(   !strcmp(s->iformat->name, "rtp")
       || !strcmp(s->iformat->name, "rtsp")
       || !strcmp(s->iformat->name, "sdp")
    )
        return 1;

    if(s->pb && (   !strncmp(s->url, "rtp:", 4)
                 || !strncmp(s->url, "udp:", 4)
                )
    )
        return 1;
    return 0;
}

……
is->realtime = is_realtime(ic);
……
if (infinite_buffer < 0 && is->realtime)
    infinite_buffer = 1;

我们看下需控制缓冲区大小的情况:

if (infinite_buffer<1 &&
    (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
    || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
        stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
        stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
    /* wait 10 ms */
    SDL_LockMutex(wait_mutex);
    SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
    SDL_UnlockMutex(wait_mutex);
    continue;
}

缓冲区满有两种可能:

  1. audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M)
  2. 音频、视频、字幕流都已有够用的包(stream_has_enough_packets)

第一种好理解,看下第二种中的stream_has_enough_packets:

static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {
    return stream_id < 0 ||
           queue->abort_request ||
           (st->disposition & AV_DISPOSITION_ATTACHED_PIC) ||
           queue->nb_packets > MIN_FRAMES && (!queue->duration || av_q2d(st->time_base) * queue->duration > 1.0);
}

在满足PacketQueue总时长为0,或总时长超过1s的前提下:

有这么几种情况包是够用的:

  1. 流没有打开(stream_id < 0)
  2. 有退出请求(queue->abort_request)
  3. 配置了AV_DISPOSITION_ATTACHED_PIC?(这个还不理解,后续分析attachement时回头看看)
  4. 队列内包个数大于MIN_FRAMES(=25)

挺饶地,没有深刻体会其设计用意,不评论。

上述的几种处理都还是在正常播放流程内,接下来是对播放已完成情况的处理。

if (!is->paused &&
    (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
    (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
    if (loop != 1 && (!loop || --loop)) {
        stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
    } else if (autoexit) {
        ret = AVERROR_EOF;
        goto fail;
    }
}

这里判断播放已完成的条件依然很“ffplay”,需要满足:

  1. 不在暂停状态
  2. 音频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了
  3. 视频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了

在确认已结束的情况下,用户有两个变量可以控制播放器行为:

  1. loop: 控制播放次数(当前这次也算在内,也就是最小就是1次了),0表示无限次
  2. autoexit:自动退出,也就是播放完成后自动退出。

loop条件简化的非常不友好,其意思是:如果loop==1,那么已经播了1次了,无需再seek重新播放;如果loop不是1,==0,随意,无限次循环;减1后还大于0(--loop),也允许循环。也就是:

static int allow_loop() {
    if (loop == 1)
        return 0;

    if (loop == 0)
        return 1;

    --loop;
    if (loop > 0)
        return 1;

    return 0;
}

前面讲了很多读线程主循环内的处理,比如暂停、seek、结束loop处理等,接下来就看看真正读的代码:

ret = av_read_frame(ic, pkt);
if (ret < 0) {
    //文件读取完了,调用packet_queue_put_nullpacket通知解码线程
    if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
        if (is->video_stream >= 0)
            packet_queue_put_nullpacket(&is->videoq, is->video_stream);
        if (is->audio_stream >= 0)
            packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
        if (is->subtitle_stream >= 0)
            packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
        is->eof = 1;
    }
    //发生错误了,退出主循环
    if (ic->pb && ic->pb->error)
        break;

    //如果都不是,可能只是要等一等
    SDL_LockMutex(wait_mutex);
    SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
    SDL_UnlockMutex(wait_mutex);
    continue;
} else {
    is->eof = 0;
}

/* check if packet is in play range specified by user, then queue, otherwise discard */
stream_start_time = ic->streams[pkt->stream_index]->start_time;
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
pkt_in_play_range = duration == AV_NOPTS_VALUE ||
        (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
        av_q2d(ic->streams[pkt->stream_index]->time_base) -
        (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
        <= ((double)duration / 1000000);

//如果在时间范围内,那么根据stream_index,放入到视频、音频、会字幕的PacketQueue中
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
    packet_queue_put(&is->audioq, pkt);
} else if (pkt->stream_index == is->video_stream && pkt_in_play_range
           && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
    packet_queue_put(&is->videoq, pkt);
} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
    packet_queue_put(&is->subtitleq, pkt);
} else {
    av_packet_unref(pkt);
}

看起来很长,实际比上述各种特殊流程的处理都直白,主要为:

  1. av_read_frame读取一个包(AVPacket)
  2. 返回值处理
  3. pkt_in_play_range计算
  4. packet_queue_put放入各自队列,或者丢弃

步骤1、步骤2、步骤4,都比较直接,看注释即可。

这里看下pkt_in_play_range的计算,我们把以上代码分解下:

int64_t get_stream_start_time(AVFormatContext* ic, int index) {
    int64_t stream_start_time = ic->streams[index]->start_time;
    return stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0;
}

int64_t get_pkt_ts(AVPacket* pkt) {//ts: timestamp(时间戳)的缩写
    return pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
}

double ts_as_second(int64_t ts,AVFormatContext* ic,int index) {
    return ts * av_q2d(ic->streams[index]->time_base);
} 

double get_ic_start_time(AVFormatContext* ic) {//ic中的时间单位是us
    return (start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000;
}

有了这些函数,就可以计算pkt_in_play_range了:

int is_pkt_in_play_range(AVFormatContext* ic, AVPacket* pkt) {
    if (duration == AV_NOPTS_VALUE) //如果当前流无法计算总时长,按无限时长处理
        return 1;

    //计算pkt相对stream位置
    int64_t stream_ts = get_pkt_ts(pkt) - get_stream_start_time(ic, pkt->stream_index);
    double stream_ts_s = ts_as_second(stream_ts, ic, pkt->stream_index);

    //计算pkt相对ic位置
    double ic_ts = stream_ts_s - get_ic_start_time(ic);

    //是否在时间范围内
    return ic_ts <= ((double)duration / 1000000);
}

相信上述代码对于pkt_in_play_range的计算表达的很清楚了,那作者为何不按这种小函数的方式组织代码呢,估计是闲麻烦、啰嗦吧(在我看来,不赞同ffplay的代码风格。在一个几千行的文件里,还是可读性比较重要)。

至此,读线程的大部分代码都分析完成了。文章开头和每小节开头都是很好的总结了,这里不再重复。就吐槽下ffplay的代码缺点吧:

  1. 很多长函数,对于本来就对播放器一知半解的人要读懂是不小的挑战
  2. 过度简化的条件判断,导致即使理解这段代码,过一段时间还得再分析一次

其实很多不必要的简化,和对函数拆分对效率影响的担忧,编译器都可以优化,甚至比手戳的代码更搞笑。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值