ffmpeg分割视频导致A/V不同时开始的问题

更新:2023-11-21 更新追记1,在最后。

昨天马娘 live 出现了花井美春不小心喊出 producerさん的名场面,当时恰好在录制的我就顺手截了一段出来发到了群里。没想到这段视频后来传了好多地方,包括有人发B站

不过这就引出一个问题,该视频在B站如果用app观看,会发现有明显的音画不同步(我用网页反而不会)——而且这个问题如果用 Chrome / Firefox 播放(或者一切基于Chromium的框架,例如 Electron,包含 Discord 等)也都会重现。那么这是为什么呢?

其实这个问题并不复杂,视频本身也没本质问题,只是这些播放器的实现不够标准导致的。

这个视频有两个比较特殊的特性:一个是其 两条 stream 的 start time 不一致:视频是0.444,音频则是0。不过这个本身一般大多数播放器都能对付,问题不大。

另外一个则比较罕见,也是导致 chromium bug 的根本原因。让我们提取最前面的 packets 展示一下(这里的v是我自己写的py小脚本):

>v p producer-san.mp4
Extracted 2069 packets from producer-san.mp4.
Video packets: 781
Audio packets: 1288
audio -1.004667 KD_
audio -0.983333 KD_
audio -0.962000 KD_
audio -0.940667 KD_
audio -0.919333 KD_
audio -0.898000 KD_
audio -0.876667 KD_
audio -0.855333 KD_
audio -0.834000 KD_
audio -0.812667 KD_
audio -0.791333 KD_
audio -0.770000 KD_
audio -0.748667 KD_
audio -0.727333 KD_
audio -0.706000 KD_
audio -0.684667 KD_
audio -0.663333 KD_
audio -0.642000 KD_
audio -0.620667 KD_
audio -0.599333 KD_
audio -0.578000 KD_
audio -0.556667 KD_
audio -0.535333 KD_
audio -0.514000 KD_
audio -0.492667 KD_
audio -0.471333 KD_
audio -0.450000 KD_
audio -0.428667 KD_
audio -0.407333 KD_
audio -0.386000 KD_
audio -0.364667 KD_
audio -0.343333 KD_
audio -0.322000 KD_
audio -0.300667 KD_
audio -0.279333 KD_
audio -0.258000 KD_
audio -0.236667 KD_
audio -0.215333 KD_
audio -0.194000 KD_
audio -0.172667 KD_
audio -0.151333 KD_
audio -0.130000 KD_
audio -0.108667 KD_
audio -0.087333 KD_
audio -0.066000 KD_
audio -0.044667 KD_
audio -0.023333 KD_
audio -0.002000 K__
audio 0.019333 K__
audio 0.040667 K__
audio 0.062000 K__
audio 0.083333 K__
audio 0.104667 K__
audio 0.126000 K__
audio 0.147333 K__
audio 0.168667 K__
audio 0.190000 K__
audio 0.211333 K__
audio 0.232667 K__
audio 0.254000 K__
audio 0.275333 K__
audio 0.296667 K__
audio 0.318000 K__
audio 0.339333 K__
audio 0.360667 K__
audio 0.382000 K__
audio 0.403333 K__
video 0.444000 K__
audio 0.424667 K__
video 0.510733 ___
audio 0.446000 K__
audio 0.467333 K__
video 0.477367 ___
audio 0.488667 K__
audio 0.510000 K__
video 0.577467 ___
audio 0.531333 K__
video 0.544100 ___

可以看到,其音频的 packets 大概前面有几十个,是有负的 PTS 外加 flag D (discarded) 的。也就是说,正确处理的情况下,这些 packets 应该被播放器舍弃,而不播放。另外由于第一个 V 包是0.444秒才有,所以最前面会有0.44秒是只有声音没有视频的(一般播放器表现为第一视频帧静止画面)。但是等到两者都开始播放后,音画是同步的。

当然这些 pakcets的元数据都是 ffmpeg 处理过后生成的了,这个特性的具体实现,其实是MP4的一个非常少用到的功能:edit list (moov.trak.edts.elst)。通过这个元数据,可以进行一些很高级的控制,除了跳过部分段落外,还可以实现重复播放某段、加速减速播放等等。

