再战视频渲染器:nVidia D3D11的BUG和应对方案

没想到又要再搞这个话题(叹气)。

在上一篇(PotPlayer无MadVR设置方案),因为我当时使用一台没有独立显卡的电脑,完全跑不动 MadVR,所以我研究了下 MadVR 之外的渲染器的方案,很欣喜地发现 D3D11 的效果其实很不错,故用之。

最近,添了一张 nV 4系的显卡,理论上很简单,只要再回到上上文用 MadVR 的方案不就行了呗?呃,其实用习惯了D3D11,发现MadVR有些小问题还能挺烦的:

  1. 估计是因为MadVR“太重”的缘故,在用它的时候,拖拽视频进度条有时候还是不够流畅。尤其是反向拖动的时候。实话说并不是很严重,一般来说你拖动前几次都非常流畅,但是如果你狂拖进度条几十次,前后来回(一般是找某个场景的时候),就会出现卡顿现象。
  2. 在开启反交错(双倍帧率)的时候,无论是LAV中开启还是Pot开启还是MadVR开启,在暂停的时候都会跳帧(画面往回跳一帧)。这个真的是个非常非常小的问题,但是强迫症很难忍受。

另外,每次放个高清视频动辄50%的GPU占用率感觉还是挺没必要的,不够环保。

那有人会问了,好吧,那你直接换回你上文说的D3D11渲染器的方案不就行了?没错,我就是这么干的。结果一干就出事了:nVidia GPU下的的D3D11,和iGPU的行为完全不一样,甚至有很多BUG!下面详述。

在开始之前,我先提一下另外一个渲染器,MPC Video Renderer(下称MPC-VR)。这个渲染器根据我的理解并不是MPC的一部分,但是是针对给MPC-BE使用为第一目标而开发的(反正他们的开发者都是那一堆人就是啦)。我第一次知道这个东西是之前搜索如何在视频播放器中调用nV的超分辨率功能时看到的。但是除了这个功能(和本文无关,后面附录谈一下)之外,这也是一个蛮优秀的渲染器,安装之后PotPlayer里也能调用(但是有坑,后谈)。

这个渲染器其实是一个很轻量的渲染器,其主要目的是方便你调用显卡自带的渲染器,再加上了一些很少的HDR相关和部分放缩算法调整,总体上而言和Pot用EVR-CP时,自带的一些设置选项差不多:

但是对于本文的目的就非常合适:非常便于我们测试各种DXVA2/D3D11相关的参数。

另外我们可以同时在MPC-BE和Pot中使用,这样可以控制变量,防止被一些PotPlayer自身引入的、而不是nVidia驱动的BUG干扰。而且其有极其详尽的OSD信息。

nVidia的D3D11视频处理器对于P010格式的BUG

要触发BUG,首先视频格式是 P010(一个10-bit的YUV格式,几乎所有的10-bit和HEVC和H.264解码后都是这个格式)。根据MPC-VR判断,这个错误其实发生在“D3D11 video processor”这一层。使用上图所示的默认设置就可以触发。如果用PotPlayer,最简单使用D3D11硬解+D3D11渲染器即可触发(不过还有一些其它条件)。

BUG1:整体颜色发绿

左为使用MPC-BE的默认渲染器(EVR-CP),右为Mad-VR

图片经过对比度、亮度加强。可以看到,右侧的使用MPC-VR明显有绿色的色调。

如果在MPC-VR的设置中进行以下任意修改:

  1. 去掉 Use Direct3D 11,也就是用 DXVA2(D3D9);
  2. 或在 DXVA2 and D3D11 video processor 中去掉P010/P016

这个BUG立刻就不见了。所以我的推测,这个BUG出现在 P010 转换到 R10G10B10A2_UNORM 这个YUV转RGB的过程中。因为如果用 D3D9,是转换成 A2R10G10B10 这个格式:

再放一张PotPlayer的绿图。作为对比的左边是使用 Intel iGPU 同样设置的结果。可以看到,颜色没有任何问题(但是有很严重的banding,这个下面单独说)。

左为使用Intel (正常),右为使用nVidia (发绿),其他设置完全一致

其实用Pot的时候(MPC-BE+MPC-VR不会),还有其他一种情况也会触发发绿BUG,就是使用D3D9解码+D3D9渲染器。但是使用D3D9解码+D3D11就不会:

左为D3D9+D3D11 (正常),右为D3D9+D3D9 (发绿)

虽然不是很确定,但通过观察 Renderer OSD里列出来的格式,我们可以揣测一下发生了什么。可以看到左边是从P010(被Pot自己?)先转成了RGB10A2才进的 Video Processor,所以规避了BUG;而右边则是很奇妙地直接P010一把梭转成了XRGB(一种4:4:4的RGB格式),可能这个过程中也触发了类似原理的变绿的BUG。

OK,所以为什么会这样?其实我很多年前就发现 nVidia 和视频相关的技术栈会有发绿的问题,当时是发现用 ShadowPlay 录制视频,有时候会发绿。虽然没有任何证据,但是我强烈怀疑这个bug和我之前在ffmpeg静态图转视频这篇blog里提过的、ffmpeg的swscale组件的一个有十几年的bgr->yuv颜色和rgb->yuv颜色不一致的BUG有关(虽然我们这里的 bug 是反向转换)。

更两人恼火的是我不知道应该去哪里汇报这个BUG。这种应该是得自己写一个不牵扯到视频渲染等一系列客户端软件layer、而是直接调用 Windows的 D3D11 API 的最小实现,再去nVidia Developers 相关论坛或者 bug tracker 汇报才会比较被重视,但是我实在没这个水平,更完全不懂C++,不知道从何下手。而且我想各种第三方渲染器的开发者应该早就知晓此问题了,但是也没有搜到比较任何相关的 documentation。比如我在MPC-VR的issues中发现了至少三个相关的问题:

https://github.com/Aleksoid1978/VideoRenderer/issues/48

这个完完全全就是这个BUG!开发者提到:

To convert from NV12 and P010 to RGB you use D3D11 VP. This conversion is controlled by the driver manufacturer and MicroSoft.
But you can disable NV12 and P010 options and convert with our shader.

v0lt

因此,提问者在关闭了 D3D11 的 Video Processor 选项(上述)之后就规避掉了bug,TA也就没有再深究。

https://github.com/Aleksoid1978/VideoRenderer/issues/98

这个虽然是HDR视频,但是我怀疑也是同样的问题,因为也是P010格式的。

https://github.com/Aleksoid1978/VideoRenderer/issues/22

这个其实是最有趣的:提报者(后来才知道,原来是某群群友)使用的其实并不是原生的 D3D11 VP,而是 MPC-VR 自带的 Shader 来做 Video process,但是居然出现了同样的bug!MPC-VR后面修复了这个BUG,理论上可以根据改动一窥端倪;但是我完全搞不懂是怎么修复的:因为并没有链接到对应的commit,根据评论中提供的2个分别反馈为bad和good的测试版本的commit hash,应该是这个diff——但是感觉完全没有相关的啊?!线索断了。

BUG2:Downscale时整个画面边糊出现重影

如果说上面那个BUG可能不是太明显,有的人估计看不出来的话,那么下面这个就非常离谱了。当使用 nVidia 的 D3D11 Video Processor(VP)进行 resize 时,如果是缩小,则整个画面会变得非常模糊且出现重影,这点在比较锐利的线条尤其是硬字幕的时候非常明显:

我相信不瞎的应该都能看出来吧!要规避这个BUG,除了上面的说的完全禁用D3D11 VP的办法之外,也可以在MPC-VR中单独关闭“Use for reszing“,或者在Pot里把 resizer 改成其他任意的。再次强调,这个BUG依然是只有P010格式的视频会触发。

实话说,很难相信这么明显的bug会一直没人修复!有点怀疑是不是最近的驱动才引入这个bug的。我又去MPC-VR的repo瞅了下,确实有两个汇报(第一个第二个),都是比较近期的。

再测 nVidia 语境下的 D3D11 渲染器的质量

OK,BUG说完,让我们来回到这一切折腾的起因——渲染器的质量。先回顾一下:

  1. MadVR质量最好/最可控,但是太重,在Pot调用时连带还有一些UX上的小毛病;
  2. 之前惊喜发现 D3D11 video renderer (Intel) 的效果其实非常好,尤其是缩放锐度和 halo 都很不赖,故用之。不过,10bit视频是直接截取,所以有非常明显的 banding 的问题,所以我们采用一些措施来尽量软解,靠前面的解码器部分来给我们dither到8bit再渲染(如果硬解会 P010 直通到 renderer)。主要的难点是Pot很弱智的内置解码器HEVC强制硬解,需要调用LAV来克服。

所以我们现在需要重新调研一下 nVidia 的D3D11(以及D3D9)在缩放和10 to 8这两个重点上的表现,毕竟已经知道了和 Intel 完全不同。当然还有 HDR tone-mapping 的问题。

不过我们先试试MPC-VR这款不错的渲染器在Pot上表现如何。如果不错,我们完全可以改用这个。很可惜,虽然没有上面提到的MadVR那些UX上的小bug,但是有个更离谱的:在调整播放窗口大小(包括切到全屏)时,会非常的卡,外加闪烁。同样,这只是个一个非常小的问题,但是我表示不能接受,故放弃。

缩放

OK,那么我们就来比较最常见的4:2:0 8-bit的1080p视频缩放到1440p的表现吧:

呃,这一比就比较尴尬了——nVidia的渲染效果比Intel差了不是一点半点。锐度不如而且 ringing 更大。

D3D9(DXVA2)和基于D3D9的EVR则更差,整体又多糊了一档;而且还有很奇怪的图像整体向左偏移约1像素的问题(假设MadVR为 ground truth),不过这个倒是不影响观看。加强对比度后对比:

当然,我们可以在 Pot 选用其他的基于 shader 的 resizer,但是那些效果也都挺差的,毕竟都是一些比较基础的算法。

10 to 8

上文提过, Intel 的渲染器上文有提到过完全没有 dithering,所以如果在那里进行10转8,会出现非常严重的 banding。这点 nVidia 这边终于有改善了!如果用 D3D11 renderer,会有 ordered dithering,而且还是渲染像素级的(非原始分辨率级),效果很好:

但是!别忘了我们上面提到的发绿的BUG!(其实这图里都能看出来绿了。)所以说,这个我们其实还是享受不到,还是得老老实实把 P010的解码设置为软解,让解码器输出NV12(也就是已经降过位深),然后再给 D3D11 VP/VR。

那么在nV下,其他几款渲染器效果如何呢?直接上结论,结合上面的BUG一起说:

  • D3D9解码+D3D9渲染:发绿,downscale重影,banding
  • D3D11解码+D3D11渲染:发绿,downscale重影
  • D3D9解码+EVR-VP渲染:完全OK!

怪了,说好的 EVR-CP 是基于 D3D9 的呢,怎么现在居然有 dithering 了?虽然不是工作在渲染分辨率(而是视频分辨率),但是效果完全OK啊!

从 OSD 可以看到其格式流程是 P010 先到 A2RGB10 进 Mixer,然后在 RGB 空间 dither 到 XRGB 进行后面的步骤。

也就是说,如果不是缩放质量太差,我们甚至可以换回 EVR-VP 了。

另外还有一点,上文提到修改内置解码器设置中,把 HEVC 的解码器改成 ffmpeg.dll 来强制软解,很可惜现在已经没有这个选项了!所以说现在没有任何办法可以关闭 Pot 对 HEVC 的硬解(也就是会导致10-bit输出 P010 导致 BUG),所以 HEVC 必须得调用外部的 LAV。

所以,最后下来,我的配置其实和之前差不多:

Pot 内置解码器设置中不开启硬解(但是先勾选一下然后勾选下面的D3D11后再取消以防万一:

对于HEVC,手动设置LAV为编码器,来规避P010直通D3D11 renderer导致的BUG:

使用内置解码器的时候(所有非HEVC的格式),其实都等价于会自动使用转换滤镜。对于调用LAV的情况(HEVC),开不开出来的结果对于D3D11渲染器其实都没差,我就开了。

不过要注意,如果 LAV 勾选了 P010、且开启转换滤镜的时候,LAV播放10-bit 会出P010给D3D11,导致触发 BUG。而且不管你在 Pot 的 Colorspaces 那里怎么设置都不行。如果真的不想开转换滤镜,那一定要进 LAV 把 P010 输出取消勾选。这点我们下面详细总结里会再赘述一遍。

BT.2020/HDR

设置完后,我们再去验证一下几个非标情况的播放结果。首先是 HDR——其实这个东西我也不是很熟,纯粹靠自己瞎看了。说错了请指教。

因为我的屏幕是SDR,所以需要tone mapping到 SDR。如果用MPC-BE先做个简单测试,会发现播放的时候其实MPC-VR端收到的 Transfer Function 变成了BT.709了(但是 Matrix 和 Primaries 还是 BT.2020):

MPC-VR OSD

EVP-CP的话则是显示了两个BT.709,搞不清楚哪个是哪个。

EVR-CP OSD

这个转换是完全不可控的,似乎也没有任何选项,无论硬解软解都会转换。当然我也不是想折腾,毕竟颜色是对的就行了。

而如果你用 MadVR,则是显示了两个BT.2020:

而且颜色是错的,你需要单独设置MadVR显示器校准那边才能把颜色给整对。而且MadVR那边的设置非常迷惑,我之前提过一次

(题外话,在MPC-VR的repo里有搜到有人说 VR 的 OSD 显示的 Primaries 和 Matrix 是和 MediaInfo 正好反了但是开发者不置可否,介于这三个东西经常有各种奇怪的别名,我不好说也不深究了。)

同理,用 Pot 调用各种渲染器也有这个类似的操作,但是有的时候没有 MPC-BE 那么灵光。比如,如果用LAV 输出 NV12(输出 P010 同理)然后喂给 MPC-VR:

可以看到和 MPC-BE 不同,这里 MPC-VR 是收到了 BT.2020 transfer function。然后出来的颜色也不对。虽然 MPC-VR 里有转换 SDR 的选项:

但是我折腾了一万年也没整明白怎么能触发他(触发了的话 OSD 中应该会有一行后处理:转换到 SDR 云云)。不过,如果我把 HEVC 的解码器改回内置(并被 Pot 强制硬解),则一切就又正常了,和 MPC-BE一致:

搞不懂啊搞不懂。还好,用 D3D11 renderer 的话没这毛病,无论什么解码器,都可以正常转 SDR。

另外有个细节,如果 resizer 选的是 auto,这里这个测试用的 4K BT.2020 视频无论解码器用的啥(Pot 自带硬解,LAV 各种格式输出,etc.),resizer都会变成 Texture Bilinear 而不是一般常见的 D3D11 Video Processor,也就是类似 EVR-CP 的画质。当然你可以手动选回去(我有想是不是因为视频是4K的缘故,但是另外找了一个 4K BT.709 的视频,没触发)。

Full range, 4:2:2, 4:4:4 等各种情况总结

还是直接先上表吧!

这次和上次的比稍微复杂了点,多加了几列,有两列还调换了下顺序,如果要对比请注意。标注思路还是一样:绿色 good,黄色可忍,红色不能忍。EVR Vista 啥的那个就不测试了,反正没人会用到。

我上次的表格没有区分是否勾选Pot的视频“转换滤镜”,后来发现这个对于LAV的还是有些区别的,故加上。

第四列那个再罗嗦一次,就是这个选项,名字太长了:

中文是叫:

下称“直接转换输出色彩空间”。其大概意思就是,如果条件允许绕过Pot自带的转换滤镜,直接从解码器通到渲染器。因此,3、4列也不是所有组合都会有区别,简单来说:

  • 使用内置解码器时,相当于始终启用了“转换滤镜”。实际上是否能跳过,取决于“直接转换输出色彩空间”是否启用。
  • 使用 LAV 等外部解码器时,只有开启转换滤镜时,开启“直接转换输出色彩空间”才有意义。因为如果不开启转换滤镜,其实就相当于可以直接转换输出色彩空间。唯一的区别就是,如果使用开启转换滤镜+直接转换输出色彩空间的组合时,Pot 不会接受LAV的P010输出为输入(所以 LAV 会变成NV12 喂 Pot),但是不开启转换滤镜则可以。

那么让我们仔细端详下这个表。

可以看到,D3D9对于 full range 的视频束手无策,总会clip,我们先一票否决。

使用内置解码器时:

对于422和444只能软解。如果不开启直接转换输出色彩空间,因为转换滤镜的缘故会用NV12,所以自然损失了不少 Chroma 空间的质量;即使开启,renderer最大也只吃422,所以444还是会损失。

对于视频本来就是8 bit YUV的,无论软硬解都OK,反正都是NV12。对于10-bit则比较tricky。如果是软解(H264的情况),内置的 ffmpeg 解码器会帮你 dithering 后 NV12 输出,除非开启了直接转换输出色彩空间,则会直通 P010。而硬解怎么搞都是 P010 ——别忘了,如果是 HEVC,Pot 的内置解码器是强制硬解的。

如上文所述,我们的渲染器碰到 P010,EVR-CP 是很OK的,D3D9、D3D11 都有严重的问题。所以如果和我一样要用 D3D11,就得不开启直接转换输出色彩空间来保证软解时输出正确。至于硬解的情况?我们直接选择 HEVC 的解码器为 LAV 来解决。

再来看看LAV:

基本来说,使用“禁用转换滤镜”和”启用转换滤镜+同时开启直接转换输出色彩空间”这两个组合都可以完美处理所有格式:对于 10-bit,如果是 D3D11 渲染器的情况,要手动取消掉 LAV 里的 P010 输出,在 LAV 中直接 dither 到 NV12。

如果使用转换滤镜但是却不开启直接转换输出色彩空间,因为Pot的转换滤镜只能工作在 NV12,这两种色彩空间,所以即使是10-bit 视频,也是会直接找 LAV 要 NV12,即使你 LAV 没有取消 P010 输出。所以很OK。但是副作用是,无法正确处理442和444的视频,这个和上面原因类似,就不赘述了。

介于我们想用 D3D11 渲染器,然后还要应对 P010 的BUG,我们其实有以下几种选择:

  1. 全局使用 LAV + 禁用转换滤镜 + LAV 中禁用 P010:所有格式都OK。缺点是如果其他情况调用LAV时无法用 P010。
  2. 全局使用 LAV + 转化滤镜 + 直接转换输出色彩空间:所有格式都OK。
  3. AVC使用内置(软解),HEVC 使用 LAV,同时禁用“直接转换输出色彩空间”防止内置软解直通 P010:422、444效果差。

理论上自然应该用2,但是因为某些很纠结的原因(主要是用内置滤镜比较流畅),我实际上是用的3……呃。我就是有毛病我承认。

再看看默认的 EVR-CP:

其中真·默认设置的是倒数第二行。可以看到,效果还是挺不错的(和我改了半天的结果基本一样)。

也就是说,如果不在意缩放画质(这个其实也可以修改算法来稍微改善),Pot的默认设置现在已经完全可以用了,因为实际上他现在已经解决了 banding (or lack of dithering) 这一大痛点。我突然感觉到一阵空虚:我干嘛要折腾这个!

附录:nVidia RTX Video Super Resolution

顺便提一嘴这个(下称VSR)。毕竟我最开始折腾有这个的因素。目前为止支持这个的播放器还不是很多,Chrome 是原生支持的,什么都不需要设置。VLC有个专门的魔改版,但是这个版本有个非常恶性的bug,播放一切非16:9等非标准宽屏分辨率的视频时会显示比例错误

说个题外话:这个BUG已经提出了9个月了,但是根本没人修复。对于这种大型知名开源项目,经常看到这种核心功能还算维护的OK、但是稍微非核心功能的、即使是严重bug也没人修复的窘状。但是与之相反,有些明明理论上规模相当的项目,比如MPV,就有非常充裕的核心开发者和路人在进行快速迭代。搞不懂这是什么原因,是项目太老不够吸引新人?还是PR审核太慢,久而久之就没有人愿意参与?(嘛,想起 ffmpeg 现在还得在 mailing list patch……)哦,对于 VLC 这个特例,丫的 repo 和报错网站(https://code.videolan.org/)居然需要审批才能注册账号,这能好吗?

对于 MPC-VR,已经很早就加入了对VSR的支持,勾选一个选项即可(必须得用D3D11,所以连带着上面各种BUG)。另外还有个fork据说是加强了对HDR的支持,不过我没试过。

至于Pot这边,虽然已经加上了选项:

(要启用D3D11渲染器才能选)但是我这边实际上并不好使,无法调用到 VSR。更离谱的是,如果我改用MPC-VR 渲染器,然后再里面勾选,在 Pot 里还是无法触发 VSR。所以我放弃了。(我有发邮件问作者,但是他说他没问题。)

效果方面,我只测试了开到4(最大):denoise 的强度还是蛮高,对于那种 compression artifact极多的超低分辨率视频效果还行:

但是看 720P 或者 1080P 之类的本身就不是很模糊的视频就油画感很强了,感觉没有必要,不如传统缩放算法。

哦对了,这个 VSR 对于小于 360P(以及大于 1080P?)的视频是无效的,这是 nV 那边写死的,不是播放器/渲染器的问题。

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 也更新了相关代码。

PotPlayer无MadVR设置方案

又来折腾这玩意了。其实之前的方案完全正常啦,但是最近组了个新电脑没买显卡,本来以为反正自己也不玩游戏够用了,没想到居然用MadVR随便播个东西就GPU占用100%:

直接卡到12fps!把所有的缩放算法全部改成最低端的Cubic也只能勉强到50多fps(显示器是75的),看来是没得救了。那让我们找个替代品吧。

面临的问题

如果有读过前文和前前文就应该知道,之所以用MadVR并不是我对画质有什么极致的追求,而是在很多基础的东西譬如10bit抖8、格式转换等上PotPlayer是非常的差,MadVR恰好可以简单地修正这些问题罢了。

具体系统性的细节就不再赘述,如果需要可以复习前面两篇文章。这里直接说我们要解决哪些问题。

其实对于最常见的,4:2:0、limited range、8bit的视频,PotPlayer开箱的默认配置也不是完全不能用。但是有以下三个特例需要处理:

  1. 10bit视频——显示器是8bit的,所以10需要抖到8,无抖动大量banding。10bit视频现在已经非常常见了,动画民间压制组几乎全都是用HEVC 10bit出片。所以这个dithering是必须要有的,否则白瞎了。
  2. full range视频——商业发行的影音产品不算太常见,但是直播等非常常见。
  3. 非4:2:0视频(例如4:2:2、4:4:4视频)——更少见,除了某些民间压制组会用,其他基本见不到。

例如,Pot默认的用EVR (Custom Preset) (下面简称EVR)这个渲染器的最大问题就是不能正常显示full range视频。无论用软解硬解还是外置LAV都不行:

所以我们直接pass。

另外,一个thumb rule是所有转换都应该只做必要的转换,比如如果本来是422,那就不要劣化成420再转RGB;本来是8bit也别转10bit,反之亦然。

PotPlayer近年来的改进以及10bit输出

在最早那篇文章提过,PotPlayer当年最大的问题是他内部默认使用YUY2这个4:2:2的格式来处理,所以常见的8bit 4:2:0格式会被Pot拉伸一次,而且默认还是极差的NN算法。现在,Pot修复了这个问题,基本正常情况都会用NV12、YV12之类的格式通到渲染器。单这一点就把其播放一般普通视频的画质提升了一个量级,完全达到了能用的水平。所以如果没有特殊需求(下述),这两个之前提过的选项可以用Auto不用动了:

不过为了以防万一,还是把下面的高质量也给勾上吧。

另外,在输出方面增加了10bit的输出的选项:

开启10bit输出后会尽量在解码器端输出为10bit,然后到renderer会转成A2RBGB10之类的10bit RGB格式。但是因为我显示器只有8bit,最后还是会banding成8bit(且和P010这类10bit YCbCr格式直接转RGBA的banding还不太一样)。这里自然不开启。而且这个10bit输出还有一些bug:

  • 用EVR,会发生奇怪的颜色反转。蓝色变成黄色
  • 用D3D9 native 解码再输出到D3D9 renderer,会出现一个奇怪的绿条在下面:

最糟糕的问题其实是8bit的视频会被拉成10bit(然后最后在display层面再降回去)——据我观察D3D11 renderer即使这样折腾一次也问题不大,但是D3D9 renderer整个画面会变得超级糊,一定要避免。

处理10bit视频

这里先明确几个前提:我们只追求抖动的有无,质量不关键。另外我是8bit屏幕,所以一定要10转8;如果是用的原生10bit,则自行保证最后renderer是10bit输出即可。

这个转换,可以在编码器阶段进行,也可以在renderer阶段进行。之前用MadVR的时候就是在renderer阶段进行,所以一定要把解码后的视频保持10bit的格式(一般为P010)输送到renderer。

不过现在不用MadVR了,我们应该在哪里进行效果好呢?经过一些基本的测试,结论如下:

首先,如果在硬解native里进行10转8,是没有dithering的,效果很差。

如果用LAV调用硬解,我测试中是发现用DXVA2 native或者 D3D11都会banding,DXVA2 copy-back和软解则无问题:

测试时强制了LAV用RGB输出,这样保证不会在接下来的任何步骤再修改。LAV自己的10转8是有dithering的。我的理解是,用了DXVA2和D3D11 native的话,LAV就完全放手了让他们来输出为NV12之类的的格式,自己仅负责转换为RGB的样子。

240105更新:上面的DXVA的测试结果仅限于 Intel CPU 带的 iGPU;我使用N卡测试时,即使使用native也是自带dithering的!

如果用Pot内置解码测试,默认是用D3D9,只有用D3D11的renderer的时候才会用D3D11的解码,不过可以手动修改。也可以用D3D9 copy-back和D3D11 copy-back。搭配MadVR的时候,这里无论选哪个都是可以直出P010的,也是我之前一直用的。顺便一提,我在这台电脑上用D3D9 native + madVR播放full range视频,会出一个很奇妙的问题:

可以看到色域虽然没有错误伸张、压缩,但是16/235外的被clip掉了。然而我用我的笔记本无法复现。不过无伤大雅,改成copy-back或者D3D11即可。

回到正题。既然我们不能用MadVR,我们可以用默认的EVR、D3D9 renderer或者D3D11的renderer。使用硬解的时候,基本都会直出10bit的P010给renderer。但是凡是在上述三个renderer里发生P010转RGBA(即10转8),也都会banding。开启10bit输出虽然可以正常转成RGB10,但是最后在display层也会转到8bit所以也是同理。一言以蔽之,不要在非MadVR之外的renderer这一层来进行10转8,因为没有dithering。

所以,如果要用LAV内置解码器,就一定要用软解或者硬解copy-back(copy-back的硬解基本在实践上和软解区别不大,应该也是用了ffmpeg来处理)来输出NV12,然后renderer直接转RGB就完事儿了。Pot默认设置不开硬解其实就是这样的。注意一点就是如果你开启了direct conversion (change default output color space) 这个选项的话,可能又变回P010输出坏事儿。

另外一个小细节:不能选D3D11 native解码+D3D9渲染。会强制给你改成D3D9。反之则可以。

总结一下:

  • 硬解直接10转8输出:banding
  • 无论软解硬解如果输出10bit,渲染器里10转8:banding
  • 所以要用:软件解码器或者硬解copy-back来完成10转8的过程。

软解的选择可以用Pot内置(即FFmpeg)、LAV(可以手选dithering的方式)。记住,Pot默认强制HEVC用硬解,即使不勾“Use DXVA”也是。可以通过修改这里为FFmpeg.dll来强行取消:

FFmpeg根据我的观察应该是用的ordered dithering。

这里来比较一下。先来一张banding的(所有的截图都是1080p片源1440p播放,然后截图后调整曲线来增加对比度,最后再NN放大到3x):

再比较一下dithering:

可以很明显看到两种dithering的区别。观感上来说其实都差不多,不过还是用random吧。中间的则是对比用的MadVR——MadVR dithering最好的地方在于他工作在全分辨率(也就是1440p),所以pattern极其小,可以说100%时肉眼完全看不出任何颗粒感。别的都是先生成dithering、然后再被放大,那自然效果远不如。不过,看片的时候还是OK的。

至于渲染器的选择在这里不影响,我们都是8bit输出了反正。

视频resize的表现

确定了10bit没问题,我们再回来看看经典8bit视频resize的效果。这里有点出乎我的意料:无论是用D3D默认的DXVA Video Processor,还是D3D11带的D3D11 Video Processor(Pot里选成Auto就好),效果都出奇的好,可以和说我MadVR拉满不相上下!

我的理论是,他这个不知道用了什么Intel的劳什子视频后处理科技,估计是在全屏分辨率级别搞了点锐化之类的?无论如何,至少看高锐度的东西效果很好,清晰,也没有太明显的ringing,远强于Cubic之类的了。我们尽量选用能调用Video Processor来resize的方案。顺便一提,EVR默认也是调用DXVA Video Processor来当resizer。

Full range、422/444视频

Full range视频不出意外地都没有问题,甚至EVR (Vista/.NET3)那个都没问题——只要别用EVR custom preset那个。

422/444视频则比较tricky。理想情况,当然是422就全程用YUY2,444就全程AYUV,不要出现被转化为420格式再喂给渲染器的现象,自降分辨率是大忌。

但是现实很骨感,这里面有好几个局限性:

  • Renderer只能接受部分格式输入
  • 某些格式用LAV+开启Pot转换滤镜时,Pot不收(虽然用内置的可以)
  • Pot的自动格式选择非常的弱

让我们先来个一览表:

这里面绿色是我们想要的,黄色的是非最佳的,红色的是无法接受的。这里面第三列就是前面提过那个开关:所谓的“direct conversion (auto change color space)”是开还是关。

简单概括下:如上面说的第三点,Pot如果把输出设为Auto,其实基本就是只会用NV12/YV12这种8bit/420的格式。唯一的例外是用硬解native的时候,会直通P010(10bit 420)到renderer(但是我们已经说过我们不想这样,因为非MadVR的renderer没有dithering)。所以,这样下来面对422/444的视频,都会被Pot给降低到420,bad。

这个问题可以通过这个开关来改善——开了之后,对于EVR,解码器会自动改用422和444的输出,对于D3D9,至少能修复422(但是444会crash)。D3D11则比较怪,即使开了开关,还是只能被喂420(或者RGB);我稍微查了下应该是支持至少422才对,可能是Pot的implementation有问题吧。

但是这个开关也有个副作用,就是用内置解码器的时候,会把10bit本来我们想要的8bit输出(抖动后)也给变成了10bit直通。

如果是用LAV,就很容易解决这个问题,LAV里把10bit的格式全部去掉,自然就只可能输出抖动后的8bit。

(这里有个疑似BUG:Pot的转换滤镜开启时,它并不收来自LAV的P010,如果你强制在LAV那边只勾选P010,Pot会直接强制自行关闭转换滤镜……嘛,纯粹是好奇罢了,毕竟我们的目的恰恰是LAV不要用10bit输出。)

另外,使用“LAV+EVR (Custom Present)+开启auto coloir space开关”这个组合时,除了会有前面多次提过的full range色域错误的问题(未伸张,即0变成16),另外播放444视频时,会出现色域过度伸张的错误(即16变成0)。

但是如果把渲染器换成EVR (Vista/.net3),看表格里似乎完美?

很可惜,EVR (Vista/.NET3)有个更阴间的BUG……字母某些特效会变成这鸟样(请无视我没安装字体。下面是对比用的EVR (CP)):

结语

既然没有一个完美方案,我们只能两害取其轻了。我最后采用的是内置软件解码器+D3D11渲染器+不开auto色域转换开关的方案(换成D3D9渲染器也行)。这样只有422、444不能完美播放(但是也不至于不能播放),而我除了我自己造的测试视频其实根本没下过这种视频w。我还检查了下BT601的视频,也是没问题的。

哦,其实还有个方案就是上面提过的LAV+强制RGB输出的方案。可以一揽子规避所有转换的坑。但是,这样就没法享受到高质量的DXVA/D3D11 Video Processor做resizer,所以还是算了。

20230520补记

最近碰到一个视频是 SMPTE 240M 的matrix,发现用D3D9的 renderer 有问题,D3D11的OK(和MPV的效果一致),所以虽然都行还是推荐用D3D11。

另外,我发现我上面明明还有一个全绿的组合:使用LAV+D3D9渲染器+开启auto coloir space开关,但是我为啥没使用?我自己也忘了,大概还是为了尽量不用外部滤镜来让截图不偏差吧。

ShadowPlay录屏的色域问题及如何正确处理BT.601 (SMPTE 170M) 视频

前几天因为某些需求,使用nV家的ShadowPlay(下称:SP)进行录屏。我录屏一般都是用较高的码率(60Mbps)录制,然后再手动二压一次。这次由于视频比较长而且质量不是很关键,就用NVEnc直接压了遍。

压完之后随便拖了下进度条对比——咦,颜色怎么有点不一样?

左:原始SP录制视频用Pot播放;右:NVEnc压制后用Pot播放

可以明显看到,左边的图红色要暗一些,右边(压制后)则艳丽许多。

根据我的经验,知道这肯定是YUV色彩空间的问题。赶紧来看看metatag:好家伙,SP录出来的视频居然是:

Color range                              : Limited
Color primaries                          : BT.601 NTSC
Transfer characteristics                 : BT.601
Matrix coefficients                      : BT.601

(注:比较老版本的SP,transfer用的是 BT.470 System M。)

那么看来,似乎应该很简单就是NVEnc转换时没有进行任何处理,生成的视频又没有tag,导致播放器默认按照BT.709播放错误了呗。至于原始视频,虽然是BT.601,但是由于有加正确的tag,那么肯定解码、转换成RGB域之后是正确的。

不过为了谨慎起见,让我们换个播放器来试试。一般而言,如果出现播放异常的情况,我第一个排查的方式就是分别用:MPV、LAV+MadVR(MPC-BE)、LAV+EVR各播放一遍来和PotPlayer的结果对比。即是说,我一般是不信任Pot的结果的“正确性”的。虽然我的PotPlayer在我调教下已经基本可以*正确*播放99%的视频,但是总有无法覆盖的奇怪情况不是。

这么一对比果然就发现了问题:同样播放原始视频的情况下,MPV和LAV+EVR结果一致,但是和LAV或者Pot内置解码器+MadVR的结果不一致。不过这里的不一致和上面情况不一样,更加微小:

左:MPV 右:Pot或LAV+MadVR

这里可以看到,会导致不同的是MadVR,而不是解码器部分。在继续分析为什么不同之前,我们需要知道哪个是“正确”的结果。这里有个很简单的方案:用PS绘制一张红色(BT.601和709矩阵区别在红色最明显)图片(我测试时没用纯红而是用了250,0,0,防止clipping影响对比),然后SP录屏,查看结果用吸管工具取色看哪个对即可。图我懒得贴了,结果就是,用MPV的结果是正确的,MadVR错误。

接下来的过程,我尝试使用ffmpeg的各种色域转换命令对视频”修复“,搜索了不少网上的帖子,花了几个小时也绕了很多弯路,感觉按时间顺序叙述就过于啰嗦,所以我直接写最终结论。

结论1:SP录制的视频确实是BT.601的color matrix,但是并不是BT.601的color primaries。metatag标注错误。

这里首先要复习下我在这个帖里讲过的matrix、primaries、和transfer function的区别(下称M、P、T)。

  • Color primaries:指定gamut的范围(三个原色[即“primaries”]的位置,白点位置)。理论上回放时只和校准(显示器)颜色有关,所以绝大部分播放器不会使用这个属性。
  • Transfer characteristics:指定gamma ramp。譬如,你可以指定为linear等。几乎所有播放器都无视,直接用那个标准的nonlinear的ramp(见下文)。
  • Matrix coefficients:这个是比较关键的也几乎是唯一会被播放器读取并采用的,指定了YUV和RGB的转换矩阵。

然后他们的值的区别:

  • P:BT.601 NTSC=SMPTE 170M,不等于BT.601 PAL=BT.470 B/G,不等于BT.709,标错理论上会导致显示错误(下述)
  • T:所有常见标准的 transfer function 基本都一致,所以即使标错也影响不大
  • M:BT.601=BT.470 B/G (PAL)=SMPTE 170M但是同样不等于 BT.709,标错几乎一定会导致显示错误

SP录制出来的视频,确实是用了BT.601的矩阵(M),但是实际上用的依然是BT.709的P——这里可以通过强行修改metatag之后再播放来验证。至于T,由于所有的标准其实都一样,所以标成601还是709不重要。

结论2:一般播放器会无视P,所以metatag写成什么都无所谓;但是MadVR开启calibration之后会对P进行转换。

如上所述,由于P的特性导致在屏幕没有校准的情况下是无法去适配的,所以一般播放器都完全无视了这个metatag。这也是为什么上述SP录制出来的视频用MPV或者LAV+EVR播放显示“正确”。但是MadVR这边,虽然我的屏幕并没有校准,但是为了能让BT.2020的HDR内容tone-mapping到感知上比较合理的SDR颜色,我“被迫”开启了MadVR的“我的屏幕已经calibrated“的选项并且把校准目标设置为BT.709:

否则,HDR视频的颜色直出,和屎一样:

我不知道为什么MadVR没有单独的tone-mapping HDR content的选项,反正我试了hdr tab下的所有选项,都没有用。MPV其实也需要设置,需要开启vo=gpu(或者直接开启copyback的硬解hwdec=auto-copy,会自动开启vo=gpu)。

这里提供一段BT.2020的视频谁想用可以拿去测试自己的视频播放方案: https://1drv.ms/u/s!Akq11jtCTJYwguclNUTq5MsccSRZ3w

呃,有点跑题了。总之,为了能”正确“显示BT.2020的内容,我开启了这个“我的屏幕已经calibrated”的选项。然而这就导致MadVR会把BT.601的Primaries也转换到BT.709。而SP的视频虽然metatag标注是P=BT.601,但是实际是只有按照P=BT.709来播放才能显示正确的RGB颜色,所以在M转换之后(这个是正确的、应该的)MadVR又进行了一次额外的P转换,导致显示结果错误。

细心点可以看到这里transfer function也被指定了。根据我的测试,如果在发生P转换的时候,这个T选项(尤其是gamma)也是会对结果有影响的。不过,如果如果仅有M转换发生时,这个选项选成什么都没有区别。

要不重新编码修复这个视频,最简单的办法是:

  1. 用ffmpeg指定P:
ffmpeg -i shadowplay.mp4 -color_primaries bt709 -c copy fixed.mkv

这里注意,要保存成mkv,mp4容器无法覆盖掉视频流的P的metatag。麻烦点也可以先直接转换成MKV,然后用MKVToolNix的header editor编辑。总之,最后出来的MKV用mediainfo查看大概是这样:

Color range                              : Limited
Color primaries                          : BT.709
colour_primaries_Original                : BT.601 NTSC
Transfer characteristics                 : BT.601
Matrix coefficients                      : BT.601

播放是正常的,会认为是BT.709的P,MadVR不会再进行转换,结果和MPV一致了(这里Pot的OSD是显示错误的,大概他直接读取了视频流的tag,没有显示MKV容器覆盖上的,请无视):

以上就是SP视频的Primaries metatag的错误和修复方式。

MadVR转换Primaries的正确性

虽然我们看到了如果开启calibrated,MadVR会对P不为BT.709的视频进行一次转换,但是这个转换结果是否正确呢?

让我们试验一下。还是用250,0,0的颜色来举例——我先用zscale制作一个BT.709的视频,用MPV和MadVR播放都正常显示为250,0,0。然后,我们用ffmpeg的colorspace vf,分别转成bt601-6-625、smpte170m、bt601-6-525:

ffmpeg -i zscale.mp4 -vf colorspace=iall=bt709:all={target_colorspace}:format=yuv420p -sws_flags accurate_rnd  zscale_to_{target_colorspace}.mp4

(Where target_colorspace = bt601-6-625, bt601-6-525, smpte170m)

接下来我们分别用Pot和MPV播放生成的视频,然后截图取色,结果如下(前两行其实是一样的):

New colorspacemediainfo Pmediainfo Tmediainfo MMadVRMPV
smpte170mBT.601 NTSCBT.601BT.601235,37,0243,0,0
bt601-6-525BT.601 NTSCBT.601BT.601235,37,0243,0,0
bt601-6-625BT.601 PALBT.601BT.470 System B/G249,0,0245,0,0

这里辨析一下几个非常迷惑的term,基本上:

NTSC标准系列:BT.601 NTSC = smpte170m = bt601-6-525(转换矩阵在mediainfo里就叫“BT.601”无后缀)

PAL标准系列:BT.601 PAL = BT.470 System B/G = bt601-6-625

有些细节问题(比如这俩的M其实是一样的;又比如T在FFMPEG里SMPTE-170M, BT.601-6 625 or BT.601-6 525都用同一个,bt470bg反而是略有不同)这里按下不表。

其中,MPV的结果就是仅做M转换,不做P转换的数值。而MadVR做了P转换之后,可以看到只有PAL的结果是比较正确的,如果是NTSC,反而比不做转换还要差好远。为什么?鬼知道。这里我不是说MadVR的结果一定是错误,毕竟整个流程可以出错的地方太多了(ffmpeg滤镜可能算法有误,我对整个流程的理解可能有误,等等)。但是总体而言,不同matrix的回放已经很成熟,也一般都很准确;但是我们尽可能要规避回放时的Primaries的转换。

我在前文“用ffmpeg静态图转视频”提到过静态图转视频需要进行一次BT.601到BT.709的转换,这个说法是没错的。但是这个转换仅限于矩阵,P是不需要转换的。所以如果真的想用我提到的“不转换,只添加metatag”的方式,也只需要添加M的tag,而不需要添加P的(添加了反而错了——虽然大部分播放器不会表现出区别)。同理,如果要用colorspace vf,也别用iall和all了,而是用ispace和space(但是这个vf要求你必须把所有的转换都显式写出来才能运行,所以你得用超长的 -vf colorspace=iprimaries=bt709:primaries=bt709:ispace=smpte170m:space=bt709:itrc=bt709:trc=bt709:format=yuv420p)。(这里用 bt601 或者 smpte170m 都可以,一样的。)

转换BT.601 Matrix到BT.709

OK,我们已经讲了如何修复SP录制的视频的P的tag的问题,但是他这个BT.601 NTSC的Matrix,虽然正常的播放器播放不会有啥大问题,但是为了兼容性(而且我们本来就要二压),还是把他转成BT.709吧。我们已经知道了P和T没有转换的必要,所以只需要转换M。

基本上,之前提到过的那些转静态图的命令都能用。唯一需要注意的是某些滤镜,例如scale,会复制旧视频的metatag而不是生成新的BT.709的,需要显式覆盖掉。

# in_color_matrix不用写,默认是auto。需要手动加output的metatag覆盖。
ffmpeg -i bt601.mp4 -vf scale=out_color_matrix=bt709:flags=accurate_rnd+full_chroma_int -colorspace bt709 -color_primaries bt709 -color_trc bt709 scale.mp4

# 因为P的tag错误,所以别用iall,逐个显式声明罢。Output metatag自动加。
ffmpeg -i bt601.mp4 -vf colorspace=iprimaries=bt709:primaries=bt709:ispace=smpte170m:space=bt709:itrc=bt709:trc=bt709:format=yuv420p -sws_flags accurate_rnd+full_chroma_int colorspace.mp4

# Input不能自动识别,需要显式声明。Output metatag自动加。
ffmpeg -i bt601.mp4 -vf colormatrix=smpte170m:bt709,format=yuv420p -sws_flags accurate_rnd+full_chroma_int colormatrix.mp4

# 还是推荐用zscale了,精度高。Input自动识别,output metatag自动加
ffmpeg -i bt601.mp4 -vf zscale=matrix=709,format=yuv420p zscale.mp4

用NVEnc的话,是

nvencc64 -i input.mp4 --vpp-colorspace matrix=smpte170m:bt709 -o output.mp4 --audio-copy --colormatrix bt709 --colorprim bt709 --transfer bt709 --colorrange limited

NVEnc的输出默认没tag,这里显式加上正确的tag(虽然一般播放器都会guess所以还好)。

参考文献

Agapanthus (麻倉もも) 的视频的相关技术信息

短文。

首先说《今すぐに》。

这是专辑的第二主打歌(自封),个人很喜欢的藤田麻衣子创作。在发售前两天公开的后来补拍的MV——YouTube有完整版,但是iTunes/mora等也有在卖。

本来没准备买,毕竟mora和iTunes都写的是SD画质那我还不如直接看YouTube,结果看到推特有人提iTunes其实有高清画质。本着“肯定是upscale的吧”去买了个结果挺惊喜,确实是高清画质(呃,很难说是否原生1080,但至少秒了mora和YouTube版)。

iTunes下载的时候要注意勾选一个下载最高清画质的选择,否则会下载720p(事实上由于我勾选的时候它已经开始下载,第一次下载到了720p的。我删了重下才下到1080p)。iTunes的版本其实是anamorphic的1440×1080,V+A~=5Mbps。

另外iTunes是真tm难用。

23.976fps录制,但是mora版和iTunes都4重复1插帧到29.97。

mora的SD版则出了和之前在《フワリ、コロリ、カラン、コロン》遇到过的同样问题,视频比例不对——虽然看似是854×480,但是实际变宽了,两侧有出血。根据上次的分析,应该是mora总是会按照1.78:1->1.8:1(索尼系DVD应该要有的操作)进行一次比例変换(然后切掉两侧),但是如果提供的raw本身就已经是16:9的话就会变换过度,变成现在这样。

下面图片对比:

歌曲《Agapanthus》的MV是BD,所以没啥好说。23.976制作。

比较容易令人纠结的自然是那个DVD带的Peachy! live的影像。

按照惯例索尼应该是1.8:1了,但是我们还是要验证下的。唯一可以验证的资源就是当年もちょ直播时放的花絮片段了。结果我一看,几乎所有镜头的分镜都和DVD版完全不一样——这怎么比嘛?

又仔细看了半天,发现有几个镜头其实是一样的,但是使用了不同的crop。我费了半天劲找出来三个镜头,对比了下。先说结论:确实DVD还是应该用1.8:1才和YouTube一致。

下面帖对比图。

场景1,原图(DVD拉伸到720p便于对比,下同):

YouTube版做纯Crop,DVD调整比例后对比:

aaa

场景2,原图:

DVD版crop、改变比例后对比(下面加了白边便于和YouTube版高度一致):

aaa

场景3,原图:

对比:

aaa

上面只是客观对比,而且假设YouTube版本正确。

主观判断的话,大部分近景感觉都OK,但是某些远景总感觉用1.8:1会人物太扁,虽然说区别很小就是了。

而且影片最开始有个3DCG的短片,总感觉那个月亮只有16:9才是正圆……

更新:又仔细对比了下。似乎还是1.8:1的月亮更圆!

aaa

总之还是相当纠结。所以DVD怎么还没死??