摘要
做内容的人大概都见过这种画面。一段竖着拍的视频点开播放是正的,素材列表里那张封面却横着躺在那儿。换一个工具转一遍格式,画面又可能被压成一张扁脸。我平时给同事搭内部小工具,素材从手机到网页要过好几道手,这种事隔一阵就冒出来一次。这回我想把它一次弄明白:方向到底存在哪、一路上谁认它谁不认。
先说结论。竖拍视频常见的存法是画面躺着存,文件里另外记一条「播放时转 90 度」的旋转标记。我管它叫便签。我拿网上找的示例视频做了几段带便签的对照样片,把能想到的读法挨个试了一遍。四个浏览器播放 MP4 和 MOV 都认便签,把画面画到画布上取帧也都认。不认的集中在五处:生成位图的接口有两个浏览器不对,直接取帧对象在 Firefox 151 里躺倒,自己拆包解码四个浏览器全躺,带便签的 WebM 在 Firefox 151 和 WebKit 26.5 里躺倒,转码时把宽高写死或者只清便签也会出事。全文按环节给了对照表,最后合成一张总表。
版本声明
我是 2026-10-12 这天试的,用的电脑是 Apple Silicon 的 Mac,系统 macOS 26.5。浏览器有四个:Chromium 149.0.7827.55、Chrome 154.0.8037.98、Firefox 151.0 和 WebKit 26.5(macOS)。WebKit 26.5 是 macOS 上的构建,不是 iPhone 上的 Safari。转码用的是 ffmpeg 7.1,读帧用的是走 FFMPEG 后端的 OpenCV 5.0.0。拆 MP4 外包装用的是 mp4box.js 0.5.2。文中说某个浏览器或工具认不认便签,都只对这几个版本成立。
适用边界
样片不是手机原片。它们是拿网上找的两段示例视频用 ffmpeg 加上旋转标记做出来的对照样片,编码全部是 H.264。手机直出的文件、HEVC 编码、iPhone 上的 Safari 和安卓浏览器我都没测。任何视频平台和云转码服务我都没碰,这篇不回答「传到某某平台会不会横」。OpenCV 只测了 5.0.0 一个版本。在线压缩那一节只在 Chromium 149 里测了「体积优先」一个档位,样片只有 5 到 6 秒。照片也有类似的方向记号,那是另一套东西。
文章目录
一、竖拍视频的方向到底存在哪
- 1.1 竖拍画面可以躺着存再记一个角度
- 1.2 旋转标记记在文件的外包装上
- 1.3 对照样片是用ffmpeg做出来的
二、怎么看一个视频带不带旋转标记
- 2.1 ffmpeg能直接读出旋转标记的角度
- 2.2 带标记的文件打印的是存储尺寸
- 2.3 转180度的标记从宽高上看不出来
三、浏览器播放竖拍视频认不认旋转标记
- 3.1 四个浏览器播放MP4和MOV都是正的
- 3.2 浏览器报的宽高是转正后的
四、网页上截取视频帧谁认旋转标记
- 4.1 网页上取一帧常见的有五种办法
- 4.2 画到画布上四个浏览器都是正的
- 4.3 生成位图这条路有两个浏览器不对
- 4.4 直接取帧对象在Firefox 151里躺倒
- 4.5 自己拆包解码四个浏览器全是躺着的
- 4.6 取帧办法和浏览器的完整对照表
五、带旋转标记的WebM哪些浏览器认
- 5.1 ffmpeg能把旋转标记带进WebM
- 5.2 Firefox和WebKit播放时画面躺倒
六、转码和压缩工具读不读旋转标记
- 6.1 ffmpeg默认先按标记把画面转正
- 6.2 宽高都写死会把竖画面压成横的
- 6.3 改元数据里的rotate清不掉标记
- 6.4 只清标记不重编码画面全躺倒
- 6.5 ffmpeg十二条命令的对照表
- 6.6 OpenCV默认按标记转正也可以关
- 6.7 浏览器里压完的成品不带旋转标记
七、哪些环节可以放心哪些要留神
- 7.1 播放和画到画布可以放心
- 7.2 换取帧接口和转WebM要留神
- 7.3 我收到竖拍素材先转成像素竖的
八、适用边界与风险
九、测量方法的坑
十、还没解决的
参考资料
一、竖拍视频的方向到底存在哪
这一章回答最底下的那个问题:一段竖着拍的视频,把「竖」这件事记在文件的什么地方。后面所有的对照都建立在这个答案上。
1.1 竖拍画面可以躺着存再记一个角度
我原来以为竖拍视频就是一张张竖着的画面。实际有两种存法。第一种最直白:每一帧的像素本身就是竖的,宽 1080 高 1920。第二种绕一点:每一帧的像素是横着躺倒存的,宽 1920 高 1080,文件里另外记一句「播放的时候请转 90 度」。播放器读到这句话,就在显示之前把画面转正。两种存法在认这句话的播放器里看起来一模一样。
这句话的正式名字叫旋转标记,我更愿意把它想成贴在文件上的一张便签。画面是一摞躺着放的照片,便签上写着「看的时候竖过来」。认真读便签的人看到的是正的。不看便签、直接抽一张照片出来的人,拿到的就是躺着的那张。后面我就用「认便签」和「不认便签」来说每个环节的表现。
便签上能写的角度不止一个,我做了三种:转 90 度、转 270 度和转 180 度。前两种画面是躺着存的,区别在头朝左还是头朝右。第三种画面是倒立着存的,宽高和正的一样。180 度这种后面会单独拎出来说,它最不容易被发现。
1.2 旋转标记记在文件的外包装上
这一节说便签具体贴在哪。先分清两层东西。视频文件像一个快递盒,盒子本身叫容器,MP4 和 MOV 都是容器的名字。盒子里装的是压缩过的画面数据,H.264 是其中一种压缩方式。把盒子拆开取出画面数据叫解封装,我后面叫它拆包。把压缩的画面数据还原成一帧帧能看的图叫解码。
便签贴在盒子上,画面数据里没有它。MP4 和 MOV 的盒子里有一块叫 tkhd 的区域,可以理解成「这条画面轨道的说明卡」。说明卡上有九个数,合起来叫矩阵,用来描述画面显示时要怎么摆。我用 mp4box.js 这个专门拆 MP4 的库把它读了出来。没有便签的样片读到的是 [65536, 0, 0, 0, 65536, 0, 0, 0, 1073741824]。我查了资料才知道,65536 在这种写法里代表 1,这一组数的意思是原样摆放。带 90 度便签的样片读到的是 [0, 65536, 0, -65536, 0, 0, 0, 0, 1073741824]。前四个数换了位置,还多了个负号,这就是「转 90 度」的写法。
知道便签贴在盒子上之后,很多现象就好理解了。负责解码的那一环只拿到画面数据,它根本看不到盒子上写了什么。谁负责拆盒子,谁就得负责读便签,再决定要不要把画面转过来。拆盒子的人不读,后面的人想读也没处读。mp4box 读到的轨道宽高是 1920×1080,这是存储尺寸,不是显示尺寸。
1.3 对照样片是用ffmpeg做出来的
样片我是这么准备的。源片是维基共享资源上的两段视频,像素本身是竖的、没有便签。一段是 Grendelkhan 发布的垂直轴风机,1080×1920。另一段是 Canbel 发布的下雪街景,720×1280 带声音。两段都是 CC BY-SA 4.0 许可。我先在画面左上角画一个红块、右上角画一个绿块,各 160×160。有了这两个色块,程序和人眼都能一下看出画面哪头朝上。
然后用 ffmpeg 分两步做带便签的版本。ffmpeg 是一个命令行的音视频处理工具,转格式、截图、改参数都靠它。第一步把像素真的转成躺着的。第二步不动画面,只在盒子上贴便签。
# 第一步:画色块,再把像素逆时针转 90 度,得到一段躺着的画面
ffmpeg -i wind_src.webm -t 5 -r 30 \
-vf "drawbox=x=0:y=0:w=160:h=160:color=red:t=fill,drawbox=x=iw-160:y=0:w=160:h=160:color=lime:t=fill,transpose=2" \
-an -c:v libx264 lying.mp4
# 第二步:画面原样照抄,只贴一张「转 90 度」的便签
ffmpeg -display_rotation -90 -i lying.mp4 -c copy wind_note90.mp4
代码说明。第一条命令里的 drawbox 是画色块,transpose=2 是把像素逆时针转 90 度。这一步做完,文件里的画面和播放出来的画面都是躺着的。第二条命令里的 -c copy 意思是画面数据一个字节都不重新压。-display_rotation -90 是告诉 ffmpeg 给这个输入贴上角度。两步合起来就得到「画面躺着存加一张便签」的样片。换成 transpose=1 配 -display_rotation 90 是 270 度那种。把画面上下左右都翻过来再配 -display_rotation 180 是倒立那种。
最后一共七段样片。风机有五段:不带便签的、90 度的 MP4 和 MOV 各一段、270 度的、180 度的。它们都是 5 秒、每秒 30 帧,没有声音。下雪有两段:不带便签的和 90 度的,都是 6 秒带 AAC 音轨。这里要老实说一句,这些便签是 ffmpeg 贴的,不是手机贴的。方向和手机竖拍常见的那种一致,可它终究是我做出来的对照样片。这七段后面要挨个喂给四个浏览器、ffmpeg 和 OpenCV,最后还要过一遍在线压缩,那一步我用的是图映 ImgIng(imging.cn/)的视频压缩,只在 Chromium 149 里跑。
二、怎么看一个视频带不带旋转标记
这一章回答一个很实际的问题:手里拿到一个视频,怎么知道它是哪种存法。不先弄清这一点,后面出了问题都不知道该怪谁。
2.1 ffmpeg能直接读出旋转标记的角度
最省事的办法是让 ffmpeg 读一下文件信息。它会打印一大段,我只留宽高和便签两样。
peek() { ffmpeg -hide_banner -i "$1" 2>&1 | grep -oE "[0-9]{3,4}x[0-9]{3,4}|rotation of .* degrees"; }
for clip in wind_up.mp4 wind_note90.mp4 wind_note270.mp4 wind_note180.mp4; do
echo "# $clip"; peek "$clip"
done
# wind_up.mp4
1080x1920
# wind_note90.mp4
1920x1080
rotation of -90.00 degrees
# wind_note270.mp4
1920x1080
rotation of 90.00 degrees
# wind_note180.mp4
1080x1920
rotation of -180.00 degrees
代码说明。ffmpeg -i 文件名 不带输出时只打印文件信息。信息是打在错误输出里的,要用 2>&1 接过来才能筛。grep -oE 只留下像 1920x1080 这样的宽高和带 rotation 字样的那一句。输出里不带便签的样片只有一行宽高。带便签的多出一行 rotation of -90.00 degrees,这就是 ffmpeg 读到的便签。
这里有个容易看晕的地方。我管那段样片叫「90 度」,意思是播放时要顺时针转 90 度才正。ffmpeg 打印出来的却是 -90.00,270 度那段打印的是 90.00。两边说的是同一件事,只是数的方向相反。我一开始对着输出愣了好一会儿,以为自己把样片做反了。后来拿去浏览器里一播是正的,才确认只是正负号的习惯不同。
2.2 带标记的文件打印的是存储尺寸
再看宽高那一行。带 90 度便签的样片打印的是 1920x1080。这是画面躺着存的尺寸,不是你播放时看到的尺寸。播放时看到的是 1080×1920,这两个数我后面分别叫存储尺寸和显示尺寸。
这一点很要命。很多小工具判断横竖的办法就是比一下宽和高谁大。拿 ffmpeg 打印的这行数去比,会把带便签的竖拍视频当成横屏。数没有错。它回答的是「盒子里的画面怎么放的」,不是「人看到的是什么样」。要回答后一个问题,得把下面那行 rotation 一起读进去。本机没有装 ffprobe 这个专门读信息的工具,它的输出我没单独测。
2.3 转180度的标记从宽高上看不出来
180 度那段样片更隐蔽。它的存储尺寸是 1080×1920,和正着存的一模一样。便签上写的是转 180 度。读便签的环节把它转正,不读的环节给你一张头朝下的画面。宽高前后完全一样,比宽高的办法在这里不管用。
我做这段样片本来只是想凑齐三个角度,结果它成了后面每张表里最要留神的一格。躺着的画面一眼就能看出来不对。倒立的画面要是内容本身上下差不多,可能要到有人反馈才发现。后面凡是说「躺倒」的格子,换成 180 度样片就是「倒立」。
三、浏览器播放竖拍视频认不认旋转标记
先看大家最先想到的问题:把带便签的视频直接放到网页上播放,会不会横过来。我原本以为答案是「看浏览器」,实测下来不是。
3.1 四个浏览器播放MP4和MOV都是正的
网页上播放视频用的是 <video> 这个标签。它是浏览器自带的播放器,给它一个文件地址它就能播。我把七段样片挨个塞给它,在四个浏览器里各看一遍,再截图留底。
结果很整齐。不管便签是哪种角度,四个浏览器播放 MP4 和 MOV 都是正的。Chromium 149、Chrome 154、Firefox 151 和 WebKit 26.5 没有一个例外。风机样片、下雪样片和不带便签的样片也都一样。这一步我试着找一个会横过来的组合,没找到。
这和我动手之前的印象不一样。我一直模模糊糊地觉得「某些浏览器播竖拍视频会横」。至少在这四个版本和我这几段样片上,播放这一步复现不出来。要是你遇到的是「点开播放是正的、别处是横的」,问题多半不在播放器,要往取帧和转码那边找。
3.2 浏览器报的宽高是转正后的
播放器除了能播,还能回答「这段视频多宽多高」。网页脚本里读的是 videoWidth 和 videoHeight 这两个属性,意思就是视频的宽和高。带 90 度便签的风机样片存储尺寸是 1920×1080,四个浏览器报出来的都是 1080×1920。
也就是说浏览器报的宽高是转正后的显示尺寸。这跟上一章 ffmpeg 打印的存储尺寸正好是两套数。同一个文件,问 ffmpeg 是 1920×1080,问浏览器是 1080×1920,两边都没说错。写小工具的时候要是前端用一套、后端用另一套,封面的框就会开错方向。下雪样片也一样:存的是 1280×720,浏览器报的是 720×1280。
四、网页上截取视频帧谁认旋转标记
这一章是全文最长的一章,回答的是「播放是正的、为什么截出来的封面是横的」。播放只有一条路。从视频里取一帧画面出来却有好几条路,每条路读不读便签各不相同。
4.1 网页上取一帧常见的有五种办法
先把五种办法用人话过一遍。第一种是直接看播放器,上一章已经试过。第二种是把播放器当前的画面画到画布上。画布在网页里叫 canvas,可以理解成一张能往上画东西、再导出成图片的白纸。画的动作叫 drawImage,做视频封面最常见的就是这一种。
第三种叫 createImageBitmap,意思是「从视频当前画面生成一张位图」。位图就是一张存在内存里的图片,后面想画到哪儿都行。第四种叫 new VideoFrame(video),是从播放器当前画面直接拿一个「帧对象」。帧对象比位图多带一些信息,比如这一帧的编码尺寸和显示尺寸。第五种最底层:不用播放器,自己用 mp4box.js 把盒子拆开,再把画面数据一块块喂给 VideoDecoder。VideoDecoder 是浏览器提供的解码接口,只管把压缩数据还原成画面。
前四种都经过播放器,第五种完全绕开了它。按第一章的结论,第五种是自己拆的盒子,便签读不读全看自己。下面这段是我测的时候用的核心几行,每种办法取到一帧之后都记下宽高。
async function grabAll(player) {
const sheet = {};
sheet.player = [player.videoWidth, player.videoHeight];
const paper = document.createElement('canvas');
paper.width = player.videoWidth; paper.height = player.videoHeight;
paper.getContext('2d').drawImage(player, 0, 0);
sheet.canvas = [paper.width, paper.height];
const pic = await createImageBitmap(player);
sheet.bitmap = [pic.width, pic.height];
const shot = new VideoFrame(player);
sheet.frame = { stored: [shot.codedWidth, shot.codedHeight],
shown: [shot.displayWidth, shot.displayHeight],
note: shot.rotation };
shot.close();
return sheet;
}
// 带 90 度便签的风机样片,Firefox 151
player: [1080, 1920]
canvas: [1080, 1920]
bitmap: [1920, 1080]
frame: { stored: [1920, 1080], shown: [1920, 1080], note: undefined }
// 同一段样片,Chrome 154
player: [1080, 1920]
canvas: [1080, 1920]
bitmap: [1080, 1920]
frame: { stored: [1920, 1080], shown: [1080, 1920], note: 90 }
代码说明。grabAll 接收一个已经加载好并且停在第 1 秒的播放器。它依次用四种经过播放器的办法各取一次,再把宽高记进 sheet。codedWidth 和 displayWidth 分别是存储尺寸和显示尺寸的宽。rotation 是帧对象自己带的角度。光记宽高还不够,我另外在每张取到的图上读四个角的颜色,看红块和绿块落在哪个角,这样才能判出是正的、躺的还是倒立的。输出里 Firefox 151 的位图和帧对象的显示尺寸都是 1920×1080,没有 rotation 这个属性。Chrome 154 两样都是竖的,rotation 是 90。
4.2 画到画布上四个浏览器都是正的
先说好消息。把播放器画面画到画布上这一种,四个浏览器都是正的,三种角度的便签全认。画布的宽高按浏览器报的 1080×1920 来开,画出来红块在左上、绿块在右上。MP4 和 MOV 一样,风机和下雪也一样。
这大概是最多人在用的取封面办法,它在我这轮测试里没出过一次错。我本来以为封面躺倒的锅会落在它头上。要是封面是用这条路取的还躺着,我会先去查画布的宽高是从哪儿来的。宽高要是来自后端按存储尺寸报的那套数,画布就开成了横的。
4.3 生成位图这条路有两个浏览器不对
换成 createImageBitmap,情况就不整齐了。Chrome 154 和 WebKit 26.5 给的位图是正的 1080×1920。另外两个浏览器不对,而且错得不一样。
Firefox 151 给的位图是画面躺倒的 1920×1080。这个好理解:它没读便签,把躺着存的那一帧原样交了出来。180 度样片在它这里是倒立的。
Chromium 149 的表现我第一眼没看懂。位图也是 1920×1080,可红块还在左上、绿块还在右上,画面是正的。正的竖画面怎么会装在一张横的图里?对着截图看才明白,它是把转正之后的竖画面硬塞进了横着的框,整个画面被压扁了。原来 160×160 的方色块变成了大约 284×90 的扁条。Chrome 154 跑同一段代码给的是 1080×1920,色块还是 160×160。Chromium 149 对 180 度样片倒是正的,那种样片宽高本来就没变。这两个版本为什么不一样,我不知道,这里只记现象。
压扁比躺倒更难靠程序发现。躺倒的时候色块跑到了别的角上,一查就知道。压扁的时候色块还在原来的角,只看「哪头朝上」会判成正常。得同时看宽高比才能抓住它。
4.4 直接取帧对象在Firefox 151里躺倒
new VideoFrame(video) 这一种,四个浏览器给了三种说法。Chromium 149 和 Chrome 154 的帧对象写得最清楚:编码尺寸 1920×1080,显示尺寸 1080×1920,另外带一个 rotation 属性,三种样片上分别是 90、270 和 180。把帧对象画出来是正的。等于它把便签抄了一份带在身上,还告诉你它已经按便签摆好了。
WebKit 26.5 的帧对象显示尺寸是 1080×1920,画出来是正的,只是身上没有 rotation 这个属性。结果对,说明少一点。Firefox 151 的帧对象显示尺寸是 1920×1080,也没有 rotation 属性,画出来躺倒。只有它交出来的是没转过的画面。
这里有个小坑我顺手记一下。要是代码里写了「读到 rotation 就自己再转一次」,在 Chrome 上要小心转重了。它交出来的帧已经是正的,rotation 只是告诉你它原来的角度。转重了是什么样我没试,只是看到这组数的时候想到了这一层。
4.5 自己拆包解码四个浏览器全是躺着的
最后是完全绕开播放器的那条路:用 mp4box.js 拆盒子,再把画面数据喂给 VideoDecoder。这条路四个浏览器全是躺着的。拿到的帧是 1920×1080,红块在左下、绿块在左上,头朝左。Chromium 149 和 Chrome 154 的帧对象上 rotation 是 0。180 度样片在这条路上四个浏览器全是倒立的。
这正好对上第一章说的那件事。便签贴在盒子上,解码器只收到画面数据,没办法知道有便签。盒子是我用 mp4box.js 拆的,便签也在我手里,就是前面读出来的那九个数。我不读,后面就没人读了。这条路本来就是这样,四个浏览器都一样,走这条路的工具得自己读矩阵,再自己把画面转过来。
走这条路的一般是要逐帧处理视频的工具,比如在浏览器里做剪辑和压缩的那一类。普通做个封面用不到它。可一旦用了,方向就完全是自己的事。
4.6 取帧办法和浏览器的完整对照表
把上面几节合成一张表。对象是带便签的样片。三种角度、MP4 和 MOV 的结果一致。
| 取法 | Chromium 149 | Chrome 154 | Firefox 151 | WebKit 26.5 |
|---|---|---|---|---|
| 播放器播放 | 正 | 正 | 正 | 正 |
| 报的宽高 | 1080×1920 | 1080×1920 | 1080×1920 | 1080×1920 |
| 画到画布 | 正 | 正 | 正 | 正 |
| 生成位图 | 1920×1080 正但压扁 | 1080×1920 正 | 1920×1080 躺倒 | 1080×1920 正 |
| 取帧对象 | 显示 1080×1920 正 | 显示 1080×1920 正 | 显示 1920×1080 躺倒 | 显示 1080×1920 正 |
| 帧对象的 rotation | 90 / 270 / 180 | 90 / 270 / 180 | 没有这个属性 | 没有这个属性 |
| 拆包再解码 | 躺倒 | 躺倒 | 躺倒 | 躺倒 |
这张图是 Chrome 154、Firefox 151 和 WebKit 26.5 三个浏览器各取五次的结果。样片是带 90 度便签的风机。看每张小图的红块和绿块就行:红在左上、绿在右上是正的,绿在左上、红在左下是躺倒。Firefox 151 那一行从第三张开始连着躺了三张,另外两行只有最右边自己拆包解码的那张是躺的。风机画面出自维基共享资源上 Grendelkhan 的视频,CC BY-SA 4.0,色块是做样片时加的方向记号。
表里有两点我想单独提一句。不带便签、像素本身就是竖的那两段样片,在所有取法、所有浏览器里都是正的。也就是说这张表里所有的分歧都是便签带来的,画面本身竖着存就没有这些事。另一点是 Chromium 149 压扁的那一格图里没有,图里放的是 Chrome 154。
五、带旋转标记的WebM哪些浏览器认
这一章的问题我原来压根没想过:便签换一个盒子贴,大家还认不认。前面的盒子都是 MP4 和 MOV。网页上还有一种常见的盒子叫 WebM,里面的画面一般用 VP9 这种方式压缩。
5.1 ffmpeg能把旋转标记带进WebM
平时用 ffmpeg 转 WebM,它默认会先按便签把画面转正再压,出来的文件不带便签,这个下一章细说。要得到一个带便签的 WebM,得加一个叫 -noautorotate 的开关,意思是「别自动转、画面原样处理」。
我用这个开关把 90 度的风机样片转成了 VP9 的 WebM。出来的文件存储尺寸还是 1920×1080,画面躺着。再让 ffmpeg 读一遍,它照样打印出 rotation of -90.00 degrees。也就是说 ffmpeg 能把旋转标记带进 WebM,自己也读得回来。便签在 WebM 里具体写在哪个位置我没去拆,这里只能说 ffmpeg 认它。
5.2 Firefox和WebKit播放时画面躺倒
把这个带便签的 WebM 放到播放器里,四个浏览器分成了两派。
| Chromium 149 | Chrome 154 | Firefox 151 | WebKit 26.5 | |
|---|---|---|---|---|
| 播放器播放 | 正,1080×1920 | 正,1080×1920 | 躺倒,1920×1080 | 躺倒,1920×1080 |
这张图是同一个带便签的 WebM 在三个浏览器里直接播放的样子。Chrome 154 是竖的,下面标着 1080 × 1920。Firefox 151 和 WebKit 26 是躺着的,绿块跑到了左上,下面标的是 1920 × 1080。这次躺倒的是播放本身,跟取帧没关系。
这是全文唯一一处「播放就不对」的情况。同一张便签,贴在 MP4 上四个浏览器都认。贴在 WebM 上,Firefox 151 和 WebKit 26.5 播放时画面躺倒,连报出来的宽高都变成了 1920×1080。原因我没去查。我自己记的就一条:带便签的 WebM,不能指望所有浏览器都读。
好在这种文件平时不太容易碰上。ffmpeg 默认转出来的 WebM 是不带便签的 1080×1920,四个浏览器播都是正的。得是有人特意加了 -noautorotate,或者用了别的会保留便签的工具,才会造出这种 WebM。我专门测它,是怕哪条转码配置里早就带着这个开关。
六、转码和压缩工具读不读旋转标记
这一章说素材进了处理流程之后的事。转格式、缩小尺寸、截封面、压体积,这几步都有工具在拆盒子。它们读不读便签、读完之后便签还在不在,决定了下游拿到的是什么。
6.1 ffmpeg默认先按标记把画面转正
先看 ffmpeg 什么参数都不加的表现。我拿 90 度的风机样片直接转了一遍 H.264。出来的文件是不带便签的 1080×1920,画面是真的竖着存的。转成 VP9 的 WebM 和截一帧存成 JPG 也都是 1080×1920 的正图。
也就是说 ffmpeg 默认先按标记把画面转正,再把便签拿掉。从这一步出来的文件已经换成了第一种存法,后面谁认不认便签都无所谓了,因为没有便签可认。这一轮测下来,最让我省心的就是这一条。
加上 -noautorotate 就反过来:画面原样躺着重新压一遍,便签留着,输出是 1920×1080 加 -90。截帧加这个开关,截出来的 JPG 是 1920×1080 的躺图。这张图上没有便签可读,到哪儿都是躺着的。还有 -c copy 这种不重新压的照抄模式,画面和便签都原样带走,输出和输入是同一种存法。照抄进 MKV 这种盒子也一样,ffmpeg 读回来还是 1920×1080 加 -90。MKV 我没放进浏览器测。
6.2 宽高都写死会把竖画面压成横的
默认会转正是好事,可它和一个很常见的写法撞在一起就出事了。很多现成的转码命令里有一句 -vf scale=1280:720,意思是把画面缩放成宽 1280、高 720。写这句的人脑子里想的是横屏视频。
竖拍视频走到这一句会怎样?ffmpeg 先按便签把画面转成了 1080×1920 的竖图,然后老老实实照指令把它缩放成 1280×720。一张瘦高的图被硬拉成了一张矮胖的图,输出是不带便签的 1280×720。画面里的东西头是朝上的,可整个被压扁了,竖画面成了横的。ffmpeg 没做错什么,写命令的人也没做错什么,两头一凑就错了。
src=wind_note90.mp4
ffmpeg -v error -y -i $src -an plain.mp4
ffmpeg -v error -y -i $src -vf scale=1280:720 -an fixed.mp4
ffmpeg -v error -y -i $src -vf scale=-2:720 -an auto.mp4
ffmpeg -v error -y -i $src -c copy -metadata:s:v rotate=0 meta.mp4
ffmpeg -v error -y -display_rotation 0 -i $src -c copy peel.mp4
for out in plain fixed auto meta peel; do echo "$out: $(peek $out.mp4 | tr '\n' ' ')"; done
plain: 1080x1920
fixed: 1280x720
auto: 406x720
meta: 1920x1080 rotation of -90.00 degrees
peel: 1920x1080
代码说明。五条命令的输入都是带 90 度便签的那段样片,peek 是第二章定义的那个只留宽高和便签的小函数。plain 是什么都不加的默认转码,出来 1080x1920。fixed 把宽高都写死,出来 1280x720,就是压扁的那个。auto 写的是 scale=-2:720,意思是高度定成 720,宽度让 ffmpeg 按原比例自己算,并且取成偶数。出来的 406x720 比例没坏。meta 和 peel 是下面两节要说的两种「清便签」写法。
对照 fixed 和 auto 两行就能看出差别:只写死一条边,另一条边交给工具按比例算,竖画面就还是竖的。我后来翻了翻自己存的几条常用命令,里面真有一条两个数都写死的。那条命令我一直只拿来处理横屏录屏,没撞上过竖拍素材,纯属运气。
6.3 改元数据里的rotate清不掉标记
有时候人会想干脆把便签清掉。网上能搜到一种写法是 -c copy -metadata:s:v rotate=0,意思是画面照抄,同时把视频轨道上一条叫 rotate 的说明改成 0。
我试了:输出文件还是 1920×1080,ffmpeg 读回来仍是 rotation of -90.00 degrees。便签纹丝不动。这条命令不报错,看起来像是成功了,其实什么都没改。在 ffmpeg 7.1 上,改元数据里的 rotate 清不掉标记。更早的版本是不是这样我没有测,只能说现在这一版不行。
这种「看起来成功」的操作最磨人。跑完要是不再读一遍输出,就会以为便签已经清掉了,下游接着按没有便签来处理。我现在凡是动过方向的文件,都会用第二章那个小函数再看一眼。
6.4 只清标记不重编码画面全躺倒
真能把便签清掉的写法,是把 -display_rotation 0 放在输入前面,再配 -c copy。这回 ffmpeg 读回来确实没有 rotation 那一行了。
可是画面呢?-c copy 是照抄,画面数据没动,还是躺着存的 1920×1080。便签没了,画面也没转。这个文件放到四个浏览器的播放器里全部躺倒,报的宽高是 1920×1080。前面说播放 MP4 四个浏览器都正,前提是便签还在。只清标记不重编码,画面全躺倒,等于把唯一告诉播放器「请转过来」的那句话抹了。
这一条我记得最牢。便签和躺着的画面是一对,要么两样都留着,要么把画面真的转正再把便签拿掉。只动其中一样,剩下的那一样就成了错的。
6.5 ffmpeg十二条命令的对照表
这一节把 ffmpeg 7.1 上试过的十二条命令列全。输入都是那段 1920×1080 躺着存、便签为 -90 的风机样片。
| 命令要点 | 输出尺寸 | 输出便签 | 存储像素 | 播放结果 |
|---|---|---|---|---|
| 默认转码 | 1080×1920 | 无 | 正 | 正 |
-noautorotate 转码 | 1920×1080 | -90 | 躺倒 | 认便签的播放器里正 |
-c copy | 1920×1080 | -90 | 躺倒 | 认便签的播放器里正 |
-vf scale=1280:720 | 1280×720 | 无 | 正但压扁 | 竖画面被压成横的 |
-vf scale=-2:720 | 406×720 | 无 | 正 | 正 |
-noautorotate 加 scale=1280:720 | 1280×720 | -90 | 躺倒 | 认便签的播放器里正 |
-c copy -metadata:s:v rotate=0 | 1920×1080 | 仍是 -90 | 躺倒 | 便签没被清掉 |
-display_rotation 0 加 -c copy | 1920×1080 | 无 | 躺倒 | 四个浏览器全部躺倒 |
| 默认转 WebM | 1080×1920 | 无 | 正 | 正 |
-c copy 进 MKV | 1920×1080 | -90 | 躺倒 | 没在浏览器里测 |
截帧 -ss 1 -frames:v 1 | 1080×1920 | — | 正 | — |
截帧加 -noautorotate | 1920×1080 | — | 躺倒 | — |
第六行有点意思。-noautorotate 配上写死的 1280×720,画面躺着被缩成 1280×720,便签还在。躺着的画面本来就是横的,缩成横的比例正好不坏。在认便签的播放器里它转正之后是好的,两个各自有风险的写法凑一块反而对了。我自己不靠这种巧合,这里只是照实列出来。
6.6 OpenCV默认按标记转正也可以关
除了 ffmpeg,另一个常被拿来读视频帧的是 OpenCV。它是一个图像处理库,很多用 Python 写的截封面、抽帧脚本靠它。我测的是 5.0.0 这一版。
OpenCV 里和便签有关的开关有两个。CAP_PROP_ORIENTATION_META 是只读的,告诉你便签上写的角度。CAP_PROP_ORIENTATION_AUTO 是能改的,1 表示读帧时按便签转正,0 表示不转。
import cv2
for clip in ["wind_note90.mp4", "wind_note180.mp4"]:
for follow_note in (1, 0):
reel = cv2.VideoCapture(clip)
reel.set(cv2.CAP_PROP_ORIENTATION_AUTO, follow_note)
note = int(reel.get(cv2.CAP_PROP_ORIENTATION_META))
ok, frame = reel.read()
tall, wide = frame.shape[:2]
print(clip, "跟着便签转" if follow_note else "不转", "便签", note, "帧", f"{wide}x{tall}")
reel.release()
wind_note90.mp4 跟着便签转 便签 90 帧 1080x1920
wind_note90.mp4 不转 便签 90 帧 1920x1080
wind_note180.mp4 跟着便签转 便签 180 帧 1080x1920
wind_note180.mp4 不转 便签 180 帧 1080x1920
代码说明。VideoCapture 是打开一个视频文件,read 是读出一帧。每段样片读两次,一次把自动转正的开关设成 1,一次设成 0。frame.shape 前两个数是这一帧的高和宽。输出里 90 度样片在开关为 1 时是 1080x1920,为 0 时是 1920x1080。180 度样片两次都是 1080x1920,光看这行数分不出来。我另外读了色块,开关为 1 时是正的,为 0 时是倒立的。
OpenCV 5.0.0 不设这个开关时,它本来就是 1。也就是说 OpenCV 默认按标记转正,也可以关,和 ffmpeg 的脾气一致。它读到的角度是 90、270 和 180,没有负号,跟 ffmpeg 打印的 -90 又是一套数法。完整结果是下面这张表。
| 样片 | 自动转正开关 | 读到的角度 | 帧尺寸 | 方向 |
|---|---|---|---|---|
| 90 度 | 1(默认) | 90 | 1080×1920 | 正 |
| 90 度 | 设为 0 | 90 | 1920×1080 | 躺倒,顶朝左 |
| 270 度 | 1 / 0 | 270 | 1080×1920 / 1920×1080 | 正 / 躺倒,顶朝右 |
| 180 度 | 1 / 0 | 180 | 1080×1920 | 正 / 倒立 |
更早的版本默认值是什么我没有测。手上的脚本要是跑在别的版本上,我会把这个开关明着设一遍。
6.7 浏览器里压完的成品不带旋转标记
最后看在线压缩这一类工具。它们在浏览器里完成拆包、解码、缩放和重新压缩。按第四章的结论,自己拆包解码拿到的帧全是躺着的,这类工具要是没有自己读便签,压出来的东西方向就会错。我想看看实际压完便签去哪了。
这一步用的就是第一章末尾提到的那个在线压缩工具,在 Chromium 149 里选「体积优先」档跑了七次任务,其余设置保持默认。导入 90 度样片之后界面上「画面与体积」那一栏写的是 1080 × 1920,是显示尺寸。预算那一行写的是 608×1080。压完要再点一下「↓ 保存成品」才会把文件写到硬盘上。我把七个成品都拿 ffmpeg 读了一遍,又读了色块。
| 导入的样片 | 输出格式 | 成品 | 便签 | 方向 | 体积(字节) |
|---|---|---|---|---|---|
| 不带便签 MP4 | MP4 H.264 | 608×1080 竖 | 无 | 正 | 1,511,731 → 616,526 |
| 90 度 MP4 | MP4 H.264 | 608×1080 竖 | 无 | 正 | 1,549,770 → 634,583 |
| 270 度 MP4 | MP4 H.264 | 608×1080 竖 | 无 | 正 | 1,576,872 → 640,552 |
| 180 度 MP4 | MP4 H.264 | 608×1080 竖 | 无 | 正 | 1,478,919 → 603,575 |
| 90 度 MOV | MP4 H.264 | 608×1080 竖 | 无 | 正 | 1,549,717 → 634,583 |
| 下雪 90 度 MP4 | MP4 H.264 + AAC | 608×1080 竖 | 无 | 正 | 2,877,616 → 1,069,505 |
| 90 度 MP4 | WebM VP9 | 608×1080 竖 | 无 | 正 | 1,549,770 → 501,678 |
这张图是压完之后的界面。看中间的预览:红块在左上、绿块在右上,画面是竖的。左下角预算那一行写着 608×1080,右边任务进度写着 1.48 MB → 620 KB。这只是这一段 5 秒样片的数,换一段视频不会是这个比例。
七个成品全部是竖着存的 608×1080,每秒 24 帧,不带便签,方向都是正的。三种角度、MP4 和 MOV 两种输入、MP4 和 WebM 两种输出都一样。浏览器里压完的成品不带旋转标记,换成了第一种存法。它里面怎么做的我看不到,我只看成品:便签没了,画面是竖的。输出 WebM 那一次我多看了两眼。第五章刚看过带便签的 WebM 在两个浏览器里会躺倒,这里出来的 WebM 没有便签,也就躲开了那个坑。
七次任务期间我一直开着网络记录,页面没有发出任何非 GET 的请求。GET 是网页向服务器要东西的那种请求。表里的体积只是这几段 5 到 6 秒样片在这一个档位下的结果,不能当成压缩比来看。耗时我没有可靠的读数,就不写了。
七、哪些环节可以放心哪些要留神
先把前面六章合成一张总表,回答开头那个问题:一路上谁认便签、谁不认。表后面是我自己现在的做法。
| 环节 | 认不认便签 | 留神什么 |
|---|---|---|
| 四个浏览器播放 MP4 / MOV | 都认 | 不用操心 |
| 播放器报的宽高 | 报显示尺寸 | 别和 ffmpeg 打印的存储尺寸混用 |
| 画到画布取帧 | 都认 | 画布宽高要用显示尺寸 |
| 生成位图 | 两个认两个不对 | Chromium 149 压扁,Firefox 151 躺倒 |
| 取帧对象 | Firefox 151 不认 | 只有 Chromium 149 和 Chrome 154 带 rotation |
| 自己拆包再解码 | 都不认 | 要自己读矩阵再转 |
| 带便签的 WebM 播放 | 两个认两个不认 | Firefox 151 和 WebKit 26.5 躺倒 |
| ffmpeg 默认转码和截帧 | 认,输出不再带便签 | 缩放别把宽高都写死 |
ffmpeg 加 -noautorotate 或 -c copy | 不动,便签原样留着 | 下游还得有人认 |
| ffmpeg 只清便签不重编码 | 便签没了画面还躺着 | 四个浏览器全部躺倒 |
| OpenCV 5.0.0 读帧 | 默认认 | 开关设成 0 就不认 |
| 在线压缩的那七次任务 | 认,成品不带便签 | 只测了一个浏览器一个档位 |
7.1 播放和画到画布可以放心
可以放心的环节有两个,一个是四个浏览器直接播放 MP4 和 MOV,另一个是把播放器画面画到画布上取帧。这两处在我测的所有组合里都是正的,没有一次例外。
ffmpeg 和 OpenCV 的默认行为也算半个放心。它们默认都读便签,只要没人去动那两个开关,也没把缩放的宽高都写死,出来的画面就是正的。说半个,是因为这份放心建立在「没人动过配置」上。一条从别处抄来的命令里有没有 -noautorotate,得自己翻一遍才知道。
7.2 换取帧接口和转WebM要留神
要留神的是那些「换了一种做法」的时刻。封面本来用画布取,后来为了快换成了生成位图或者取帧对象。换完在自己常用的浏览器里看着没事,到了 Firefox 151 就躺倒,到了 Chromium 149 就压扁。同一段代码在不同浏览器里给出不同的方向,只在一个浏览器里测是测不出来的。
转 WebM 是另一个时刻。默认转出来没问题,带着便签转出来的 WebM 在 Firefox 151 和 WebKit 26.5 里播放就是躺的,连播放都保不住。还有就是动便签的时候只动一半:改元数据以为清掉了,其实没清;真清掉了,画面又没跟着转。
这几处出错的时候都没有任何报错,文件能打开,画面能显示,只是方向或比例不对。要靠人看一眼,或者像我这样在画面角上放两个色块让程序去读。
7.3 我收到竖拍素材先转成像素竖的
测完这一轮我给自己定了个做法。竖拍素材进到我手里,第一步先用 ffmpeg 默认转码过一遍,把它变成像素本身就是竖的、不带便签的文件。之后的截封面、缩放、转 WebM 都从这份文件出发。依据是前面反复出现的那条:不带便签的样片,在所有取法、所有浏览器里都是正的。
这么做有代价。默认转码要把画面重新压一遍,比 -c copy 照抄慢,画质也会多损失一道。我考虑过另一条路:一路留着便签,只在每个环节确认它被读到。那条路不用重新压,可我得盯住每一个取帧接口、每一条转码命令和每一个浏览器。我搭的小工具用的人不多,素材量也不大,多压一遍我付得起。盯不住的环节出一次错,同事就得来找我一次。两边一比,我选了先转正。
要是素材量很大,或者对画质很在意,这笔账可能算出来是另一个结果。那就得照着上面那张总表,把自己流程里经过的每个环节挨个对一遍。
八、适用边界与风险
最大的边界是样片。便签是 ffmpeg 贴的,矩阵是它写的,手机自己贴的便签写法是不是完全一样,我没有拿原片验证过。前置摄像头拍的视频可能还带镜像翻转,那种矩阵我没测。编码只有 H.264。HEVC 的文件我没有数据,结果不能直接搬过去。
浏览器这边,WebKit 26.5 是 macOS 上的构建。iPhone 上的 Safari、安卓上的 Chrome 和各种 App 里内置的浏览器我都没测。它们用的内核版本可能不一样,表里的格子不能直接套。Chromium 149 和 Chrome 154 在生成位图上表现就不同,版本一变,结果可能跟着变,过一阵再拿这张表来对可能就不准了。
风险主要在于拿这张表当成普遍结论。视频平台和云转码服务我都没测,它们内部怎么处理便签,我不知道。在线压缩那一节只是一个工具在一个浏览器、一个档位下对七个短样片的表现。别的档位、别的浏览器和更长的视频我没有数据。
九、测量方法的坑
头一个坑是色块判方向会漏掉压扁。我的判断程序在画面四个角各取一小块颜色,看红和绿落在哪儿。Chromium 149 压扁的那张位图上红绿还在原位,程序报的是「正」。我是看到宽高是 1920×1080 觉得不对,才去翻截图量色块的。只判哪头朝上不够,还得同时比宽高比。
第二个坑刚好相反,缩小之后色块判不出来。scale=1280:720 和 scale=-2:720 那两条命令的输出,程序给的是问号。取样点离角的距离是固定的,画面缩小之后色块也跟着变小,取样点落到了色块外面。这两格的「正但压扁」和「正」是我对着截出来的帧看的。要是重做,我会让取样位置按画面尺寸的比例来算。
第三个坑是那两张压扁对照的截图不能当配图。Chromium 149 和 Chrome 154 各截了一张位图,色块的几何关系是清楚的,可画面主体是黑的。原因是截图那一步的视频请求没走分段加载。这两张图只用来量色块的 284×90 和 160×160,文里没有放。
第四个坑是角度有三套数法。我的样片名用的是 90、270、180,ffmpeg 打印的是 -90、90、-180,OpenCV 读到的是 90、270、180。记录的时候要是不写清是谁的读数,过两天自己都对不上。还有体积的单位。界面上写的是 1.48 MB → 620 KB,表里的字节数是 1,549,770 → 634,583。我按 1024 进位算了一下能对上,是同一个文件的两种写法。
十、还没解决的
最想补的是手机原片。我得找几台不同牌子的手机各拍一段,再把这些表重跑一遍,看手机贴的便签和 ffmpeg 贴的有没有差别。HEVC 也要一起补。这两样没测之前,这篇只能算「便签这个机制谁认谁不认」,还不能算「你手机里那段视频会怎样」。
第二件没弄清的事,是各家浏览器的结果为什么对不上。Chromium 149 生成位图会压扁,Chrome 154 不会。Firefox 151 的位图和帧对象是躺的,播放和画布又是正的。带便签的 WebM 在两个浏览器里躺倒。这几处我都只看到了现象。往后这几个浏览器出了新版本,我想把同一套样片再跑一遍,看表里的格子有没有变。
第三个是 MKV。带便签的 MKV 我只用 ffmpeg 读了回来,没放进浏览器,播放和取帧是什么样都没测。
手上要是正好有一段竖拍视频,可以先用第二章那个小函数读一下,看输出里有没有 rotation 那一行。有的话,再对照第七章的总表,把它接下来要经过的环节挨个核对一遍。拿不准的环节就先用 ffmpeg 默认转码过一道,再检查输出是不是 1080×1920 这样的竖尺寸。
参考资料
- ISO/IEC 14496-12(ISO 基础媒体文件格式):tkhd 盒子与 matrix 字段的定义
- FFmpeg 文档:
-noautorotate、-display_rotation、scale滤镜、transpose滤镜 - MDN:HTMLVideoElement.videoWidth、CanvasRenderingContext2D.drawImage、createImageBitmap、VideoFrame、VideoDecoder
- OpenCV 文档:
CAP_PROP_ORIENTATION_AUTO、CAP_PROP_ORIENTATION_META - mp4box.js 0.5.2 项目说明
- 风机画面:Wikimedia Commons「Vertical-axis wind turbine at Hartnell College Alisal Campus.gk.webm」,作者 Grendelkhan(CC BY-SA 4.0)
- 下雪画面:Wikimedia Commons「Snowfall in Freiburg Switzerland.ogv」,作者 Canbel(CC BY-SA 4.0)
- 本文实测数据:2026-10-12 本机运行记录,环境见「版本声明」