如果用 ffprobe -report producer-san.mp4 来生成一个详细报告,可以看到里面有如下语句:

[mov,mp4,m4a,3gp,3g2,mj2 @ 000001f4670fe840] Processing st: 0, edit list 0 - media time: -1, duration: 39960
[mov,mp4,m4a,3gp,3g2,mj2 @ 000001f4670fe840] Processing st: 0, edit list 1 - media time: 3003, duration: 2345400
[mov,mp4,m4a,3gp,3g2,mj2 @ 000001f4670fe840] Processing st: 1, edit list 0 - media time: 88160, duration: 1270704

不过,大多数播放器对 edit list 都没有很好的支持就是了。这个视频用本地播放器播放,虽然音画都同步,行为也不太一样:

  • MPV: 永远从第一个 video pakcet 开始播放,所以不只是前面的 -1 秒音频不会播放,提前开始的那0.44秒也不会播放。
  • PotPlayer: 从pts=0处开始播放音频,视频先静止帧0.44秒。
  • MPC-HC: 同 PotPlayer
  • MPC-BE: 完整播放所有packet,即先视频静止帧+播放前1.44秒音频,然后AV同时播放。

B站的压制我是没法很方便地测试啦,但是 Chromium 这边的表现则是,前面的有 D flag 的音频确实被扔掉了,音轨是正确地从 PTS=0 的 packets 开始播放,但是其视频流直到1秒多而不是0.44秒(没法准确确定是1秒还是1.44秒)才开始播放,自然音画就不同步了。可以看到,其对 PTS 的处理不正确。这点我已经汇报到官方的 issue tracker

问题视频产生的根源

但是这就引出一个更重要的话题:为什么会切出这种奇怪的视频?这么多年切 TS 视频,其实经常出现这种问题。不过因为不影响播放,其实我也没深究过。现在想来,虽然这个视频是完全”合法“的,但是为了兼容性显然我们还是想要最大程度规避这种现象。而且即使是正确播放,最前面0.4秒视频不动看着也不太舒服,所以为啥切出来的视频V/A两轨的开头时间会不一样呢?

我切视频都是用我的自己写的一个 CLI 交互小脚本,因为我经常给日本音番切片。我们这里切片并不需要特别准确(需求精确时我就用 TMPGEnc 了),所以 ffmpeg 的 stream copy 只能切到视频的关键帧,或者时间不是完全准确都是无关紧要的。其实切 TS 的其他乱七八糟的小问题也一大堆,我已经尽量在脚本里把各种情况都处理了,不过这里按下不表。总之,这里最后实际切片的命令如下(去掉了一些无关紧要的):

ffmpeg -ss 11:39:06.444 -i "XXX.ts" -t 0:00:29.192000 -c copy cut.mp4

这里我的 .ts 是在下载 m3u8 过程中即时二进制合并 segments 出来的一个大文件。我使用了 input seeking 的方式跳到我想要的时间戳,然后再在 output option 里加了一个长度来切片。

插播:简单科普 ffmpeg seeking

关于 ffmpeg 两种 seeking 如果不了解可以先看看官方 wiki 补课(有些内容稍显过时,问题不大)。简单来说,如果用 input seeking (也就是把相关参数填写在 -i 前面),会在读取该 input 的时候直接 seek 到这个位置,不会解码前面的内容,速度基本是瞬时的;如果用 output seeking (把相关参数填写到 -i 后面),则需要解码整个视频至此处,需要很长的时间,而且对于我这种前面有11小时内容的更是不现实。

注意这里解释一下我在网上经常看到有人误解的地方:对于正常的视频,无论用哪个 seeking,出来的结果都是精确的:并不是说只有 output seeking 才能精确到毫秒级时间戳。当你 transode 的时候,即使你用的是 input seeking,而且切割点不在关键帧上,ffmpeg也会自动先 seek 到上一个关键帧然后解码到你需要的帧处(这个行为由 -accurate_seek 这个来控制,默认是开启的,可以用 -noaccurate_seek 关闭),再进行 transcode。当然,如果是 stream copy (本文的议题),则就只能 seek 到最近的关键帧了,这个无论是用 input seeking 还是 output seeking 都是一样的, -accurate_seek 这个开关对于 stream copy 也是完全没有任何效果的。

