《ffplay 读线程与解码线程分析:从初始化到 seek 操作,对比视频与音频解码的差异》

1 read-thread

1.1 初始化部分

1.分配. avformat_alloc_context 创建上下⽂

cpp 复制代码
ic = avformat_alloc_context();
    if (!ic) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }

2 ic->interrupt_callback.callback = decode_interrupt_cb;

ic->interrupt_callback.opaque = is;//这个是设置参数

cpp 复制代码
 //特定选项处理
    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;
    }

 /* 3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等 */
 //特定选项处理
    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;
    }
    /* 3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等 */
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    if (err < 0) {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }
    if (scan_all_pmts_set)
        av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);

    if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret = AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }
    is->ic = ic;    // videoState的ic指向分配的ic

int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);

4 avformat_find_stream_info(ic, opts);

cpp 复制代码
 if (find_stream_info) {
        AVDictionary **opts = setup_find_stream_info_opts(ic, codec_opts);
        int orig_nb_streams = ic->nb_streams;

        /*
         * 4.探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息
         * 调用该函数后得多的参数信息会比只调用avformat_open_input更为详细,
         * 其本质上是去做了decdoe packet获取信息的工作
         * codecpar, filled by libavformat on stream creation or
         * in avformat_find_stream_info()
         */
        err = avformat_find_stream_info(ic, opts);

该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说MPEG(⾥应该说ts更准确)(FLV⽂件也是需要读取packet 分析流信息)。⽽被读取⽤以分析流信息的数据可能被缓存,供av_read_frame时使⽤,在播放时并不会跳过这部分packet的读取。

5 检测是否指定播放起始时间

cpp 复制代码
/* 5. 检测是否指定播放起始时间 */
    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;
        // seek的指定的位置开始播放
        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);
        }
    }

6 查找查找AVStream

具体现在那个流进⾏播放我们有两种策略:

  1. 在播放起始指定对应的流
  2. 使⽤缺省的流进⾏播放
cpp 复制代码
// 选择相关流
int avformat_match_stream_specifier(AVFormatContext *s, AVStream *st, const char *spec);

//自动选择
av_find_best_stream

7 设置窗口大小

cpp 复制代码
   if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
        AVCodecParameters *codecpar = st->codecpar;
        //根据流和帧宽高比猜测视频帧的像素宽高比(像素的宽高比,注意不是图像的)    //AVRational 是 FFmpeg 中用于精确表示分数的结构体
        AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
        if (codecpar->width) {
            // 设置显示窗口的大小和宽高比
            set_default_window_size(codecpar->width, codecpar->height, sar);
        }
    }

根据流和帧宽⾼⽐猜测帧的样本宽⾼⽐。

由于帧宽⾼⽐由解码器设置,但流宽⾼⽐由解复⽤器设置,因此这两者可能不相

等。

此函数会尝试返回待显示帧应当使⽤的宽⾼⽐值。

基本逻辑是优先使⽤流宽⾼⽐(前提是值是合理的),其次使⽤帧宽⾼⽐。

这样,流宽⾼⽐(容器设置,易于修改)可以覆盖帧宽⾼⽐。

8 stream_component_open

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

一共有两个选择 一是通过id 二是 通过name

decoder_init 初始化解码器

d->avctx = avctx; 绑定对应的解码器上下⽂

d->queue = queue; 绑定对应的packet队列

d->empty_queue_cond = empty_queue_cond; 绑定VideoState的continue_read_thread,当

解码线程没有packet可读时唤醒read_thread赶紧读取数据

d->start_pts = AV_NOPTS_VALUE; 初始化start_pts

d->pkt_serial = -1; 初始化pkt_serial
decoder_start启动解码器

packet_queue_start 启⽤对应的packet 队列

SDL_CreateThread 创建对应的解码线程

1.2 创建packet队列部分

开始

|

|-- 1. 检测是否退出

| |

| |-- 如果退出

