九月半的笔记

笔记

实况图翻车记录

博客里折腾实况图时,配套视频在部分浏览器里横过来的排查和处理。

实况图Live PhotoFFmpeg浏览器兼容折腾

最近给博客加实况图,本来以为最麻烦的地方会是上传、压缩、封面和视频怎么绑在一起,结果真正卡住我的反而是一个很莫名其妙的问题:同一段竖屏视频,在 Chrome 和 Edge 里都好好的,到了 ColorOS 自带的欢太浏览器里,画面直接横过来了。

封面图是正的,横屏 Live 也正常,只有竖屏视频一播放就不对。那种感觉就很微妙,代码看起来没错,文件看起来也没错,只有浏览器在很认真地告诉你:我有自己的想法。(左图是封面,视频文件还在加载中,右图是视频播放中)

72507e11b4c9cb4ba62bf4eb5f6d4244-1777104848899 60e0027e62dd9e1451afbf1faca5243c-1777104856741

一开始我以为是 rotation: 90 这个信息没被读到。后来又对比了几段视频,感觉这个判断不一定完全准。横屏视频不管手机充电口朝右拍,还是朝左拍,在欢太浏览器里都没问题;真正翻车的是竖屏视频。也就是说,它未必是完全没读 rotation,更像是某个环节先按视频宽高比例判断了方向,后面再和旋转信息没对齐。

要理解这个问题为什么容易出现,还得从竖屏视频本身说起。以前视频更多是电视、电影、相机这类场景,默认就是横屏,编码和播放链路也基本围绕横屏来设计。到了手机时代,大家开始天天竖着拿手机拍视频,但底层的传感器、编码器,还有很多播放器逻辑,仍然带着过去那套横屏习惯。

所以手机厂商为了录制性能和处理速度,很多时候不会在拍摄时把每一帧都真的转成竖着的像素,而是继续按更方便处理的横向画面去编码,再在 MP4 里面写一个方向提示,也就是类似 rotation: 90 的标识。大概意思是:文件里的画面先这样存,播放的时候记得帮我转一下。

所以一个看起来是竖屏的视频,在文件内部可能长这样:

真实编码帧:1920 x 1080
播放时方向:旋转 90 度
最终看起来:1080 x 1920

这套设计其实挺合理的。手机录制视频的时候少做一层旋转处理,性能和链路都会省心一点。播放器如果足够听话,读到方向矩阵之后按它来展示,用户也完全感觉不到中间发生了什么。

问题就在这里:它要求播放器不只是读到方向信息,还要在宽高判断、页面布局、视频解码和最终渲染这几步里都保持一致。

Chrome、Edge 这些浏览器处理得没问题,封面、布局、视频方向都能对上。但一些系统浏览器或者 WebView 里,视频这条链路可能不是同一套逻辑走到底。也许前面按 videoWidth / videoHeight 或原始编码帧宽高判断它是横屏,也许后面又读到了 rotation,也许硬解那里还有自己的处理方式。只要其中一步没对齐,最后落到页面上,就可能变成封面是竖的,框也是竖的,但视频画面一开始动就横了。

这个问题最烦的地方是,它不是那种一眼能修的 bug。你不能简单写一句 CSS 旋转一下,因为在正常浏览器里它又会被你转歪。你也不能只改 metadata,重新写个 rotate=90,因为出问题的浏览器本来就可能是不稳定地处理这个信息。它愿意听的时候都正常,不愿意听的时候你再写一遍也没什么用。

后来我干脆换了思路:不让浏览器猜了,直接把方向烙进视频像素里。

也就是用 FFmpeg 重新转一遍。输入文件如果是“横向编码帧 + 旋转提示”,转码时让 FFmpeg 先按这个提示把画面摆正,再输出一个真正竖屏尺寸的 MP4。处理完之后文件就变成:

真实编码帧:720 x 1280
播放时方向:不用额外旋转

这样浏览器就不用理解什么 display matrix,也不用在播放时猜方向。它只需要做一件最普通的事:播放一个正常的 H.264 MP4。