所以我用 input seeking 来 -ss 的原因也很好理解了。至于为什么 -t 反而要用 output seeking,则是为了规避 ffmpeg 当年 seek MEPG-TS 格式的一个bug:当年如果在 input 同时用 -ss 和 -to/-t,会出现并不会在指定的 duration 或者结束时间戳结束的问题。不过这个 bug 在我汇报后过了几个月已经修复了,其实现在已经没有必要再这样,不过既然没有副作用就先不改了(另外注意,不要 -ss input seeking 但是 -to output seeking。当视频读取到 output 侧后,其时间戳会被重置,所以你的 -to 是从 ss 处重新开始计算的而不是原始视频的时间戳。对 -t 则没有区别)。

另外对于一般视频,其音频部分的 packets 可以理解为每个都是“关键帧”,也就是任意可分的。只有视频会有 GOP 的概念只能切割到关键帧,不能在任意 packet 处无损切割。再加上一般音频的包本来就比视频包短(本文中此视频音频包长度只有20ms),基本可以认为能任意位置无损切割了。

出问题的 input 简介

让我们回到正题。这个问题一言以蔽之,主要产生于 MPEG-TS 这个格式的问题上,尤其是 m3u8 直播时产出的 segment 的文件里。其实这个问题只需要取此次直播的任意一个 segment 即可实现,所以问题和我们后期 binary 合并过多个 segment 没有关系。我们以 index_4_6992.ts (下文重命名为 raw.ts)为例,其长度是6s。我们先按顺序罗列下他的所有 packets:

表格中第一列是类型(视频 or 音频),第二列是 PTS time,第三列是 DTS time,第四列是 flags。顺序是从左往右,从上往下。

可以看到,对于这种 livestream 的 segment,其 PTS 都是连续的所以并不会从零开始。这里问题不大。但是比较奇怪的是其 packets 的排列方式:可以看到在最前面有几十个 video 的 packets,然后是比较正常的两者交替进行 (interlaced),最后又变成有一大堆 audio 的 packets。

我们把所有的 time 都用 PTS/DTS的最小值 offset 一下,看起来更方便些:

可以看到,他是先包含了快2秒的视频 packets,然后是前 0.36秒的音频的 packets,然后又跟了0.3秒的视频 packets.. 以此类推。但是这样音频还是追不上视频,所以最后又一口气塞了快2秒的音频的 packets。

很显然,这些 packets 并不是按照 DTS (或 PTS) 排列的。如果你按照 DTS 重新排序,会变成:

这就和一个正常的视频没什么区别了。事实上,如果直接播放这个 segment,会发现没有任何问题,音画同步且同时都从最开始开始。我的理解是,正常播放器都有一个足够大的 buffer,而不是指望视频的 packets 一定会 DTS 单调增(事实上很多视频的DTS都不是单调增的)。这样他就会一次读取足够多的 pakcets 进去然后按照 DTS 解码、 PTS 顺序播放。

但是到了 ffmpeg 这里,作为 input,seeking这个(种)视频就会有各种问题。这个问题的具体表现形式对于不同的 output 封装形式还不太一样。接下来,让我们罗列下使用不同 seek + copy 或 transcode,对于此问题 input 会出现什么后果。

Input seek + stream copy

如果我 stream copy 到 mp4 容器:

ffmpeg -ss 00:00:03 -i raw.ts -c copy input_seeking_copy_tomp4.mp4 -y

Format start time: 0.0
Stream video start time: 1.004
Stream audio start time: 0.0
Earliest video packet pts time: 1.004
Earliest audio packet pts time: -1.010667

这就是万恶之源,上文提到的那种有负 PTS + discarded packets 的视频。DTS倒是单调增。

对于负 PTS 的问题,可以通过增加 -avoid_negative_ts make_zero 参数来解决:

ffmpeg -ss 00:00:03 -i raw.mp4 -c copy -avoid_negative_ts make_non_negative temp.mp4 -y

这样出来的视频就会和下面的MKV容器的结果一样。

如果是 copy 到 .ts 容器 (ffmpeg -ss 00:00:03 -i raw.ts -c copy input_seeking_copy.ts -y),output 则是

