libass 字幕显示问题修复:384×288分辨率字幕无法在其他分辨率的视频上正确显示 时间: 2021-02-26 14:19 分类: JAVA ####前言 这个问题其实困扰了我很久了,不知大家发现没有,网上下载下来的字幕用`Aegisub`打开发现大多都是`384×288`的分辨率,与片源的分辨率是不同的。 我也不知道为什么字幕组都喜欢用`384×288`分辨率来做字幕,这个分辨率其实就是标准的`4:3`比例的尺寸。 如果片源是`1920×1080`的,在某些基于`libass`的播放器(如VLC、MPV)上显示就会显示不正确,比如字幕脚本如下: ``` [Script Info] ; Script generated by Aegisub 3.2.2 ; http://www.aegisub.org/ Title: Lupin Original Script: FIXSubtitleGroup Synch Point: 1 ScriptType: v4.00+ WrapStyle: 2 Timer: 100.0000 PlayResX: 384 PlayResY: 288 [Aegisub Project Garbage] Last Style Storage: Default Video File: ?dummy:23.976000:36000000:1920:1080:25:101:87: Video AR Value: 1.777778 Video Zoom Percent: 0.718750 Scroll Position: 846 Active Line: 882 Video Position: 64379 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Default,华文黑体,15,&H00FFFFFF,&H00000000,&H00FFFFFF,&H80000000,-1,0,0,0,100,100,0,0,1,0.1,0.4,8,10,10,20,1 Style: 特效,微软雅黑,20,&H00FFFFFF,&HFF0000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1 Style: 注释,微软雅黑,18,&H00DEDEDE,&HFF000000,&H00000000,&H00000000,0,0,0,0,99.9998,100,0,0,1,1.5,0,8,0,0,5,1 Style: 歌词,微软雅黑,13,&H00DEDEDE,&H00000000,&H00000000,&H00000000,0,0,0,0,99.9998,100,0,0,1,1,0,7,15,5,20,1 Style: 特效 - 背景,微软雅黑,20,&H00FFFFFF,&HFF0000FF,&H00000000,&H00000000,0,0,0,0,99.9998,100,0,0,3,1,0,2,10,10,10,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&H392810&}m 0 0 l 250 0 l 250 344 l 0 344 l 0 0 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&HC6C772&}m 0 41 l 65 41 l 65 111 l 0 111 l 0 41 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&H5E4E11&}m 65 41 l 250 41 l 250 111 l 65 111 l 65 41 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&HCED6C7&}m 0 111 l 250 111 l 250 301 l 0 301 l 0 111 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&HD2D6D1&}m 6 123 l 108 123 l 108 263 l 6 263 l 6 123 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\pos(12,26.5)\a4\p1\fscx30\fscy40\c&H82791E&\1a&H8D&}m 6 248 b 14 243 23 238 33 234 l 30 205 l 28 200 b 25 199 25 197 25 193 b 23 190 24 186 24 183 b 24 182 24 180 25 179 b 25 177 22 174 23 172 b 24 170 25 171 26 173 b 26 165 27 156 30 151 b 33 141 42 132 57 129 b 77 129 85 138 89 153 b 91 159 92 166 92 172 b 94 172 94 174 94 176 b 93 178 92 180 92 182 b 93 185 93 189 93 193 b 91 197 91 200 89 201 b 88 209 86 215 83 221 b 83 227 82 235 86 236 b 93 237 101 239 108 242 l 108 263 l 6 263 l 6 248 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\fn华文中宋\b1\fs16\pos(58.971,65)\fsp1\c&HCED6C7&}卢浮{\fsp0}宫 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\a5\fn方正兰亭准黑_GBK\\fs7\c&H5F500F&\pos(47.2,75)}室内维护{\fs4}\N\N{\fs7}及保洁\N\N\N清洁工\N\N\N\N\N\NLVR716-ACC Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\a3\fn方正兰亭准黑_GBK\\fs7\c&H5F500F&\pos(84.5,145)}有效证件\N{\b0\fn方正兰亭中黑_GBK}31/12/2020 Dialogue: 0,0:00:44.94,0:00:48.42,特效,,0,0,0,,{\a1\fn方正兰亭准黑_GBK\fs10\c&H5F500F&\pos(14,145)}路易·佩雷纳 ``` 显示效果如下: ![aaa.mp4_20210226_123926.294.jpg](https://0o0.me/usr/uploads/2021/02/154483341.jpg) 很明显,左边的特效字幕背景宽度是扁的,为什么会这样?因为字幕组是用`384×288`的分辨率做的字幕,源视频为`1920×1080`,因此代码里背景宽度做了缩放`\fscx30\fscy40`,用`Aegisub`将脚本重设分辨率为`1920×1080`选择拉伸,你会发现宽度会变成`\fscx40\fscy40`,如此一来再用`VLC`或者`MPV`播放器挂载会发现变正确了: ![aaa.mp4_20210226_124504.952.jpg](https://0o0.me/usr/uploads/2021/02/2579169380.jpg) 但是,这真的是脚本分辨率的问题导致的吗? 其实不是的,如果你用`PotPlayer`挂载`384×288`分辨率的字幕,发现它显示也是正确的。 亦或者使用`MEGUI`将其压制内嵌到视频中去也是正常的,因为`MEGUI`使用的是`VSFilter`进行的字幕渲染。 但是在`linux`系统上我们无法使用`VSFilter`压制硬字幕,只能选择`libass`,发现压制出来的字幕效果也是如第一张图所示,可以说目前基于`libass`的所有播放器和`ffmpeg`都有此问题。 正如前不久倒下的人人字幕组,他们的字幕几乎都是`384×288`分辨率的,因此在`github`上也有人提出了相同的`issue`:[#171](https://github.com/libass/libass/issues/171 "#171") 但是里面的开发人员好像并没有意识到根本不是评论上`astiob`说的问题导致的。 下面来分析下究竟为什么会如此,以及该如何解决这个问题。 ####问题分析 首先,我们肯定是想尽可能的不动源码,修改运行参数能不能将其正确显示,这里指的是`ffmpeg`,如果你是基于`libass`开发播放器也和`ffmpeg`差不多,因为`ffmpeg`也是调用的`libass`库。 网上搜索了一番,发现可以设置`original_size`参数来指定压制时视频分辨率,不设置时默认值源视频分辨率。 我们试一下,将其指定为字幕脚本分辨率`384×288`,再次压制会发现特效背景的宽度正常了,但是字幕的文字也被拉伸了,可以通过以下两张图进行对比: 默认为视频分辨率时: ![a.jpg](https://0o0.me/usr/uploads/2021/02/3830444527.jpg) 指定`original_size=384×288`时: ![b.jpg](https://0o0.me/usr/uploads/2021/02/815242020.jpg) 可以看到,第一张图文字显示正常,没有被水平拉伸,第二张图背景正常,文字被拉伸了。 仔细分析一下,你会发现,指定`original_size=384×288`时,其实就是以`384×288`的分辨率进行渲染字幕,然后把它`overlay`覆盖到`1920×1080`的视频上,它的处理方式是:保持高度比例不变,将宽度进行拉伸,因此也就是图一的背景拉伸拉伸了。 因为字幕脚本分辨率虽然是`384×288`,但制作时对应的片源是`1920×1080`,所以脚本中是对背景宽度进行了处理,也就是:`\fscx30\fscy40`,它在`4:3`的视频上其实跟图一是一样的宽度被压缩了,这是正常的,但是如果在`16:9`的视频上`VSFilter`却能做到自动将宽度拉伸至正常,`Aegisub`中预览也是正常显示。 其实不难发现,为什么设置了`original_size=384×288`时,文字被拉伸了,因为文字没有指定`\fscx`与`\fscy`,也就是原始字幕样式中的`\fscx100\fscy100`。指定`original_size=384×288`时,整个过程就是: 按照字幕脚本里的字幕顺序,先是渲染背景,背景本来是`\fscx30`,此时渲染出来的结果其实就如图一所示,只不过因为设置了`original_size`与脚本分辨率一致,将其压制到`16:9`的视频上时,会将宽度进行拉伸,就相当于先把一张`4:3`的图片高度不变,拉伸成`16:9`,这个时候背景图片的宽度就被拉伸看起来正常了。 不过在渲染文字的时候,由于文字没有指定`\fscx\fscy`进行缩放,也可以理解为它是正方形的,渲染时先是以`4:3`的尺寸渲染,然后拉伸至`16:9`,本来是正常的,一拉伸文字肯定也跟着拉伸了。 所以并不能通过`original_size`来修正此错误,压制时也建议不要去指定该参数。 其实这里关于图像的拉伸,涉及到了`DAR`、`SAR`、`PAR`,这里不做过多赘述,有兴趣可以百度,我们只需要知道下面公式即可: > DAR = SAR x PAR 在这里,`DAR`其实就是源视频的分辨率,`SAR`为脚本分辨率,也就是采样纵横比。后面再讲如何利用它去解决这个问题。 放弃`original_size`,再次回到第一张图,可以发现文字正常,就是特效背景宽度是错误的。 因此可以确定为`libass`对于脚本分辨率与源视频分辨率不同的时候,对`\fscx`的处理不当导致的,也就是对`\fscx`没有进行缩放。 问题找到了,下面说下如何解决。 ####问题解决 知道了是`fscx`的问题,于是直接`github`源码中搜索`fscx`,可以发现只有一处在`libass/ass_parse.c`中: ``` } else if (tag("fscx")) { double val; if (nargs) { val = argtod(*args) / 100; val = render_priv->state.scale_x * (1 - pwr) + val * pwr; val = (val < 0) ? 0 : val; } else val = render_priv->state.style->ScaleX; render_priv->state.scale_x = val; } else if (tag("fscy")) { ``` 在此处,我们利用本身自带的日志命令`ass_msg`将其打印出来,发现其确实没有对它进行缩放,当然了,这也可能只是简单的从字幕脚本中读取出来,还没进行处理。 可以看到`fscx`保存到了`render_priv->state.scale_x`属性中,继续搜索`state.scale_x`,在`libass/ass_render.c`中找到两处代码: ``` render_priv->state.scale_x = style->ScaleX; render_priv->state.scale_y = style->ScaleY; render_priv->state.hspacing = style->Spacing; ``` 但这是在`reset_render_context`方法中,所以这是重设值,可以忽略。还有一处在`parse_events`方法中: ``` info->scale_x = render_priv->state.scale_x; info->scale_y = render_priv->state.scale_y; ``` 很明显了,这就是处理字幕了,但是它也只是简单的赋值。 下面我们来修改此处的代码修复这个问题。 前面说到了`DAR`、`SAR`、`PAR`,我们先来计算`PAR`,在这里`DAR`为`源视频宽/源视频高`,源视频宽高通过查找发现保存在`render_priv->orig_width`与`render_priv->orig_height`中,`SAR`为`脚本分辨率`,脚本的分辨率由`PlayResX`与`PlayResY`在脚本中定义,保存在`render_priv->track->PlayResX`与`render_priv->track->PlayResY`中,因此得到: > double dar = ((double) render_priv->orig_width) / render_priv->orig_height; >double sar = ((double) render_priv->track->PlayResX) / render_priv->track->PlayResY; 计算`PAR = DAR / SAR`: > double par = dar / sar; 然后我们修复`info->scale_x = render_priv->state.scale_x;`的值: > info->scale_x = render_priv->state.scale_x * par 但是,上面的代码是有问题的,你会发现它的效果与设置`original_size=384×288`一样,文字的宽度也被拉伸了。 在这个例子中,我们分析一下看对不对: dar = 1920 / 1080 = 16 / 9 sar = 384 / 288 = 4 / 3 par = dar / sar = 4 / 3 fscx30 * par = fscx40 发现与利用`Aegisub`重设脚本分辨率为`1920×1080`的一致,因此没有问题。 对于没有指定`fscx`的字幕行,其值为脚本样式里的`ScaleX`的值,这里为100,`ScaleX`与`ScaleY`都未100,也就是个正方形的。 所以对于正常字幕行,我们也会将其进行修复: fscx100 * par = 133.333... 而文字的高还是`fscy100`,所以出现了文字被拉伸的效果。 正常情况下,文字通常都是`fscx100`、`fscy100`显示的,也就是正方形显示,因此当`fscx=100`、`fscy=100`时,无论在`4:3`还是`16:9`的视频中,我们都希望它是正方形显示,因此代码可以修改为如下: > info->scale_x = render_priv->state.scale_x * ( render_priv->state.scale_x != 1. && render_priv->state.scale_y != 1. ? par : 1.); 再次重新编译测试,效果与`VSFilter`一致。 ####总结 其实这个问题不难分析,关键是理解`DAR`、`SAR`、`PAR`三者的关系,然后就是通过`fscx`、`PlayResX`快速定位到问题所在的代码,最后就是文字被拉伸的坑需要注意一下。 花费我更多时间的是`libass`编译到`ffmpeg`中去的问题,下篇文章再记录了。 标签: 无