| | |

| | |-- 退出程序

| |

| |-- 如果未退出

| |

| |-- 2. 检测是否暂停/继续

| |

| |-- 如果暂停

| | |

| | |-- 保持暂停状态,不进行后续数据读取和处理

| |

| |-- 如果继续

| |

| |-- 3. 检测是否需要seek

| |

| |-- 如果需要seek

| | |

| | |-- 执行seek操作,调整播放位置

| |

| |-- 如果不需要seek

| |

| |-- 4. 检测video是否为attached_pic

| |

| |-- 如果是attached_pic

| | |

| | |-- 进行attached_pic相关处理(具体处理方式依需求而定)

| |

| |-- 如果不是attached_pic

| |

| |-- 5. 检测队列是否已经有足够数据

| |

| |-- 如果队列数据不足

| | |

| | |-- 等待数据到达,不进行后续操作

| |

| |-- 如果队列有足够数据

| |

| |-- 6. 检测码流是否已经播放结束

| |

| |-- 如果码流播放结束

| | |

| | |-- a. 检查是否循环播放

| | |

| | |-- 如果循环播放

| | | |

| | | |-- 重置播放状态,回到起始位置重新播放

| | |

| | |-- 如果不循环播放

| | | |

| | | |-- b. 检查是否自动退出

| | | |

| | | |-- 如果自动退出

| | | | |

| | | | |-- 退出程序

| | | |

| | | |-- 如果不自动退出

| | | | |

| | | | |-- 保持当前状态(例如显示播放结束画面等)

| |

| |-- 如果码流未播放结束

| |

| |-- 7. 使用av_read_frame读取数据包

| |

| |-- 8. 检测数据是否读取完毕

| |

| |-- 如果数据读取完毕

| | |

| | |-- 进行数据读取完毕相关处理(如更新播放状态等)

| |

| |-- 如果数据未读取完毕

| |

| |-- 9. 检测是否在播放范围内

| |

| |-- 如果不在播放范围内

| | |

| | |-- 进行超出播放范围相关处理(如跳过数据等)

| |

| |-- 如果在播放范围内

| |

| |-- 10. 将数据插入对应的队列

| |

| |-- 继续后续播放处理(如解码、渲染等)

note1 attached_pic

在 FFmpeg 中,attached_pic 是一种特殊的视频帧,用于表示媒体文件的封面图片(如电影海报、音乐专辑封面)。这些图片通常作为媒体文件的一部分被嵌入,与视频流中的普通帧不同。检测视频是否为 attached_pic 的目的是识别并处理这类封面图片。
note 2 检测队列是否已经有⾜够数据

⾳频、视频、字幕队列都不是⽆限⼤的,如果不加以限制⼀直往队列放⼊packet,那将导致队列占⽤⼤量的内存空间,影响系统的性能,所以必须对队列的缓存⼤⼩进⾏控制。PacketQueue默认情况下会有⼤⼩限制,达到这个⼤⼩后,就需要等待10ms,以让消费者------解码线程能有时间消耗。
note 3 检测码流是否已经播放结束

PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕
note 4 检测数据是否读取完毕

是使用空包
note 5 检测是否在播放范围内

播放器可以设置:-ss 起始位置,以及 -t 播放时⻓

1.3 退出线程处理

  1. 如果解复⽤器有打开则关闭avformat_close_input
  2. 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
    a. 发送的FF_QUIT_EVENT退出播放事件由event_loop()函数相应,收到FF_QUIT_EVENT后调⽤
    do_exit()做退出操作。
  3. 消耗互斥量wait_mutex

1.4 核心要点

1 seek 怎么做:

当is->seek_req为真,即有 seek 请求时,会进行 seek 操作。首先计算seek_target、seek_min和seek_max的值,然后调用avformat_seek_file 函数进行主要的 seek 操作。如果 seek 成功,需要清除 PacketQueue 的缓存,并放入一个flush_pkt,让 PacketQueue 的serial增 1,以区分 seek 前后的数据,同时该flush_pkt会触发解码器重新刷新解码器缓avcodec_flush_buffers(),避免解码出现马赛克。此外,还需要同步外部时钟。如果播放器本身处于 pause 状态,则执行step_to_next_frame(is),显示一帧后继续暂停。

cpp 复制代码
int avformat_seek_file(AVFormatContext *s, int stream_index, 
                       int64_t min_ts, int64_t ts, int64_t max_ts, 
                       int flags);
     

时间戳三元组 (min_ts, ts, max_ts) //核心公式 时间戳 = 秒数 × 时间基

ts(目标时间戳)

你希望 seek 到的理想时间点(以时间基为单位)。

例如:如果你想 seek 到 30 秒位置,且时间基为 AV_TIME_BASE,则 ts = 30 * AV_TIME_BASE。

min_ts(最小时间戳)

seek 操作允许的最小时间边界。

FFmpeg 不会 seek 到小于此值的位置,即使 ts 比它小。

max_ts(最大时间戳)

seek 操作允许的最大时间边界。

FFmpeg 不会 seek 到大于此值的位置,即使 ts 比它大。
为什么需要范围?

媒体文件的 seek 通常只能定位到关键帧(I 帧),因为只有关键帧可以独立解码。

例如:你想 seek 到 30.5 秒,但最近的关键帧在 29.8 秒和 31.2 秒,此时:
如果允许向前搜索(AVSEEK_FLAG_BACKWARD 未设置),FFmpeg 可能选择 31.2 秒。

如果强制向后搜索(设置 AVSEEK_FLAG_BACKWARD),FFmpeg 会选择 29.8 秒。
通过设置 min_ts 和 max_ts,你可以约束 FFmpeg 的选择范围,避免 seek 到太远的位置
int64_t seek_target = is->seek_pos; // 目标位置

int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;

int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;

// 前进seek seek_rel>0

//seek_min = seek_target - is->seek_rel + 2;

//seek_max = INT64_MAX;

// 后退seek seek_rel<0

//seek_min = INT64_MIN;

//seek_max = seek_target + |seek_rel| -2;

//seek_rel =0 鼠标直接seek

//seek_min = INT64_MIN;

//seek_max = INT64_MAX;

2 数据播放完毕和码流数据读取完毕

  • 数据播放完毕:判断播放已完成需要同时满足多个条件,包括不在暂停状态;音频未打开,或者打开了但解码已解完所有 packet,自定义的解码器serial等于 PacketQueue 的serial,并且 FrameQueue 中没有数据帧;视频未打开,或者打开了但解码已解完所有 packet,自定义的解码器serial等于 PacketQueue 的serial,并且 FrameQueue 中没有数据帧。只有 PacketQueue 和 FrameQueue 都消耗完毕,才是真正的播放完毕。
  • 码流数据读取完毕:当av_read_frame读取数据返回值ret < 0,并且(ret == AVERROR_EOF || avio_feof(ic->pb))且!is->eof时,表示码流数据读取完毕,此时会向对应音频、视频、字幕队列插入 "空包",通知解码器冲刷 buffer,将缓存的所有数据都解出来并取出。

3 循环播放 :在确认码流已播放结束的情况下,如果loop变量控制循环播放,当loop不等于 1(loop为 0 表示无限次循环,减 1 后大于 0 也允许循环 ),则将文件 seek 到起始位置,起始位置不一定是从头开始,具体要看用户是否指定了起始播放位置,通过stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);实现。

4 指定播放位置:可以通过ffplay -ss设置起始时间来指定播放位置,时间格式为hh:mm:ss,例如ffplay -ss 00:00:30 test.flv则是从 30 秒的起始位置开始播放。在代码中,检测是否指定播放起始时间,如果指定时间则通过avformat_seek_file函数 seek 到指定位置。

2 音视频解码线程分析