Format start time: 1.4
Stream video start time: 4.033333
Stream audio start time: 1.4
Earliest video packet pts time: 4.033333
Earliest audio packet pts time: 1.4

可以看到整个视频会有一个1.4的 start time,但是 video更晚在4。1.4 产生的原因SO有个问题提到了,可以通过 -muxdelay 0 来消除。另外注意,这个视频的DTS也不是单调增(在V和V或者A和A之间是,但是跨类型不是),但比 raw.ts 好多了。

如果是 copy 到 .mkv 容器:

Format start time: 0.0
Stream video start time: 2.633
Stream audio start time: 0.0
Earliest video packet pts time: 2.633
Earliest audio packet pts time: 0.0

容器 start time是0,视频要在2.6秒后才开始,DTS完美单调增*,没有负数 PTS。

*:这个视频的第一个 V packet 很奇怪地并没有DTS的数据(图中显示为0)。我理解是 MKV 容器第一帧视频必须是首先解码,所以可以默认为0?

我们把几种方法产生的视频具体包含的 packets 给 visualize 一下:

图中 audio 是交替颜色显示逐个 packets;视频则是按照GOP来交替显示。PTS 按照 raw 来对齐,有D flag的部分显示为红色。可以看到,视频都是只有最后一个GOP(从约4秒开始),但是音频都反而要比我们指定的地点提前不少:TS / MKV / 禁用了 edit list 的 MP4 都是提前了比音频提前2.6秒(比指定切割点提前1.6秒),而 MP4 如果不加 -avoid_negative_ts make_non_negative,和TS/MKV相比又短了些(比视频提前2秒,比切割点提前1秒,但是有D flag来丢弃到正好到切割点处),不是很懂。

顺便还可以看到对于 mkv 格式,因为其对时间戳的处理和其他容器不同(之前看过一次已经记不太清了,大概简单来说似乎是因为其他格式本质上类似于 time code 累加的形式,mkv则是对于每个 packet 都有定死的时间,然后考虑到舍入误差?),所以偶尔会出现相邻的两个 packet 没有完全连续,而是有 1~2个 time base 的间隔现象——尤其是对于NTSC的 29.97/23.974帧率的视频来说(上面的 plot 由于图像分辨率的问题(香农采样原理!)并不能把所有的细小间隔都显示出来,只是随机显示了几个)。

Input seek + transcode

因为问题是由于 seeking 导致的,所以即使 transcode 也会有问题。对于下列命令:

ffmpeg -ss 00:00:03 -i raw.ts -c:v libx264 -c:a aac input_seeking_encode.ts -y
ffmpeg -ss 00:00:03 -i raw.ts -c:v libx264 -c:a aac input_seeking_encode_tomp4.mp4 -y
ffmpeg -ss 00:00:03 -i raw.ts -c:v libx264 -c:a aac input_seeking_encode_tomkv.mkv -y

>v start input_seeking_encode.ts
Format start time: 1.4
Stream video start time: 2.422333
Stream audio start time: 1.4
Earliest video packet pts time: 2.422333
Earliest audio packet pts time: 1.4

>v start input_seeking_encode_tomp4.mp4
Format start time: 0.0
Stream video start time: 0.0
Stream audio start time: 0.0
Earliest video packet pts time: 0.0
Earliest audio packet pts time: -0.021333

>v start input_seeking_encode_tomkv.mkv 
Format start time: -0.021
Stream video start time: 1.001
Stream audio start time: -0.021
Earliest video packet pts time: 1.001
Earliest audio packet pts time: -0.021

可以看到,现在变成这样:

  • TS 容器:AV start time 依然不同,错1秒。
  • MP4 容器:看似一切都正常了,但是实际播放可以确认,只是前面几帧被 ffmpeg 默认给 duplicate 来实现 CFR 罢了。如果加上 -vsync 0 ,又变成
Format start time: 0.0
Stream video start time: 1.001
Stream audio start time: 0.0
Earliest video packet pts time: 1.001
Earliest audio packet pts time: -0.021333

这样子了。

  • MKV 容器:同上 vysnc=0 的情况。

也就是说,无论哪种情况,都是从原视频 audio 3秒处、视频4秒处(第三个GOP处)开始 encode 的。

