BuildNot yet confirmed elsewhere1 publisher3 min readPublished
Portrait MP4 frames decoded with WebCodecs come out sideways in all four tested browsers
Portrait MP4 clips decoded with mp4box.js and VideoDecoder came out 1920x1080 and sideways in Chromium, Chrome, Firefox and WebKit, a dev.to post reports. The rotation lives in the track header matrix that the demuxer exposes, so the app has to read it and turn the canvas itself.
The Engineer · Build desk
Three figures. The author's snippet reading the tkhd matrix gives 90 degrees for the tagged sample. ffmpeg prints a rotation of -90.00 degrees for the same file. Frames from the author's own decoder reported rotation 0 in both Chromium-based browsers.
| Figure | Value | Note | Claim |
|---|---|---|---|
| Angle from the tkhd matrix, author's snippet | 90 degrees | 10 | |
| ffmpeg's printed rotation | −90 degrees | 11 | |
| Decoder frames' rotation | 0 rotation value | in both Chromium-based browsers | 6 |
What happened
- The same file played upright at 1080x1920 in a <video> element in all four browsers, and drawImage(video) onto a canvas was upright too.
- The test clip was built with ffmpeg 7.1: pixels stored sideways at 1920x1080 plus a rotation tag, the usual way portrait video is stored.
- Frames built from the video element carried a VideoFrame.rotation value in Chromium 149 and Chrome 154, but Firefox 151 and WebKit 26.5 had no such property.
Why it matters
- constraint An app with its own demux and decode path cannot wait on VideoFrame.rotation. It reads 0 on decoder frames in Chromium and Chrome and does not exist in Firefox or WebKit.
- cost Each such app takes on a tkhd parse, a width and height swap, and a canvas rotation. A 180-degree clip passes every size check, so a bug there ships quietly.
- decision Exporters must choose between baking upright pixels and re-writing the tag. In this test only baking played upright everywhere, because Firefox and WebKit showed the tagged WebM sideways.
The author wrote, "A decoder that is fed raw samples only ever gets the picture." [17] The angle is in the container. In MP4 and MOV it is a 3x3 matrix in the track header box, tkhd, and mp4box returns it along with the track [8]. The 90-degree sample's matrix was [0, 65536, 0, -65536, 0, 0, 0, 0, 1073741824], while the upright sample with no tag returned identity [9]. The values are fixed-point, so 65536 is 1.0, and the first two entries are enough to recover the angle [9][10]:
``` const [a, b] = trak.tkhd.matrix; // 65536 = 1.0 const turn = (Math.round(Math.atan2(b, a) * 180 / Math.PI) + 360) % 360; ```
With a = 0 and b = 65536, that gives 90 degrees [20].
Two details tripped the author. ffmpeg prints a rotation of -90.00 degrees for the same file, so the author checks the computed turn against the painted marker blocks instead of trusting a sign [11]. And mp4box reports track_width and track_height as 1920x1080, the stored size, so a canvas sized from the track comes out landscape [12]. For a quarter turn the author swaps width and height, then rotates the context before drawing the frame [12]. A 180-degree clip keeps its size, so a width check never flags an upside-down frame [13].
The createImageBitmap(video) results show how far the browsers drift apart. Chrome 154 returned 1080x1920 with square blocks, and Chromium 149 returned a 1920x1080 bitmap that was upright but squashed, with the 160x160 blocks at about 284x90 [7]. Those blocks were stretched by about 1.78 across and squeezed to 0.56 down, which match 1920/1080 and 1080/1920 [19]. We think Chromium rotated the picture and then fitted it into the stored-size box. The post does not give a cause.
On the way out, the author tried keeping the sideways pixels and writing the tag again. ffmpeg with -noautorotate to VP9 kept the 1920x1080 sideways pixels, and the output file still carried a -90 rotation that ffmpeg could detect [15]. In Chromium 149 and Chrome 154 that WebM played upright at 1080x1920, while Firefox 151 and WebKit 26.5 played it lying down at 1920x1080 [15]. Upright pixels with no tag were upright on every path in every browser tested, so the author rotates at encode time and writes no tag [16]. We think that is the right call when you do not control the player.
The scope is narrow. Every tag was written by ffmpeg, and the post does not cover phone originals, HEVC, iPhone Safari or Android [18]. WebKit was tested on macOS [2]. Mirrored matrices, which the author says a front camera might write, were not tested, and the snippet ignores a flip [14]. For the result to transfer to your app, your clips would need a plain rotation matrix in tkhd, and your frames would have to come from VideoDecoder fed by your own demuxer.
What to watch
- Whether anyone repeats the test with phone originals, HEVC, iPhone Safari or Android, where the tags were not written by ffmpeg.
- Whether Firefox and WebKit add VideoFrame.rotation, and whether Chromium fills it in on frames produced by VideoDecoder.
- Whether mirrored front-camera matrices break the atan2 snippet, since it ignores a flip.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence62
- Adoption
- Insufficient
- Hype gap0
- Incentives
- Insufficient
- Confidence58
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A portrait test clip run through the author's pipeline, mp4box.js 0.5.2 to demux and then VideoDecoder, produced frames that all came out 1920x1080 with the picture lying on its side.
ReportedSupportedSource: dev.to post by an individual developer (single-source test)View cited source - [2]
Chromium 149, Chrome 154, Firefox 151 and WebKit 26.5 (macOS) all returned lying-down frames from the decoder path, and all four played the same file upright.
- [3]
The clip is not phone footage: the author used ffmpeg 7.1 on a Wikimedia Commons wind turbine clip to store the pixels sideways at 1920x1080 and write a rotation tag.
- [4]
In a <video> element the clip played upright at 1080x1920: videoWidth x videoHeight reported 1080x1920 and drawImage(video) onto a canvas was upright in all four browsers.
- [5]
With new VideoFrame(video), Chromium 149 and Chrome 154 reported coded size 1920x1080, display size 1080x1920 and a rotation value of 90, 270 or 180 to match the sample, while Firefox 151 and WebKit 26.5 had no rotation property on the frame and the Firefox frame drew sideways.
- [6]
Frames from the author's own decoder reported rotation = 0 in both Chromium-based browsers, so the author cannot rely on the VideoFrame.rotation property.
- [7]
createImageBitmap(video) disagreed across browsers: Chromium 149 returned a 1920x1080 bitmap, upright but squashed, with the 160x160 marker blocks at about 284x90; Chrome 154 ran the same code and returned 1080x1920 with square blocks; Firefox 151 returned 1920x1080 lying down.
- [8]
In MP4 and MOV the rotation is a 3x3 matrix in the track header box, tkhd, and mp4box gives it along with the track.
- [9]
For the author's 90-degree sample the tkhd matrix read [0, 65536, 0, -65536, 0, 0, 0, 0, 1073741824], the untagged upright sample gave the identity matrix, and the numbers are fixed-point so 65536 is 1.0.
- [10]
The first two matrix entries are enough to get the angle: const [a, b] = trak.tkhd.matrix; const turn = (Math.round(Math.atan2(b, a) * 180 / Math.PI) + 360) % 360; which gives 90 for the tagged sample and 0 for identity.
- [11]
ffmpeg prints a rotation of -90.00 degrees for the same file, so the author checks the result against the marker blocks instead of trusting a sign.
- [12]
mp4box reports track_width x track_height as 1920x1080, the stored size, so a canvas sized from the track is landscape; for a quarter turn the author swaps width and height, then rotates the context before drawing the frame.
- [13]
The 180-degree sample's size does not change, so a width check will never catch an upside-down frame.
- [14]
The author has not tested the mirrored matrices a front camera might write, and the snippet would ignore a flip.
- [15]
ffmpeg with -noautorotate to VP9 kept the 1920x1080 lying pixels and ffmpeg could still read the -90 rotation from the result; Chromium 149 and Chrome 154 played that WebM upright at 1080x1920, while Firefox 151 and WebKit 26.5 played it lying down at 1920x1080.
- [16]
Upright pixels with no tag were upright on every path in every browser the author tested, so the author rotates at encode time and writes no tag.
- [17]
A decoder that is fed raw samples only ever gets the picture.
- [18]
None of this covers phone originals, HEVC, iPhone Safari or Android; the tags in the test were all written by ffmpeg.
- [19]
In the Chromium 149 bitmap the 160x160 blocks were scaled by about 1.78 horizontally and about 0.56 vertically, which match the ratios 1920/1080 and 1080/1920.
- [20]
For the 90-degree sample, a = 0 and b = 65536, so atan2(b, a) converted to degrees gives 90.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toWebCodecs gave me portrait video lying on its side in all four browsers I tested
1 article · October 13, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- Browser Compatibility and Progressive EnhancementFollow
- WebCodecsFollow
- Video rotation metadataFollow