ffplay的解码线程独⽴于数据读线程,并且每种类型的流(AVStream)都有其各⾃的解码线程,如:

video_thread⽤于解码video stream;

audio_thread⽤于解码audio stream;

subtitle_thread⽤于解码subtitle stream。

解码器相关函数(decoder 为 ffplay 自定义、重新封装,avcodec 由 ffmpeg 提供)

cpp 复制代码
typedef struct Decoder {
AVPacket pkt;
PacketQueue *queue; // 数据包队列
AVCodecContext *avctx; // 解码器上下⽂
int pkt_serial; // 包序列
int finished; // =0,解码器处于⼯作状态;=⾮0,解码器处于空闲状态
int packet_pending; // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状
态
SDL_cond *empty_queue_cond; // 检查到packet队列空时发送 signal缓存read_thread读取数据
int64_t start_pts; // 初始化时是stream的start time
AVRational start_pts_tb; // 初始化时是stream的time_base
int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts
时则使⽤next_pts进⾏推算
AVRational next_pts_tb; // next_pts的单位
SDL_Thread *decoder_tid; // 线程句柄
} Decoder;

api

  • 初始化解码器
    函数:void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond);
  • 启动解码器
    函数:int decoder_start(Decoder *d, int (*fn)(void *), const char thread_name, void arg)
  • 解帧
    函数:int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub);
  • 终止解码器
    函数:void decoder_abort(Decoder *d, FrameQueue *fq);
  • 销毁解码器
    函数:void decoder_destroy(Decoder *d);
    使用方法

启动解码线程

调用 decoder_init()

调用 decoder_start()

2.1 视频解码

cpp 复制代码
static int video_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();  // 分配解码帧
    double pts;                 // pts
    double duration;            // 帧持续时间
    int ret;
    //1 获取stream timebase
    AVRational tb = is->video_st->time_base; // 获取stream timebase
    //2 获取帧率,以便计算每帧picture的duration
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);



    if (!frame)
        return AVERROR(ENOMEM);

    for (;;) {  // 循环取出视频解码的帧数据
        // 3 获取解码后的视频帧
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;   //解码结束, 什么时候会结束
        if (!ret)           //没有解码得到画面, 什么情况下会得不到解后的帧
            continue;

AVRational av_guess_frame_rate(AVFormatContext *ctx, AVStream *stream, AVFrame *frame); 猜测帧率

线程的总体流程很清晰:

  1. 获取stream timebase,以便将frame的pts转成秒为单位
  2. 获取帧率,以便计算每帧picture的duration
  3. 获取解码后的视频帧,具体调⽤get_video_frame()实现
  4. 计算帧持续时间和换算pts值为秒
  5. 将解码后的视频帧插⼊队列,具体调⽤queue_picture()实现
  6. 释放frame对应的数据

get_video_frame()

cpp 复制代码
static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;
    // 1. 获取解码后的视频帧
    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) {
        return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1
    }

    if (got_picture) {
        // 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
            dpts = av_q2d(is->video_st->time_base) * frame->pts;    //计算出秒为单位的pts

        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);

        if (framedrop>0 || // 允许drop帧
                (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))//非视频同步模式
        {
            if (frame->pts != AV_NOPTS_VALUE) { // pts值有效
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) &&     // 差值有效
                        fabs(diff) < AV_NOSYNC_THRESHOLD && // 差值在可同步范围呢
                        diff - is->frame_last_filter_delay < 0 && // 和过滤器有关系
                        is->viddec.pkt_serial == is->vidclk.serial && // 同一序列的包
                        is->videoq.nb_packets) { // packet队列至少有1帧数据
                    is->frame_drops_early++;
                    printf("%s(%d) diff:%lfs, drop frame, drops:%d\n",
                           __FUNCTION__, __LINE__, diff, is->frame_drops_early);
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}
  1. 调⽤ decoder_decode_frame 解码并获取解码后的视频帧;
  2. 分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊Frame队列)

decoder_decode_frame

  1. 持续获取解码帧(处理多帧情况)
    解码器通过循环调用 avcodec_receive_frame() 获取解码后的帧,确保处理以下情况:
  • 多帧解码:单个 Packet 可能解码出多个 Frame(如 B 帧序列),需循环读取直至返回 AVERROR(EAGAIN)。

  • 流连续性校验:每次读取前检查 d->queue->serial == d->pkt_serial,确保处理的是同一播放序列的连续数据。若序列不一致,说明发生了 seek 或其他中断,需丢弃旧数据。
    2. 获取新 Packet 并处理序列变更

    通过 packet_queue_get() 获取新的 Packet,该操作可能阻塞等待数据:

  • 序列校验:若获取的 Packet 的 serial 与当前解码器的 pkt_serial 不一致,说明数据流已中断(如 seek 后),需丢弃该 Packet 并继续获取。

  • 空队列通知:当 PacketQueue 为空时,发送 empty_queue_cond 条件信号,唤醒读线程继续读取数据(对应 read_thread 中的 SDL_CondWait())。

  1. 提交 Packet 到解码器
    将校验后的 Packet 通过 avcodec_send_packet() 提交至解码器,触发解码流程:
    错误处理:若发送失败(如解码器资源不足),需释放 Packet 并等待下次机会。
    状态更新:成功发送后,更新解码器的 pkt_serial 为当前 Packet 的 serial,确保后续帧校验一致性。

queue_picture()

cpp 复制代码
static int queue_picture(VideoState *is, AVFrame *src_frame, double pts,
                         double duration, int64_t pos, int serial)
{
    Frame *vp;

#if defined(DEBUG_SYNC)
    printf("frame_type=%c pts=%0.3f\n",
           av_get_picture_type_char(src_frame->pict_type), pts);
#endif

    if (!(vp = frame_queue_peek_writable(&is->pictq))) // 检测队列是否有可写空间
        return -1;      // 请求退出则返回-1
    // 执行到这步说已经获取到了可写入的Frame
    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;

    set_default_window_size(vp->width, vp->height, vp->sar);

    av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据转移到dst中,并复位src。
    frame_queue_push(&is->pictq);   // 更新写索引位置
    return 0;
}

queue_picture 的代码很直观:

⾸先 frame_queue_peek_writable 取FrameQueue的当前写节点;

然后把该拷⻉的拷⻉给节点(struct Frame)保存

再 frame_queue_push ,"push"节点到队列中。唯⼀需要关注的是,AVFrame的拷⻉是通过

av_frame_move_ref 实现的,所以拷⻉后 src_frame 就是⽆效的了

2.2 视频解码流程 要点分析

1 . flush_pkt的作⽤
强制解码器输出所有缓冲帧

当调用 avcodec_send_packet(NULL) 时,解码器会认为输入流已结束,并将内部缓存的所有待输出帧(如 B 帧队列)全部输出。

在 seek 或切换流时重置解码器状态

当用户执行 seek 或切换音视频流时,需要丢弃解码器中旧的缓冲数据,避免显示过时帧:

2 Decoder的packet_pending和pkt的作⽤
pkt

  • 作用:存储当前待发送给解码器的 AVPacket。
  • 生命周期:
    从 PacketQueue 中获取一个新的 AVPacket。
    通过 avcodec_send_packet() 发送给解码器。
    发送成功后,释放该 AVPacket。

packet_pending

  • 作用:标记 pkt 中是否有未发送的数据包。
  • 应用场景:
    当 avcodec_send_packet() 因解码器临时繁忙(返回 AVERROR(EAGAIN))而失败时,将 packet_pending 置为真,保留当前 pkt 以便下次重试。例如:

3 解码流程:avcodec_receive_frame-> packet_queue_get-> avcodec_send_packet

开始

├── 1. 检查解码器是否有剩余帧

│ │