而理论上,transcode 的情况下应该是可以 accurate seek 才对。介于音频正常,我们可以大概猜测这个 mpegts 的 input 的本质问题在于他导致 ffmpeg 没能正确判断切割点(t=3)的上一个 keyframe (t=2)在哪里,于是直接给切到下一个(t=4)去了。

Output seek + stream copy

那么,如果我们改用 output seeking,能否改善这个问题呢?

ffmpeg -i raw.ts -ss 00:00:03 -c copy output_seeking_copy.ts -y
ffmpeg -i raw.ts -ss 00:00:03 -c copy output_seeking_copy_tomp4.mp4 -y
ffmpeg -i raw.ts -ss 00:00:03 -c copy output_seeking_copy_tomkv.mkv -y
output_seeking_copy.ts
Format start time: 1.413333
Stream video start time: 2.404
Stream audio start time: 1.413333
Earliest video packet pts time: 2.404
Earliest audio packet pts time: 1.413333

output_seeking_copy_tomp4.mp4
Format start time: 0.013
Stream video start time: 1.004
Stream audio start time: 0.013
Earliest video packet pts time: 1.004
Earliest audio packet pts time: 0.013

start output_seeking_copy_tomkv.mkv
Format start time: 0.013
Stream video start time: 1.004
Stream audio start time: 0.013
Earliest video packet pts time: 1.004
Earliest audio packet pts time: 0.013

视频和音频起点不一致的问题依然存在,但是现在所有格式都会是固定的从原视频 audio 3秒处、视频4秒处(第三个GOP处)开始。这里对于MP4,即使不加 -avoid_negative_ts make_non_negative 也不会出现负的PTS了(output seeking 原理所致,TS 是重新计算的),所以 Chromium 也可以正确播放。

Output seek + transcode

如果使用 output seeking + 重编码,倒是可以完美解决:

ffmpeg -i raw.ts -ss 00:00:03 -c:v libx264 -c:a aac output_seeking_encode.ts -y
ffmpeg -i raw.ts -ss 00:00:03 -c:v libx264 -c:a aac output_seeking_encode_tomp4.mp4 -y
ffmpeg -i raw.ts -ss 00:00:03 -c:v libx264 -c:a aac output_seeking_encode_tomkv.mkv -y

>v start output_seeking_encode.ts
Format start time: 1.4454
Stream video start time: 1.466733
Stream audio start time: 1.4454
Earliest video packet pts time: 1.466733
Earliest audio packet pts time: 1.4454

>v start output_seeking_encode_tomp4.mp4
Format start time: 0.0
Stream video start time: 0.0
Stream audio start time: 0.0
Earliest video packet pts time: 0.0
Earliest audio packet pts time: -0.021333

>v start output_seeking_encode_tomkv.mkv
Format start time: -0.021
Stream video start time: 0.0
Stream audio start time: -0.021
Earliest video packet pts time: 0.0
Earliest audio packet pts time: -0.021

这些视频不但时间戳都正常,实际观看也可以确认,确实是视频音频同时开始,没有重复帧等问题。

Workaround

这里先强调一下,上面切出来的这些“有问题”的文件一个是 AV 开始点不同的问题,一个是MP4容器特有的 edit list 导致部分播放器无法正常播放的问题。第二个问题如上所述可以通过切成别的格式、加 avoid_negative_ts 解决,甚至你切出来的MP4再重新封装一次也行(-ignore_editlist 1 加到 input option);但是第一个问题则是实打实的缺少那些 packets,是切了之后就救不回来的。

这个问题最简单或者说唯一的解决办法其实就是重新 remux 一下原视频,无论是用 ffmpeg 还是 mkvmerge,无论是 remux 成 mkv 还是 mp4(可别再 remux 成ts),都会重新生成 PTS/DTS 且重新对 packets 进行排序,从而会出来一个你随便切也不会切出问题的 input。例如我们简单地用 ffmpeg -i raw.ts -c copy raw.mp4,再去 inspect 这个 raw.mp4:

可以看到 packets 的排序就比较正常了。

