ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FFmpeg时间戳转换详解:av_packet_rescale_ts原理与实战

FFmpeg时间戳转换详解:av_packet_rescale_ts原理与实战 1. av_packet_rescale_ts到底在干什么先说结论av_packet_rescale_ts是FFmpeg里用来把AVPacket的时间戳从一个时间基切换到另一个时间基的函数。凡是做过FFmpeg开发、处理过音视频封装转换、流拷贝、切片、倍速播放的人基本都绕不开它。我第一次被这个函数坑到是在做一个转封装工具的时候。场景很简单输入是一个MP4文件我需要把里面的H.264视频流和AAC音频流原封不动地提取出来重新封装成TS格式输出。当时心想转封装嘛不就是解复用再复用AVPacket直接拷过去不就行了结果一做就翻车——输出文件播放时画面卡顿、音画不同步用ffprobe一查时间戳乱成一团。排查到最后问题就出在时间基上。MP4内部用的时间基是1/90000这种高精度时钟而TS流用的是90kHz时钟转封装的过程中如果不把AVPacket的pts/dts做一次时间基换算输出流的时间戳就是错的。而av_packet_rescale_ts就是干这件事的。这个函数声明很简单就一行void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);输入一个AVPacket告诉它源时间基和目的时间基函数内部直接把pkt-pts、pkt-dts、pkt-duration全部换算成目的时间基下的新值。看起来人畜无害但用的时候坑不少什么时候该调、什么时候不该调、调了之后原始数据有没有影响、对AAC这种有延迟补偿的格式要怎么处理——这些才是真正值钱的经验。这篇文章就把我实际开发中关于av_packet_rescale_ts的完整理解写出来从原理到用法从踩坑到排查尽量一次说透。2. 时间基和时间戳理解这个函数的前置知识2.1 什么是时间基要理解av_packet_rescale_ts必须先理解时间基。时间基time_base在FFmpeg里是一个AVRational结构体本质就是一个分数表示“一个时间单位等于多少秒”。typedef struct AVRational { int num; // 分子 int den; // 分母 } AVRational;比如time_base为{1, 90000}就表示每个时间单位是1/90000秒。时间戳pts的值是整数它乘以时间基就得到了实际的时间秒。你可以把时间基想象成一把尺子的刻度。MP4的尺子上一格是1/90000秒TS流的尺子上一格是1/90000秒——等等这两个看着一样啊但实际情况是不同封装格式、不同编码器、不同容器里尺子刻度可能完全不同。有的用1/1000毫秒级有的用1/90000有的用采样率相关的值。当数据从一个容器换到另一个容器刻度对不上时间就乱了。2.2 pts、dts、duration的含义AVPacket里有几个关键时间字段ptsPresentation Time Stamp显示时间戳告诉解码器这个包应该在什么时候显示。dtsDecoding Time Stamp解码时间戳告诉解码器这个包应该在什么时候解码。duration这个包持续的时长。pos在文件中的字节位置严格说不是时间相关但常被一起讨论。对于大多数视频帧pts和dts是相同的即不存在B帧重排但存在B帧时dts会小于等于pts。av_packet_rescale_ts会同步处理这三者不会出现只换算pts忘了dts的尴尬情况。2.3 为什么需要重新打时间戳再回到我那个转封装的例子。输入MP4的时间基可能是{1, 90000}或{1, 12800}这类值输出TS容器希望的时间基是{1, 90000}。如果二者刚好一致那确实不需要换算。但问题在于每个流stream的time_base可能不同编码器产生的原始时间戳也可能基于采样率。比如AAC音频编码器的时间基往往和采样率相关如44100Hz而封装成MP4时容器层的时间基又变成了{1, 90000}或别的值。从编码器拿到AVPacket后它的pts是基于编码器时间基的你直接塞给muxermuxer会按照自己的时间基去解释结果就是时间戳被错误放大或缩小。av_packet_rescale_ts做的事情翻译成人话就是把同一时刻用另一把尺子上的刻度来表示。数值会变但代表的实际时间点不变。3. 核心细节解析这个函数的几个关键点3.1 函数内部到底怎么算的av_packet_rescale_ts的换算核心就是分数乘法。假设源时间基是tb_src {num_src, den_src}目的时间基是tb_dst {num_dst, den_dst}原始pts值为pts_src那么换算后的pts值大概等于pts_dst pts_src * (den_src * num_dst) / (num_src * den_dst)这是典型的比例换算。FFmpeg实现时使用了av_rescale_q或类似的函数它能避免中间结果溢出并且尽量保持精度。这也是为什么你几乎不需要自己写换算逻辑——封装好的函数在极端大数场景下都做了处理。但注意FFmpeg的换算不只是简单的四舍五入它内部有舍入策略AV_ROUND_NEAR_INF等保证pts/dts之间的相对关系尽量不被破坏。所以即便源和目的时间基非常接近也不建议完全跳过这个函数因为这里有对边界情况的处理。3.2 修改的是包本身不是流av_packet_rescale_ts直接修改传入的AVPacket把里面的pts、dts、duration字段改为新时间基下的值。它不会动packet里的数据data、size也不会重新分配内存。这是个就地操作所以调用前最好确认这个包是否还需要用旧时间基的值如果需要提前备份。我当时踩过一个很蠢的坑从demuxer读出的AVPacket先送去做字幕处理字幕处理需要原始时间处理完又回调了av_packet_rescale_ts结果后面再想拿原始时间戳对比发现已经变了。后来学乖了要么在rescale之前把旧值保存到局部变量要么尽量在需要原始值的逻辑之后再调。3.3 源的time_base怎么取调用这个函数时源时间基tb_src怎么确定是新手最容易搞错的地方。如果你是从AVFormatContextdemuxer读出的packet那么源时间基应该用packet所在的AVStream的time_base也就是AVStream *stream fmt_ctx-streams[packet-stream_index]; av_packet_rescale_ts(packet, stream-time_base, dst_time_base);如果你是从解码器拿到的packetdecode后重编码场景源时间基应该用AVCodecContext的time_baseav_packet_rescale_ts(packet, codec_ctx-time_base, dst_time_base);混淆这两个来源是最典型的错误。可能短期看不出问题但换一个输入文件、换一个编码器就露馅了。3.4 目标time_base怎么选目标时间基的选择取决于你要把数据送到哪里如果送给muxer封装目标是输出容器的流时间基通常可以直接用output_stream-time_base。但FFmpeg在avformat_write_header时可能调整流的time_base所以更稳妥的方式是让muxer自己处理或者先写header再获取最终的time_base。如果送给filter滤镜目标时间基通常是AVFilterLink的time_base。如果送给编码器目标时间基通常和编码器的frame_rate、time_base设置有关。在实际工程里很多人喜欢统一用一个高精度时间基如{1, 1000000}微秒级来减少精度损失然后再转成最终目标。这种做法可行但注意多做一次换算就多一次精度损失和潜在风险如果不是特殊需求不建议中间层过度转换。4. 实操过程三种典型场景下的正确用法4.1 场景一转封装时的时间基处理上面说过转封装是最常见的应用场景。核心流程是用avformat_open_input打开输入文件。用avformat_find_stream_info获取流信息。为输出文件创建AVFormatContext和AVStream。循环读取输入AVPacket进行时间基换算然后写入输出。收尾。这里有一个关键决策究竟应该手动调av_packet_rescale_ts还是让muxer自动处理如果你使用的FFmpeg版本较新并且通过av_interleaved_write_frame写包那么部分封装器内部其实会自动做转换。但为了代码的明确性和跨版本兼容性绝大多数生产代码仍然会显式调用av_packet_rescale_ts。我的建议是显式调用不要依赖隐式行为。参考代码如下while (av_read_frame(ifmt_ctx, pkt) 0) { AVStream *in_stream ifmt_ctx-streams[pkt.stream_index]; AVStream *out_stream ofmt_ctx-streams[pkt.stream_index]; // 把输入流的时间基换算到输出流的时间基 av_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base); pkt.pts av_rescale_q_rnd(pkt.pts, in_stream-time_base, out_stream-time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.dts av_rescale_q_rnd(pkt.dts, in_stream-time_base, out_stream-time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.duration av_rescale_q(pkt.duration, in_stream-time_base, out_stream-time_base); pkt.pos -1; av_interleaved_write_frame(ofmt_ctx, pkt); av_packet_unref(pkt); }注意到没有我这里先调了av_packet_rescale_ts又手动用av_rescale_q_rnd算了一遍看起来重复了这其实是我在实际工程里见过的一种冗余写法源自某些旧版本代码。正确的做法只用一处——要么用av_packet_rescale_ts简简单单搞定要么手动分别换算。混着用虽然结果可能一致但会让代码很迷惑。我建议直接用av_packet_rescale_ts然后这行av_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base);后面不需要再手动算pts/dts/duration。但注意pkt.pos -1这个操作是必要的因为不同容器里的字节位置没有意义留着旧值可能误导调试工具。4.2 场景二使用滤镜时的处理如果你在转码过程中用了滤镜比如缩放、字幕叠加时间戳处理要特别小心。滤镜框架内部有自己的时间基通常滤镜链的输入link会告诉你期望的时间基。操作顺序建议是从demuxer拿到AVPacket。先不做时间基转换直接送到decoder解码得到AVFrame。AVFrame送到滤镜前确保frame的pts基于正确的time_base。一般解码后的frame的pts对应codec_ctx-time_base。通过av_buffersrc_add_frame_flags送入滤镜时可以设置AV_BUFFERSRC_FLAG_PUSH等标志滤镜内部会把时间基转到它自己的参考系。滤镜输出AVFrame通过av_buffersink_get_frame取出此时frame的pts对应滤镜输出的时间基。编码前可能需要把frame的pts从滤镜时间基转换到编码器时间基。编码器输出AVPacket这时packet的pts/dts通常已经基于编码器time_base再用av_packet_rescale_ts转成输出容器time_base。这个链路里av_packet_rescale_ts出现在第6步。前面的frame时间戳处理需要的是av_frame_set_pts或直接对frame-pts做换算不是这个packet函数的事。很多新手在滤镜场景下容易多做一步明明已经通过滤镜把时间戳整理好了又对解码前的packet做了一次rescale结果把时间搞坏。记住滤镜链中处理的是frame不是packetpacket层面的rescale只发生在滤镜链之外。4.3 场景三流拷贝stream copy特殊注意事项流拷贝stream copy也就是不重新编码直接复制编码数据是转封装最常见的手段。这种场景下时间戳处理也是必须的而且比全转码更需要注意因为没有编码器帮你校准时间戳。有些封装格式对时间戳的起始值有要求。比如某些格式要求第一帧的pts/dts从0开始或者要求从某个固定值开始。如果你的源文件第一帧不是0直接写入可能导致播放器行为异常。处理方式是先记录第一帧的时间戳偏移量在rescale之后减去这个偏移static int64_t first_pts AV_NOPTS_VALUE; if (first_pts AV_NOPTS_VALUE) { first_pts pkt.pts; } av_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base); if (first_pts ! AV_NOPTS_VALUE) { pkt.pts - av_rescale_q(first_pts, in_stream-time_base, out_stream-time_base); pkt.dts - av_rescale_q(first_pts, in_stream-time_base, out_stream-time_base); }这个技巧在做一些特殊规格输出时特别有用例如生成某些要求首帧时间戳为0的流。4.4 参数选择速查表为了方便查阅我把不同场景下的源/目标时间基选择整理成表格使用场景源时间基tb_src目标时间基tb_dst转封装/demuxer→muxer输入AVStream的time_base输出AVStream的time_base编码器输出→muxerAVCodecContext的time_base输出AVStream的time_base滤镜输出→编码器滤镜输出AVFrame的时间基编码器期望的时间基统一中间时间基方案原始包时间基自定义中间时间基如{1, 1000000}5. 常见问题与排查技巧实录5.1 问题一转封装后音画不同步表现播放时声音正常画面滞后或提前几百毫秒或者画面正常声音不对。排查思路用ffprobe检查输出文件每个流的time_base确认容器层的时间基是否是预期值。对比每个流第一帧的pts看是否在同一时间起点上。如果视频第一帧pts0音频第一帧pts0那起点没问题。检查duration字段是否合理。如果duration错误会导致帧与帧之间的时间间隔不均播放器估算时间就会出现累积偏差。常见的根因有两个忘了调av_packet_rescale_ts直接把源时间基的pts写入了输出容器。源时间基取错了。比如输入流实际是VFR可变帧率但你误以为time_base固定导致换算表错误。实操心得排查时间戳问题最有力的工具是ffprobe显示每个帧的时间戳ffprobe -show_frames -select_streams v -show_entries framepts,pkt_dts,pkt_duration,pkt_pts_time output.ts把输出和源文件的帧时间戳做对比很快能找到哪一段出问题。5.2 问题二转换后pts非单调递增表现播放器进度条跳来跳去或者某些帧被丢弃。原因可能出在B帧处理上。如果你只处理了pts而没注意dts的顺序或者源流本身时间戳就有乱序转换后可能仍然非单调。FFmpeg的muxer内部有interleaving机制在写包时会缓冲和排序但前提是你传入的包的时间戳要基本合理。过度依赖muxer排序不是好习惯。实操心得对于有B帧的视频流转换后最好校验一下dts是否严格递增。如果发现dts跳跃检查是否在rescale时用了错误的舍入方式导致相邻帧的dts差值变成了0或负值。5.3 问题三duration转换异常导致播放器显示时长不对表现播放器总时长显示几十个小时或只有几秒。这通常是因为AVPacket里的duration在rescale后变得过大或过小。看下duration字段的计算如果源duration是0rescale后还是0。如果源duration基于源time_base但目标time_base差异极大比如从{1, 90000}转到{1, 1000}duration会缩小90倍这是正常的但要确保没有溢出或截断。实操心得有一个细节很多人忽略——av_packet_rescale_ts内部对duration的处理和pts/dts略有不同。它单独用av_rescale_q进行换算但不会做AV_ROUND_PASS_MINMAX这类特殊标记处理。如果你需要极端的精度控制可以考虑手动只对duration做一次av_rescale_q并指定舍入方式。5.4 问题四误差累积长时间转封装后时间戳出现漂移越到后面偏移越明显。原因多次rescale每次都有舍入误差累积后不可忽视。尤其是从高精度向低精度转换再转回高精度比如{1, 1000000}转成{1, 1000}再转回{1, 1000000}每次都会损失精度。实操心得我的习惯是能一步换算就不要两步。源时间基和目标时间基直接用一次av_packet_rescale_ts搞定。如果确实需要中间时间基尽量保持中间时间基和高精度侧一致避免低精度环节成为误差放大器。5.5 常见错误速查表错误操作后果正确做法从demuxer拿到packet后用codec_ctx的time_base做源时间戳错乱不同步源用stream-time_base转封装时漏调rescale播放器时间错乱显式调用rescale后还手动改pts且改错方向双重换算只用一种换算方式对滤镜后的packet重复rescale时间戳被错误缩放确认packet当前时间基再决定没处理首帧偏移输出流首帧pts非0特殊播放器异常记录并减去首帧偏移6. 更具工程深度的进阶用法6.1 自定义时间基转换封装在某些工程里你可能不想在每个读包循环里都写一遍rescale逻辑。我建议封装一个工具函数void rescale_packet_for_stream(AVPacket *pkt, AVStream *src_stream, AVStream *dst_stream) { av_packet_rescale_ts(pkt, src_stream-time_base, dst_stream-time_base); pkt-pos -1; }这样能减少重复代码。如果项目里有多个转封装入口还可以统一在这个函数里打日志调试时非常方便。6.2 同步处理其他字段av_packet_rescale_ts不会修改AVPacket的pos字段。前面说了容器的pos信息在跨封装时没有意义但也有些场景想在调试时保留原pos。这时注意区分需求如果是单纯调试可以用stderr打印原始值如果是生产代码建议统一将pos置为-1避免下游误用。另外AVPacket的flags字段如AV_PKT_FLAG_KEY不会因为时间基转换而改变这个不需要额外处理。6.3 结合AV_NOPTS_VALUE的判断有些包可能没有有效的pts或dts例如某些附加数据包、字幕包。调用av_packet_rescale_ts前最好判断一下if (pkt.pts ! AV_NOPTS_VALUE) { pkt.pts av_rescale_q(pkt.pts, src_tb, dst_tb); } if (pkt.dts ! AV_NOPTS_VALUE) { pkt.dts av_rescale_q(pkt.dts, src_tb, dst_tb); }av_packet_rescale_ts内部对AV_NOPTS_VALUE其实有处理但显式判断可以让你更清楚哪些包没有时间戳排查时少走弯路。6.4 变帧率流的时间基处理VFR流variable frame rate的帧间隔不固定。这时time_base本身不代表实际帧率只是时间戳的刻度。rescale的公式依然成立因为转换的是“刻度”而不是“帧率”。但VFR流在转封装后如果目标容器不支持VFR就麻烦了。比如某些容器假设恒定帧率VFR源写入后播放器可能按固定帧率解释导致时长错误。这不是av_packet_rescale_ts能解决的需要在封装层面处理比如用AVFormatContext的avoid_negative_ts、max_interleave_delta等参数配合调整。7. 我的几点实操心得最后分享一些个人经验都是代码之外的东西但对减少踩坑很有帮助。第一先把时间基的来龙去脉搞清楚再动手写代码。我见过太多同事一头扎进读包写包的循环里出了问题才回头研究时间基。时间基是FFmpeg里最基础也最容易忽视的概念花半小时搞清楚能省一整天的排查时间。第二调试时间戳问题时写一个小工具专门打印packet的时间信息。不需要多复杂就是遍历输出每个包的stream_index、pts、dts、duration、time_base。有了这个工具你能快速定位问题是出在demuxer、rescale、还是muxer阶段。第三多看ffprobe的输出。很多时间戳问题用ffprobe扫一眼输出文件就能看出端倪。比如音频流时间戳乱跳、视频流首帧pts不是0这些一眼就能发现问题所在。第四不同封装格式之间时间基差异是常态。MP4、TS、MKV、FLV各有各的习惯时间基。不要凭经验猜测写代码时从stream-time_base动态获取不要硬编码。第五转封装时如果不需要重新编码尽量走stream copy路径。但stream copy不代表时间戳可以乱来。反而因为没有编码器这个“天然校准器”时间戳处理更要小心。av_packet_rescale_ts本身很小但牵涉的知识面不小。把时间基、时间戳、封装格式的关系理清楚这个函数用起来就是顺手的事。希望这篇文章能把你在时间戳处理上的坑都填平。
返回列表