│ ├── 调用 avcodec_receive_frame()

│ │ ├── 成功(返回0)→ 处理帧并重复步骤1

│ │ └── 失败(返回AVERROR(EAGAIN))→ 继续步骤2

├── 2. 获取新 Packet

│ │

│ ├── 检查 serial 是否匹配(是否发生 seek)

│ │ ├── 不匹配 → 丢弃 Packet 并重复步骤2

│ │ └── 匹配 → 继续步骤3

└── 3. 发送 Packet 到解码器

└── 回到步骤1

设计意图

  • 最大化解码效率:
    通过优先调用 avcodec_receive_frame(),确保解码器缓冲区中的所有帧都被及时处理,避免积压。
  • 应对多帧解码:
    一个 Packet 可能解码出多个 Frame(如 H.264 的 B 帧组),循环调用
  • receive_frame 可确保处理所有帧。
    处理解码器延迟:
    某些编解码器(如 AAC、H.264)有内部延迟,即使没有新 Packet 输入,仍可能输出残留帧。

2.3 ⾳频解码线程

cpp 复制代码
static int audio_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();  // 分配解码帧
    Frame *af;

    int got_frame = 0;  // 是否读取到帧
    AVRational tb;      // timebase
    int ret = 0;

    if (!frame)
        return AVERROR(ENOMEM);

    do {
        // 1. 读取解码帧
        if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
            tb = (AVRational){1, frame->sample_rate};   // 设置为sample_rate为timebase


                // 2. 获取可写Frame
                if (!(af = frame_queue_peek_writable(&is->sampq)))  // 获取可写帧
                    goto the_end;
                // 3. 设置Frame并放入FrameQueue
                af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                af->pos = frame->pkt_pos;
                af->serial = is->auddec.pkt_serial;
                af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

                av_frame_move_ref(af->frame, frame);
                frame_queue_push(&is->sampq);

#if CONFIG_AVFILTER
                if (is->audioq.serial != is->auddec.pkt_serial)
                    break;
            }
            if (ret == AVERROR_EOF) // 检查解码是否已经结束,解码结束返回0
                is->auddec.finished = is->auddec.pkt_serial;
#endif
        }
    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
the_end:
#if CONFIG_AVFILTER
    avfilter_graph_free(&is->agraph);
#endif
    av_frame_free(&frame);
    return ret;
}

差异一:

在 ffplay 中,视频线程(video_thread())和音频线程对时间基(timebase)的处理确实存在差异,这主要源于音视频同步的复杂性和不同的处理逻辑。以下是详细解释:

  1. 视频线程(video_thread())使用 stream->time_base 的原因**
    视频帧的时间戳(PTS)直接决定了其显示时间,因此:
  • 精确同步 :视频线程需要基于原始流的时间基计算每一帧的显示时间(pts * stream->time_base),以确保与音频同步。
  • 帧显示逻辑 :视频帧的显示时间(vp->pts)在入队前已转换为 stream->time_base,因此解码后直接使用该时间基进行同步计算。

示例代码