这个新的 input(或用 ffmpeg 或 mkvmerge 重新封装成 MKV 当 input),无论你怎么切,音视频起始点都一致的。不过上面提到过的 output 各种容器的 quirk 依然存在:

  • MKV: AV 的 PTS 都是从0开始到4。
  • MP4: AV 都从-1开始到3。0之前的都是有 D flag(也就是edit list)。
  • TS: AV 都从1.4开始,到5.4。

可以看到,总长度都是4s左右,因为原始视频有3个2秒的GOP,我们的切割点恰好在中间,这次和之前不同,都是往前切了一点从2s开始,重点是音频终于和视频开始时间相同,而不会像上面一样错开。

对于 MP4 格式,FFMPEG 再次试图通过 edit list (discarded flag+负 PTS)的方式来藏起来最前面1S(这次是同时藏视频和音频)。这里问题就来了……试着将这个视频放到 Chrome 里播放,果然又出现了音画不同步的问题!果然还是老老实实加上 -avoid_negative_ts make_zero 吧!

顺便一提,用播放器播放这个 mp4 视频,行为也和之前不太一样(音画同步都没问题):

  • MPV: 播放时,会从-1开始播放视频,但是音频只有0秒才开始,也就是说第一秒没有声音。
  • PotPlayer: 从-1同时播放视频和音频。
  • MPC-HC: 同 MPV
  • MPC-BE: 同 PotPlayer

结合播放这个、上面那个既有负 PTS 又有 AV 不同时开始的 mp4 视频、以及其他一些 单纯 AV 不同步开始但是没有负 PTS 的视频,可以大概猜测下每个播放器的特性了:

  • MPV: 从第一个视频包开始播放(即使有 D flag)。早于第一个视频包的音频不播放(即使是正PTS+无D flag)。不会播放有 D flag的音频包。
  • PotPlayer: 从第一个视频包(即使有D flag)或者第一个没有 D flag 的音频开始播放。会播放有 D flag的音频包。
  • MPC-HC: 从第一个视频包(即使有D flag)或者第一个没有 D flag 的音频开始播放。不会播放有 D flag的音频包。
  • MPC-BE: 无视一切 D flag,会完整播放所有存在的 pakcets,无论 PTS 正负。

最后还是上张图:

另外还有一点,如果使用 output seeking + stream copy,即使是使用重新封装过的 input,也会产出和上面使用 raw.ts 作为 input 一样的 4-6这2秒视频 + 3-6这3秒音频的结果。

大概可以猜到是为啥:output seeking时会解码原视频,当解码完第1个GOP时还不到 -ss,那就只能继续解码下一个,结果就超过到了4了;音频同理但是音频可分区间更小。也就是说,是音视频分别自动采取了指定 -ss 之后的最近分割点。下图为 output seeking+stream copy,统一使用修复过的 raw.mp4 作为 input,使用 mkv 作为 ouput 容器,不同 -ss 的情况:

而 input seek 则似乎是先选定一个离指定ss之前最近的视频分割点(关键帧),然后再以此时间戳寻找音轨的分割点。下图为 input seeking+sream copy,使用修复过的 raw.mp4 作为 input,不同 -ss 的情况:

实战

Workaround 虽然有了,但是问题来了,对于 input 过于庞大的,总不能重新封装几十G的文件吧?

所以我们只能用曲线救国的方式:先粗切一次切出一个 intermediate 文件,比如从 -ss 前 5秒开始切,然后再进行第二次切割。

至于这个中间文件,我们知道它本身会有AV不同时开始的问题,是否需要重新封装一次再切第二次呢?我实验了一下:

稍微解释一下,图中,raw.ts 就是原始视频,这次我换了个稍微长点的(18s)。我们先用 ffmpeg -ss 3 -i raw.ts -c copy 切出两个中间文件(没用 9-5=4 是因为想搞个正好在 GOP 中间的情景),分别封装为 .ts 和 .mp4;然后,再在此文件基础上再 ffmpeg -ss 6 -i intermediate.XX -c copy 一下,同样是保存为 mp4 和 ts,这样一共就有了四个文件。作为对比,我们同样把 raw.ts 直接封装成 raw.mp4,然后直接 -ss 9 到另外两个文件(known good)。

