
1. 时间基到底在解决什么问题但凡用FFmpeg做过音视频开发的人几乎都绕不开一个让人头大的概念——时间基Time Base。我第一次接触这玩意儿的时候看着代码里到处出现的AVRational、time_base、pts、dts脑子里全是问号为什么不能统一用一个秒或者毫秒来表示时间搞这么复杂到底图什么后来踩的坑多了才慢慢理解这套设计的精妙之处。简单来说时间基就是FFmpeg用来表示时间的一套“刻度系统”。你可以把它想象成不同手表上的刻度——有的表最小刻度是秒有的是毫秒有的是微秒。当你拿两块不同刻度的表来对时间不换算就会出大问题。FFmpeg内部要处理各种各样的时间来源摄像头采集的原始帧率可能是29.97fps这种非整数音频采样率可能是44100Hz容器格式里封装的时间戳单位又各不相同。如果全部统一成秒用浮点数表示精度损失和累积误差会非常严重。所以FFmpeg选择用有理数分数来表示时间基每个时间戳都是一个整数乘以时间基就得到真实的时间值。这个设计带来的核心好处有三个精度可控、运算高效、适配灵活。精度可控是因为有理数运算可以精确表示1/3这种无限循环小数运算高效是因为整数运算比浮点运算快且没有累积误差适配灵活是因为不同的流、不同的容器可以各自定义自己的时间基互不干扰。理解时间基是掌握FFmpeg音视频同步、转码、封装、裁剪等所有核心操作的基础。不管你是做播放器、做转码服务、还是做音视频编辑工具时间基搞不清楚音画不同步、时间戳错乱、seek不准这些问题就会一直缠着你。这篇文章我会从实际开发的角度把时间基的来龙去脉、换算方法、常见坑点全部拆开讲清楚适合有一定FFmpeg基础但被时间基困扰的开发者。2. 核心概念拆解与底层逻辑2.1 AVRational结构体有理数的表达方式FFmpeg里时间基的定义非常朴素就是一个结构体typedef struct AVRational { int num; // 分子 int den; // 分母 } AVRational;这个结构体表示的就是分数num/den。比如{1, 1000}表示千分之一秒也就是毫秒{1, 90000}表示九万分之一秒这是MPEG-TS流常用的时间基。为什么用int而不是int64_t因为时间基的分母通常不会超过几十万分子一般是1或者1001这种小数值int的范围完全够用。但时间戳本身用的是int64_t因为长时间的视频时间戳数值会很大比如90kHz时间基下一小时的视频时间戳就是90000 * 3600 324000000虽然int32勉强能装下但为了安全统一用64位。这里有个容易混淆的点时间基本身是一个分数但它表示的是“一个时间单位是多少秒”。比如{1, 1000}的时间基时间戳值为500表示的时间就是500 * (1/1000) 0.5秒。这个换算关系必须刻在脑子里。2.2 不同层级的时间基从流到帧FFmpeg里时间基不是只有一个而是分层级的每一层有自己的含义和用途。搞混层级是新手最常犯的错误。AVStream.time_base这是流级别的时间基表示该流中时间戳的单位。对于视频流它通常等于1/framerate比如25fps的视频时间基就是{1, 25}。对于音频流它通常等于1/sample_rate比如44100Hz的音频时间基就是{1, 44100}。这个时间基在封装格式的头部定义用于描述该流中所有时间戳的含义。AVCodecContext.time_base这是编解码器上下文的时间基表示编码器或解码器内部使用的时间单位。对于解码器这个值通常由解码器自己设置对于编码器需要我们在打开编码器之前设置好。它和AVStream.time_base可能不一致转码时需要做转换。AVFrame.time_base新版本已废弃改用AVFrame的pts配合AVCodecContext的time_base表示帧时间戳的时间基。在旧版本FFmpeg中AVFrame有自己的time_base字段新版本中这个字段被移除了帧的时间戳统一用AVCodecContext.time_base来解释。AVPacket.time_base同样在新版本中行为有变化表示包时间戳的时间基。历史上AVPacket也有自己的time_base后来被废弃统一用AVStream.time_base来解释。注意FFmpeg 4.0之后的版本对时间基的处理做了不少调整很多旧教程里的写法在新版本上会报错或者行为不一致。如果你看的资料比较老一定要对照官方文档确认当前版本的行为。2.3 PTS和DTS时间戳的两个维度PTSPresentation Time Stamp是显示时间戳告诉播放器这一帧应该在什么时间被显示出来。DTSDecoding Time Stamp是解码时间戳告诉解码器这一帧应该在什么时间被解码。对于没有B帧的视频PTS和DTS是相等的解码顺序和显示顺序一致。但一旦有B帧情况就复杂了B帧需要参考后面的帧才能解码所以它的解码顺序在参考帧之前但显示顺序在参考帧之后。这就导致DTS和PTS出现差异。用一个具体例子说明假设视频帧的显示顺序是I帧、B帧、B帧、P帧。由于B帧依赖后面的P帧实际解码顺序变成I帧、P帧、B帧、B帧。那么帧类型显示顺序解码顺序PTSDTSI帧1100B帧2323B帧3434P帧4211可以看到PTS和DTS的顺序完全不同。播放器根据PTS来显示帧解码器根据DTS来解码帧。如果搞混了这两个概念音视频同步就会出大问题。2.4 时间基的换算av_rescale_q的核心作用不同时间基之间换算FFmpeg提供了专门的函数int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);这个函数的作用是把时间戳a从时间基bq转换到时间基cq。内部实现是a * bq / cq但为了避免溢出和精度损失FFmpeg用了更复杂的算法。举个实际例子假设你有一个视频流AVStream.time_base是{1, 25}某个帧的PTS是50。现在你要把这个帧送给编码器编码器的AVCodecContext.time_base是{1, 1000}。那么转换后的PTS就是av_rescale_q(50, {1, 25}, {1, 1000}) 50 * (1/25) / (1/1000) 50 * 1000 / 25 2000验证一下50个1/25秒就是2秒2000个1/1000秒也是2秒换算正确。还有一个常用的函数是av_q2d把AVRational转成doubledouble av_q2d(AVRational a);这个函数返回a.num / (double)a.den。比如av_q2d({1, 25})返回0.04表示每个时间单位是0.04秒。这个函数在需要做浮点运算时很方便但要注意浮点精度问题不要用它来做精确的时间戳计算。3. 实操中的时间基处理与避坑指南3.1 转码场景下的时间基转换全流程转码是时间基问题最集中的场景。一个典型的转码流程是解封装 → 解码 → 编码 → 封装。每个阶段的时间基都可能不同需要仔细处理。解封装阶段从AVFormatContext中读取AVPacket此时AVPacket的pts和dts是基于AVStream.time_base的。比如一个MP4文件视频流的time_base可能是{1, 12800}音频流的time_base可能是{1, 44100}。解码阶段把AVPacket送给解码器得到AVFrame。此时AVFrame的pts是基于AVCodecContext.time_base的。对于大多数解码器这个时间基和AVStream.time_base一致但不绝对。编码阶段把AVFrame送给编码器之前需要确保帧的pts是基于编码器的time_base的。如果编码器的time_base和帧的pts时间基不一致必须用av_rescale_q转换。封装阶段编码器输出的AVPacket其pts是基于编码器time_base的。写入输出文件之前需要把pts转换到输出流的time_base。整个流程中最容易出错的是编码阶段。很多人在打开编码器时随便设一个time_base结果导致输出文件的时间戳完全错乱。正确的做法是编码器的time_base应该设置为1/framerate并且要保证帧的pts按照这个时间基递增。// 设置编码器时间基 codec_ctx-time_base (AVRational){1, fps}; codec_ctx-framerate (AVRational){fps, 1}; // 转换帧的pts到编码器时间基 frame-pts av_rescale_q(frame-pts, decoder_ctx-time_base, codec_ctx-time_base);这里有个细节framerate和time_base是倒数关系。framerate是{25, 1}表示25fps对应的time_base是{1, 25}。设置的时候不要搞反了。3.2 音视频同步中的时间基统一音视频同步是时间基问题的另一个重灾区。音频和视频有各自的时间基要同步播放必须统一到同一个时间基准上。常见的做法是选择一个参考时钟通常是音频时钟因为音频对抖动更敏感然后把视频帧的PTS转换到音频的时间基上进行比较。// 假设音频时间基是audio_tb视频时间基是video_tb // 视频帧的pts是video_pts int64_t video_pts_in_audio_tb av_rescale_q(video_pts, video_tb, audio_tb); // 获取当前音频时钟 int64_t audio_clock get_audio_clock(); // 计算差值 int64_t diff video_pts_in_audio_tb - audio_clock; // 根据差值决定是等待还是丢帧 if (diff THRESHOLD) { // 视频超前等待 av_usleep(diff); } else if (diff -THRESHOLD) { // 视频落后丢帧 drop_frame(); }这里的THRESHOLD通常取几十毫秒太小会导致频繁调整太大会导致同步不准。具体取值要根据实际场景调优。实操心得音视频同步的阈值不要设得太死建议做成动态的。刚开始同步时用大阈值快速收敛接近同步后用小阈值精细调整。我在一个直播项目里用固定阈值结果网络抖动时画面频繁卡顿后来改成动态阈值才解决。3.3 seek操作中的时间基陷阱seek是时间基问题最隐蔽的地方。av_seek_frame函数的签名是int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);这里的timestamp是基于哪个时间基的答案是基于AVStream.time_base。但很多人会误以为是基于AV_TIME_BASE即{1, 1000000}微秒。AV_TIME_BASE是FFmpeg定义的一个全局时间基值为1000000表示微秒。当stream_index传-1时timestamp才是基于AV_TIME_BASE的。如果指定了具体的流timestamp就是基于该流的time_base。// 方式一指定流timestamp基于流的time_base int64_t target 10 * stream-time_base.den / stream-time_base.num; // 10秒 av_seek_frame(fmt_ctx, stream_index, target, AVSEEK_FLAG_BACKWARD); // 方式二不指定流timestamp基于AV_TIME_BASE int64_t target_us 10 * AV_TIME_BASE; // 10秒 av_seek_frame(fmt_ctx, -1, target_us, AVSEEK_FLAG_BACKWARD);这两种方式的结果可能不同因为方式一会seek到该流时间戳对应的位置方式二会seek到全局时间对应的位置。实际开发中建议用方式一因为更精确。还有一个坑seek之后解码器里可能还有缓存的帧这些帧的PTS可能小于seek目标。如果不处理会出现seek后画面跳回旧位置的问题。正确的做法是seek之后调用avcodec_flush_buffers清空解码器缓存。3.4 常见问题速查表问题现象可能原因排查方法解决方案音画不同步音视频时间基未统一打印两路PTS对比用av_rescale_q统一到同一时间基时间戳跳跃编码器time_base设置错误检查codec_ctx-time_base设置为1/framerateseek不准timestamp时间基搞错确认av_seek_frame的参数含义指定流索引并用流时间基播放速度异常framerate和time_base搞反检查两者是否互为倒数修正设置输出文件时长错误封装时未转换时间基对比编码器和输出流时间基写入前用av_rescale_q转换首帧时间戳不为0未做时间基偏移打印首帧PTS减去首帧PTS做归零处理4. 进阶技巧与实战经验4.1 自定义时间基的选择策略虽然FFmpeg允许任意有理数作为时间基但实际选择时有几个推荐值视频常用{1, 90000}是MPEG-TS的标准时间基兼容性最好{1, 1000}是毫秒级适合大多数场景{1, framerate}是帧级最直观但framerate为29.97这种非整数时会有精度问题。音频常用{1, sample_rate}是采样级最精确{1, 1000}是毫秒级适合和视频统一。选择时间基的核心原则是精度够用即可不要盲目追求高精度。时间基分母越大时间戳数值越大溢出风险越高。比如用{1, 1000000}做时间基一小时的视频时间戳就是36亿接近int32上限必须用int64。而用{1, 90000}一小时才3.24亿安全得多。踩过的坑我曾经在一个项目里为了“精确”把所有时间基都设成{1, 1000000}结果处理长视频时时间戳溢出画面直接花屏。后来改成{1, 90000}就正常了。精度不是越高越好够用就行。4.2 处理可变帧率视频的时间基可变帧率VFR视频是时间基处理的高级话题。固定帧率视频每帧间隔相同时间基可以简单设为1/framerate。但VFR视频每帧间隔不同不能用固定时间基。处理VFR视频的正确做法是保持原始时间基不变不要强行转换成固定帧率。编码时把AVCodecContext.time_base设为一个足够精细的值比如{1, 90000}然后每帧的PTS按照实际显示时间设置。// VFR视频处理 codec_ctx-time_base (AVRational){1, 90000}; // 每帧的pts根据实际时间计算 frame-pts av_rescale_q(actual_pts, src_time_base, codec_ctx-time_base);如果强行把VFR转成CFR会出现重复帧或丢帧画面会卡顿或跳跃。除非业务明确要求CFR否则保持VFR是更好的选择。4.3 时间基相关的调试技巧时间基问题往往很隐蔽调试时需要一些技巧。我常用的方法有这几个打印时间基和PTS在关键节点打印时间基和PTS对比是否符合预期。printf(stream tb: %d/%d, codec tb: %d/%d, pts: %ld\n, stream-time_base.num, stream-time_base.den, codec_ctx-time_base.num, codec_ctx-time_base.den, frame-pts);用av_q2d转成秒来验证把时间戳转成秒直观判断是否正确。double pts_seconds frame-pts * av_q2d(codec_ctx-time_base); printf(pts in seconds: %f\n, pts_seconds);检查时间戳单调性正常情况下PTS应该单调递增B帧的DTS可能不单调但PTS一定单调。如果发现PTS回退说明时间基处理有问题。用ffprobe验证输出转码完成后用ffprobe检查输出文件的时间戳信息确认是否符合预期。ffprobe -v quiet -show_streams -show_frames output.mp4这些方法看起来简单但实际调试时非常有效。我遇到过一个问题音视频同步始终差几百毫秒打印时间基才发现音频流的时间基被错误地设成了{1, 48000}而不是{1, 44100}导致所有时间戳都偏了。4.4 新旧版本FFmpeg的时间基差异FFmpeg 4.0是一个分水岭之前和之后的时间基处理有不少差异。如果你维护的项目跨版本这些差异必须注意。AVFrame.time_base4.0之前AVFrame有time_base字段4.0之后被移除。旧代码里frame-time_base的写法在新版本会编译报错。替代方案是用AVCodecContext.time_base来解释帧的PTS。AVPacket.time_base类似地AVPacket的time_base字段也被废弃。新版本中AVPacket的PTS统一用AVStream.time_base解释。av_stream_get_side_data这个函数在4.0之后行为有变化影响某些时间基相关的元数据获取。编码器时间基设置新版本对编码器时间基的校验更严格如果设置不合理会直接报错。旧版本可能只是警告。经验之谈升级FFmpeg版本时时间基相关的代码一定要重点测试。我见过太多项目升级后音视频不同步最后发现是时间基API变了。建议在升级前先写一个最小复现用例验证时间基处理逻辑在新版本上的行为。5. 从源码角度看时间基的实现5.1 av_rescale_q的内部实现av_rescale_q看起来简单内部实现却有不少讲究。核心逻辑在av_rescale_rnd函数里它要解决的核心问题是如何在避免溢出的前提下尽可能精确地计算a * b / c。直接算a * b可能溢出所以FFmpeg用了分步计算和位运算的技巧。具体实现比较复杂但作为使用者我们只需要知道这个函数会尽量保证精度但极端情况下仍可能溢出。如果时间戳特别大建议先用av_rescale_q_rnd指定舍入方式或者手动做除法。// 安全的时间基转换 int64_t safe_rescale(int64_t ts, AVRational from, AVRational to) { // 先约分减少溢出风险 int64_t a ts * from.num * to.den; int64_t b from.den * to.num; return a / b; }这个简化版在大多数情况下够用但不如av_rescale_q精确。生产环境还是建议用官方函数。5.2 时间基在封装格式中的体现不同的封装格式对时间基的处理不同理解这些差异有助于排查问题。MP4MP4的mvhd和tkhd原子里有timescale字段相当于时间基的分母。视频轨的timescale通常是帧率的整数倍音频轨通常是采样率。MP4的时间戳是整数单位是1/timescale秒。MPEG-TSMPEG-TS用90kHz的时间基PTS和DTS都是33位整数。这个时间基是固定的所有流都用同一个。MKVMKV用纳秒级时间基精度很高。时间戳是int64单位是纳秒。FLVFLV用毫秒级时间基精度较低。时间戳是int32单位是毫秒。转码时如果输入输出格式不同时间基转换是必须的。比如MP4转FLV时间基从1/timescale转到毫秒精度会损失可能导致音视频轻微不同步。这时候需要在转换时做舍入处理尽量减少误差。5.3 时间基与帧率的关系再梳理帧率和时间基的关系是初学者最容易搞混的地方这里再梳理一遍。帧率framerate是每秒显示的帧数单位是fps。时间基time_base是每个时间单位对应的秒数。两者互为倒数关系time_base 1 / framerate framerate 1 / time_base对于25fps的视频framerate {25, 1}time_base {1, 25}。每帧的PTS间隔是1以时间基为单位对应真实时间0.04秒。对于29.97fps的视频framerate {30000, 1001}time_base {1001, 30000}。每帧的PTS间隔是1对应真实时间1001/30000 ≈ 0.03337秒。注意这里不能用{1, 30}近似否则长时间播放会累积误差。关键点非整数帧率必须用有理数精确表示。29.97不是3023.976不是24这些差异在短视频里看不出来但长视频里会累积成秒级的偏差。6. 实战案例一个完整的时间基处理流程6.1 场景描述假设我们要做一个视频转码工具输入是一个MP4文件输出是FLV文件。输入视频是25fps音频是44100Hz。输出要求视频保持25fps音频保持44100Hz。这个场景涉及的时间基转换有MP4视频流时间基 → 解码器时间基 → 编码器时间基 → FLV视频流时间基音频类似。每一层都要正确处理。6.2 关键代码实现// 打开输入 AVFormatContext *in_fmt NULL; avformat_open_input(in_fmt, input_path, NULL, NULL); avformat_find_stream_info(in_fmt, NULL); // 打开输出 AVFormatContext *out_fmt NULL; avformat_alloc_output_context2(out_fmt, NULL, NULL, output_path); // 视频流处理 int video_index av_find_best_stream(in_fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); AVStream *in_video_stream in_fmt-streams[video_index]; // 创建输出视频流 AVStream *out_video_stream avformat_new_stream(out_fmt, NULL); out_video_stream-time_base (AVRational){1, 25}; // FLV视频时间基 // 编码器设置 AVCodecContext *video_enc_ctx avcodec_alloc_context3(video_encoder); video_enc_ctx-time_base (AVRational){1, 25}; video_enc_ctx-framerate (AVRational){25, 1}; // 转码循环 AVPacket *pkt av_packet_alloc(); AVFrame *frame av_frame_alloc(); while (av_read_frame(in_fmt, pkt) 0) { if (pkt-stream_index video_index) { // 解码 avcodec_send_packet(video_dec_ctx, pkt); while (avcodec_receive_frame(video_dec_ctx, frame) 0) { // 时间基转换解码器时间基 → 编码器时间基 frame-pts av_rescale_q(frame-pts, video_dec_ctx-time_base, video_enc_ctx-time_base); // 编码 avcodec_send_frame(video_enc_ctx, frame); while (avcodec_receive_packet(video_enc_ctx, pkt) 0) { // 时间基转换编码器时间基 → 输出流时间基 pkt-pts av_rescale_q(pkt-pts, video_enc_ctx-time_base, out_video_stream-time_base); pkt-dts av_rescale_q(pkt-dts, video_enc_ctx-time_base, out_video_stream-time_base); pkt-duration av_rescale_q(pkt-duration, video_enc_ctx-time_base, out_video_stream-time_base); // 写入输出 av_interleaved_write_frame(out_fmt, pkt); } } } av_packet_unref(pkt); }这段代码里有三次时间基转换每一次都不能少。第一次是解码器到编码器第二次是编码器到输出流第三次是duration的转换容易被忽略。6.3 常见错误与修正错误一忘记转换duration。AVPacket的duration字段也需要转换时间基否则播放器计算帧时长会出错。错误二编码器时间基和输出流时间基不一致。有些开发者编码器用{1, 25}输出流用{1, 1000}但忘记转换导致时间戳差了40倍。错误三音频时间基处理不当。音频的时间基通常是采样率转换时要注意采样数和时间戳的区别。一个音频帧可能包含1024个采样时间戳间隔是1024不是1。错误四首帧PTS不为0。有些输入文件首帧PTS不是0直接转码会导致输出文件开头有一段空白。需要在转码前记录首帧PTS后续所有帧减去这个值做归零。// 首帧PTS归零 static int64_t first_pts AV_NOPTS_VALUE; if (first_pts AV_NOPTS_VALUE) { first_pts frame-pts; } frame-pts - first_pts;这个处理看似简单但实际项目中非常必要。我做过一个批量转码工具输入文件来源五花八门不做归零处理的话输出文件的时间戳乱七八糟播放器seek都会出问题。6.4 验证与测试转码完成后验证时间基处理是否正确我通常做这几步用ffprobe查看输出文件的流信息确认时间基和帧率符合预期。播放输出文件检查音视频是否同步seek是否准确。对比输入输出文件的时长误差应该在毫秒级以内。如果是长视频检查中间位置和结尾位置的时间戳是否正常。# 查看流信息 ffprobe -v error -show_entries streamindex,codec_type,time_base,r_frame_rate -of defaultnoprint_wrappers1 output.flv # 查看时长 ffprobe -v error -show_entries formatduration -of defaultnoprint_wrappers1 output.flv如果发现时长偏差超过1秒基本可以确定是时间基处理有问题。常见原因是帧率转换时的舍入误差累积或者音频采样数计算错误。7. 时间基处理的通用原则与个人体会7.1 五条通用原则经过这么多项目的打磨我总结了时间基处理的五条通用原则基本能覆盖90%的场景原则一明确每个时间戳的时间基。拿到一个时间戳第一件事是确认它基于哪个时间基。不确定就打印出来看不要猜。原则二跨层必转换。数据在不同层级之间传递时时间基必须转换。解码到编码要转编码到封装要转音频到视频要转。原则三保持单调递增。PTS必须单调递增DTS在无B帧时也必须单调递增。发现回退立即排查。原则四首帧归零。输出文件的首帧PTS应该归零避免播放器处理异常。原则五精度够用即可。不要盲目追求高精度时间基够用就行避免溢出和性能问题。7.2 个人踩坑体会时间基这个问题看文档觉得简单实际动手全是坑。我印象最深的一次是一个直播转码项目音视频同步始终差200毫秒左右排查了两天才发现是音频编码器的时间基设成了{1, 48000}而实际采样率是44100。这个错误很隐蔽因为48000和44100很接近短时间看不出来长时间才累积成明显偏差。还有一次是做视频剪辑工具seek总是不准有时候偏几秒。后来发现是av_seek_frame的timestamp用错了时间基传的是微秒但函数期望的是流时间基。改成正确的时间基后seek精度立刻提升到帧级。这些经历让我明白一个道理时间基问题不会自己消失只会以各种奇怪的现象表现出来。音画不同步、seek不准、时长错误、播放卡顿背后都可能是时间基处理不当。与其出了问题再排查不如在写代码时就把每个时间基转换都写清楚、测明白。7.3 后续扩展方向时间基处理掌握之后可以进一步深入几个方向一是研究FFmpeg源码里av_rescale_q的具体实现理解它如何处理溢出和精度二是研究不同封装格式的时间基规范比如MP4的timescale、MKV的纳秒时间基三是研究可变帧率视频的时间基处理这是进阶话题四是研究硬件编解码器的时间基处理硬件编码器对时间基的要求可能和软件编码器不同。这些方向每一个都值得深入但前提是基础的时间基转换要扎实。我见过太多人跳过基础直接搞高级特性结果问题一堆。时间基就是FFmpeg开发的基本功练好了后面的事情会顺很多。