c 复制代码
// 视频帧入队前的时间戳转换
vp->pts = av_frame_get_best_effort_timestamp(frame);
vp->pts *= av_q2d(stream->time_base);  // 转换为秒
  1. 音频线程不直接使用 stream->time_base 的原因**
    音频同步更为复杂,主要依赖于以下因素:
  • 音频时钟(audio_clock :音频播放的实时进度,以 AV_TIME_BASE 为单位。
  • 音频缓冲区:音频数据通常以块(packet)形式解码,但播放时需要连续的样本流,因此需要更精细的时间控制。
  • 重采样需求:音频可能需要重采样以匹配输出设备的采样率,这会改变实际播放时间。

处理逻辑

  1. 音频线程解码后,计算音频帧的结束时间(audio_clock),单位为 AV_TIME_BASE
  2. 视频同步时,通过比较视频帧的 PTS(基于 stream->time_base)与 audio_clock(基于 AV_TIME_BASE)来调整显示时机。

示例代码

c 复制代码
// 更新音频时钟(单位:AV_TIME_BASE)
is->audio_clock = af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;
is->audio_clock *= AV_TIME_BASE;  // 转换为 AV_TIME_BASE 单位
  1. 关键差异总结**

    | 维度 | 视频线程 | 音频线程 |

    |----------------|----------------------------------|----------------------------------|

    | 时间基 | stream->time_base | AV_TIME_BASE |

    | 核心目标 | 帧精确显示时间 | 连续播放与实时时钟维护 |

    | 同步方式 | 基于视频 PTS 与音频时钟比较 | 基于音频缓冲区和系统时钟 |

    | 复杂度 | 较低(直接显示) | 较高(重采样、缓冲区管理) |

  2. 为什么设计不同?

  • 音频连续性:音频播放需要保持连续性,任何微小的时间误差都会导致卡顿或变调,因此使用更稳定的全局时间基。
  • 视频灵活性:视频帧可以通过丢帧、重复帧等方式调整显示时间,对时间基的精度要求相对较低。
  • 简化计算 :音频时钟作为主时钟(默认模式),使用 AV_TIME_BASE 可以避免频繁的时间基转换,提高性能。
  1. 总结
    视频线程使用 stream->time_base 是为了直接处理原始视频帧的显示时间,而音频线程使用 AV_TIME_BASE 是为了维护稳定的主时钟,确保音视频同步的精确性和连续性。这种差异是基于音视频特性和同步需求的优化设计。

视频解码线程和音频解码线程在ffplay中确实存在一些关键差异,除了时间基的不同外,主要体现在以下几个方面:

总结对比表

特性 视频解码线程 音频解码线程
解码API 封装函数 get_video_frame() 直接调用 decoder_decode_frame()
持续时间计算 基于帧率(av_guess_frame_rate() 基于采样率和样本数
同步策略 丢帧/重复帧 硬件时钟驱动播放
缓冲区大小 较小(3帧) 较大(9帧)
错误容忍度 较低(遇到错误易退出) 较高(持续尝试解码)
后处理 格式转换(YUV→RGB) 重采样(适配输出设备)

核心差异原因

  • 实时性要求

    音频对实时性要求更高,任何延迟或卡顿都会明显影响体验,因此采用更简单直接的处理流程。

  • 视觉容忍度

    人眼对视频的短暂丢帧或重复帧相对不敏感,因此视频线程可以更灵活地调整同步策略。

  • 数据特性

    音频数据是连续的流式数据,而视频数据是离散的帧,处理逻辑天然不同。

2.4 音视频滤镜

在 ffplay 中,视频和音频滤镜的处理差异源于其数据特性和应用场景的不同。以下是两者的核心差异:

1. 默认启用状态

  • 视频滤镜

    默认启用,即使未指定滤镜参数,也会通过 空滤镜(null filter) 确保数据一致性。
    目的:统一处理流程,简化代码逻辑。

  • 音频滤镜

    默认禁用,需通过命令行参数(如 -af "volume=0.5")显式启用。
    原因:音频重采样通常由解码器直接处理,无需额外滤镜。

2. 滤镜配置与初始化
视频滤镜

  • 通过 configure_video_filters() 配置,依赖 avfilter_graph_parse2() 构建滤镜图。

  • 必须包含 buffer(输入)和 buffersink(输出)两个特殊滤镜。

  • 示例配置

    c 复制代码
    // 默认空滤镜配置
    const char *filters = "null";

音频滤镜

  • 通过 configure_audio_filters() 配置,需指定输入/输出格式和采样率。

  • 更复杂,可能涉及多阶段处理(如重采样、效果调整)。

  • 示例配置

    c 复制代码
    // 带重采样的滤镜链
    "aresample=44100,volume=0.5"

3. 时间基处理

  • 视频

    滤镜处理后,时间基通常保持为 stream->time_base,与原始视频流一致。

  • 音频

    滤镜可能改变采样率,导致时间基变化,需重新计算 PTS。

    c 复制代码
    tb = (AVRational){1, frame->sample_rate};  // 基于新采样率的时间基

4. 处理流程与API调用
视频线程

  1. 通过 get_video_frame() 获取解码帧。
  2. 帧数据流经滤镜图,输出处理后的帧。
  3. 处理后的帧入队等待显示。
c 复制代码
// 视频滤镜处理流程
ret = get_video_frame(is, frame);  // 内部包含滤镜处理
if (ret > 0) {
    // 处理后的帧入队
    ret = queue_picture(is, frame, ...);
}

音频线程

  1. 解码后的数据直接入队,不经过滤镜。
  2. 音频播放时,通过 audio_decode_frame() 动态应用滤镜(如需要)。
c 复制代码
// 音频滤镜处理流程(简化)
if (got_frame) {
    // 解码帧直接入队
    af->frame = frame;
    frame_queue_push(&is->sampq);
}

// 音频播放时应用滤镜(在 audio_decode_frame() 中)
if (is->agraph) {
    // 滤镜处理
    av_buffersrc_add_frame_flags(...)
    av_buffersink_get_frame(...)
}

5. 性能与延迟

  • 视频滤镜

    可能引入显著延迟(如复杂的缩放或特效),但人眼对视频延迟容忍度较高。

  • 音频滤镜

    对实时性要求极高,复杂滤镜可能导致卡顿,因此默认保持最简处理。

6. 典型应用场景

场景 视频滤镜 音频滤镜
格式转换 缩放、色彩空间转换(如 YUV→RGB) 重采样(如 48kHz→44.1kHz)
效果调整 亮度/对比度、裁剪、去噪 音量调节、均衡器、3D音效
默认行为 强制通过空滤镜 仅在显式请求时启用

总结对比表

特性 视频滤镜 音频滤镜
默认启用 是(含空滤镜) 否(需显式配置)
配置函数 configure_video_filters() configure_audio_filters()
处理阶段 解码后立即处理 播放时动态处理
时间基影响 通常不变 可能因重采样改变
延迟容忍度 较高 极低(需实时播放)
典型滤镜链 buffer → scale → buffersink abuffer → aresample → volume

核心差异原因

  • 数据特性

    视频是离散帧,可容忍一定延迟;音频是连续流,对实时性要求极高。

  • 处理复杂度

    视频滤镜通常更复杂(如画面缩放),而音频滤镜多为线性处理(如重采样)。

相关推荐
鱼鱼说测试8 分钟前
Jenkins+Python自动化持续集成详细教程
开发语言·servlet·php
早睡身体好~2 小时前
【lubancat】鲁班猫4实现开机后自动播放视频
音视频·linux开发
小幽余生不加糖3 小时前
电路方案分析(二十二)适用于音频应用的25-50W反激电源方案
人工智能·笔记·学习·音视频
胡耀超3 小时前
DataOceanAI Dolphin(ffmpeg音频转化教程) 多语言(中国方言)语音识别系统部署与应用指南
python·深度学习·ffmpeg·音视频·语音识别·多模态·asr
byxdaz3 小时前
FFmpeg QoS 处理
ffmpeg
网硕互联的小客服5 小时前
Apache 如何支持SHTML(SSI)的配置方法
运维·服务器·网络·windows·php
苏琢玉5 小时前
如何让同事自己查数据?写一个零依赖 PHP SQL 查询工具就够了
mysql·php
shix .9 小时前
bilibili视频总结
音视频
全栈软件开发10 小时前
PHP域名授权系统网站源码_授权管理工单系统_精美UI_附教程
开发语言·ui·php·php域名授权·授权系统网站源码
mit6.82411 小时前
ubuntu远程桌面很卡怎么解决?
linux·ubuntu·php