可以看到,虽然两个中间文件本身重现了问题,但是使用 MP4 的中间文件再切割时,完全不影响最后的结果,最后效果和直接先转整个 raw 到 MP4 再直接切9秒完全一样,包括时间戳。但是用 MPEGTS 做中间文件就不行了,所以还是别用了吧。这样就省事了,我们不需要再封装一下中间文件了。当然还是提示一下,如果不喜欢 MP4 这种带 negative TS + D flag 的,可以加那个 flag 来干掉。

总结

此次的文章废话有点多,来写个TL;DR。

问题:

  1. 当用 ffmpeg input seeking 切割 mpegts 视频时,会出现 A/V 没有切割到同一开始点的问题。
  2. 对于任意 input 格式,当 input seeking stream copy 以及输出封装为 mp4 时,ffmpeg会默认使用 edit list 来给部分 packets 赋予负 PTS + discarded flag,试图隐藏掉这些部分实现精确切割,可惜这一特性兼容性很差(不同播放器处理不同),尤其是在浏览器播放会导致音画不同步。

解决方案:

  1. 要修复1,要么将 input 视频整个重新 remux 成 mp4 或 mkv 再切,要么先粗切一次到 mp4/mkv,然后再细切至最终结果。
  2. 要修复2,可以在 output option 中加 -avoid_negative_ts make_zero

其他细节:

  1. 上述均为 input seeking。对于 stream copy,output seeking 虽然可以规避 mpegts带来的 seek 问题,但是:1) 解码到 ss 时间戳太慢;2) 无论怎么封装都会出现A/V切割点不一致的问题(因为A/V是分别从切割点后第一个key frame开始输出),强烈不推荐使用。
  2. transcode 的时候则相对无所谓,如果用 input seeking 则依然需要规避上述的问题1。

本文中出现的所有脚本(包括查看视频属性、画图、以及我个人用的一个切割视频的小工具)源代码以及两个测试用的文件都已经公布:点击这里查看

追记1:切割成有 edit list 的 mp4 的一个细节

在 Workaround 章节我有提到,只要把 raw 封装成 mp4,就万事大吉了,“论你怎么切,音视频起始点都一致的”,只不过如果输出是 mp4 会“FFMPEG 再次试图通过 edit list 同时藏视频和音频”。

另外我还对比了input seeking + stream copy时,不同 -ss 时产出的 mkv 的视频,可以看到视频都是向前取整GOP,然后音频和视频切割点基本一致,无论离得多远。

但是这个其实是不适用于 mp4 输出的!实际上,如果切成有 edit list 的 mp4,并不能总是切出音视频起始点一致的视频:

可以看到,视频总是切到上一个整GOP没错,这点和MKV输出一致,但是音频实际上是总是多切且只多切一秒,而不是一定和视频一致。我上面得出那个结论是因为我恰好选择了在GOP前一秒处切割囧。虽然对于支持 edit list 的播放器来说,这都没差,因为多的部分都加了D flag,但是别忘了我们的根本目的就是规避音视频不等长的情况来增强兼容性。所以……还是老老实实用 -avoid_negative 吧。这个出来的结果和 mkv 是完全一致的。

Repo 也更新了相关代码。

一个有关“ffmpeg分割视频导致A/V不同时开始的问题”的想法

  1. ” 对于 mkv 格式,偶尔会出现相邻的两个 packet 没有完全连续,而是有 1~2个 time base 的间隔现象 ”

    这个会不会是因为 timebase 精度太低呢,注意到 mkv 的 tbn 是 1k, ts 和 mp4 都是 90k.

    1. 显然 29.97 avgfps 更容易整除 90k tbn 而不是 1k tbn…

      来自 libavformat/matroskaenc.c:3430
      // ms precision is the de-facto standard timescale for mkv files
      avpriv_set_pts_info(st, 64, 1, 1000);

      看来这个 timescale 精度是 hard-wired 进去的了

  2. 越查越停不下来…

    在 libavformat/mov.c: 第3919行:
    // Audio decoders like AAC need need a decoder delay samples previous to the current sample,
    // to correctly decode this frame. Hence for audio we seek to a frame 1 sec. before the
    // edit_list_media_time to cover the decoder delay.

    也许这就是为什么audio总会往前多切一秒?investigating…

留下评论