我后来把这套逻辑放进了 CMS。实际上传的时候不是无脑把所有视频都重压一遍,而是先让浏览器读一下视频的展示宽高。如果读出来是横屏,并且没有勾选“横屏也转码压缩”,就直接上传原文件;如果是竖屏,或者我手动要求横屏也压缩,才会拉起 FFmpeg 处理。

CMS 里现在有几个预设:均衡是 720 / CRF 24 / 30fps / 96k 音频,清晰是 1080 / CRF 21,小体积是 540 / CRF 28。平时我基本用均衡档,够清楚,文件也不会太夸张。

它最后拼出来的 FFmpeg 参数大概是这样:

ffmpeg -i input.mov \
  -map 0:v:0 -map 0:a? \
  -vf "scale=w=min(720\,iw):h=-2,setsar=1,format=yuv420p" \
  -r 30 \
  -c:v libx264 -preset veryfast -crf 24 \
  -profile:v main -level 4.0 \
  -c:a aac -b:a 96k -ac 2 -ar 44100 \
  -map_metadata -1 -map_chapters -1 \
  -metadata:s:v:0 rotate=0 \
  -movflags +faststart \
  output.mp4

这里不是为了追求参数有多漂亮,主要是把几个容易出问题的地方提前收掉。scale 控制尺寸,避免一张实况图视频动不动就很大;setsar=1 把像素比例归一,少一点奇怪的宽高比问题;format=yuv420plibx264 是为了让输出尽量普通,普通到大部分浏览器都能直接播;rotate=0 是把方向问题在转码阶段解决掉,别再留一个旋转提示给播放器猜;+faststart 则是让 MP4 在网页里更快开始加载。

上传链路里还做了几层小保险。比如会扫视频头里的 hvc1hev1avc1 这些标识,检测到 HEVC / H.265 就直接拦下来,因为这个格式在网页预览里太容易翻车。iPhone 单视频上传时,CMS 会从视频里截一帧当封面;安卓 Motion Photo 则会先从 JPG 里把封面和内嵌 MP4 拆开,封面再用 canvas 过一遍方向,视频再走同一套 FFmpeg 归一化。最后上传到 R2 的时候,已经是封面图和视频分开的两份资源,再生成一段 <LivePhoto cover="..." video="..."></LivePhoto> 给文章用。

这个方案肯定不算最省事,也不一定最省体积,但对博客来说挺合适。文章里的实况图不是拿来做无损存档的,重点是别人打开页面时能正常看,不要在某个浏览器里突然横着躺下。

顺便还遇到另一个问题。QQ 浏览器这类带 TBS/X5 内核的安卓浏览器,会对页面里的 video 做自己的处理。我给视频加了 playsinlinewebkit-playsinlinex5-playsinlinex5-video-player-type="h5",也禁了画中画和远程播放,但实际表现还是不够稳定:有时视频层会被接管,盖到封面上面;有时点击之后又会走浏览器自己的播放提示,甚至跳到单独页面播放。

折腾到这里我就不想硬刚了。展示端现在直接检测 Android 上的 QQBrowser / MQQBrowser / TBS,命中后进入封面模式:关掉自动播放和循环,移除 video 的 src,只保留封面。这样至少页面不会坏,也不会给读者一个半能播半不能播的奇怪体验。

本文专业性问题,靠 AI 搜索专业文献后总结整理,理解上如果有偏差,请大佬指正

最后给自己记个结论:

如果视频只是自己看,保留 rotation metadata 一般没问题。
如果视频要放到网页里给各种浏览器播放,尤其还要照顾系统浏览器和 WebView,最好把方向直接转进像素里。

这次折腾下来最大的感受是,网页里的视频兼容性比图片麻烦多了。图片基本就是给一个地址,浏览器展示出来;视频中间还隔着编码、封装、方向矩阵、硬解、内联播放、浏览器策略。每一层都可能很合理,但叠在一起就会出现一些很不好解释的现象。

所以跟视频相关的东西,把麻烦尽量留在上传阶段,别把播放效果交给浏览器临场发挥。浏览器能不能正确处理这些细节不太确定,能提前处理掉就提前处理掉。

最后放两张live图

评论区