A portrait clip can play upright in every browser as an MP4, then end up lying on its side after the tooling converts it to WebM or "cleans" its metadata. Nothing looks wrong in between. I ran into this while deciding whether to add a WebM rendition to a small upload feature, so before shipping I spent an afternoon testing what happens to orientation along the way. The short version is that "portrait" means two different files, and I now turn both into the same kind at the door.
The first kind is what you would expect: the pixels are stored tall, say 1080×1920. The second kind is how tall footage is commonly saved: the pixels are stored sideways as 1920×1080, and the file carries a small rotation tag, a label that says "turn this 90 degrees when you play it". The picture itself is still lying down. ffmpeg -i gives it away, because it prints the stream as 1920x1080 plus a line like rotation of -90.00 degrees. My test files were of that second kind, with one caveat I should state early. They are not phone originals. I took a public clip of a wind turbine, added a red square top-left and a green square top-right so orientation is obvious, and used ffmpeg 7.1 to store the pixels sideways and write the tag.
As MP4 and MOV, the tagged files were boring in a good way. Chromium 149, Chrome 154, Firefox 151 and WebKit 26.5 (the macOS build) all played them upright in a plain <video> element and reported videoWidth × videoHeight as 1080×1920. So the label is honored on playback in those four. If I only ever served the uploaded MP4 untouched, I would probably never have looked further.
WebM was different. I made a VP9 WebM that keeps the sideways pixels and the tag, using ffmpeg's -noautorotate flag, and ffmpeg could still read the -90 rotation from it. Chromium 149 and Chrome 154 played it upright at 1080×1920. Firefox 151 and WebKit 26.5 played it sideways at 1920×1080.
All three panels are the same file in a plain <video> element. Watch the red and green squares: they belong at the top, and in Firefox and WebKit they sit on the left edge. The size under each panel is what the browser reported. Footage is from a video by Grendelkhan on Wikimedia Commons, CC BY-SA 4.0.
My first instinct was the cheap fix. Stream copy costs almost no CPU, and on a small VPS that matters to me, so I tried to simply get rid of the tag without re-encoding. Attempt one, -c copy -metadata:s:v rotate=0, did nothing: the output still read -90. Attempt two did remove it:
ffmpeg -display_rotation 0 -i tagged.mp4 -c copy untagged.mp4
The output is 1920×1080 with no tag, and the pixels are still lying down, because nothing ever turned them. All four browsers then played it sideways, with videoWidth at 1920×1080. The tag was the only thing keeping the video upright, and I had just deleted it.
So the fix that holds is the one that costs CPU: re-encode once, and let ffmpeg rotate the pixels while it does. That is its default behavior. A plain transcode of the tagged sample came out as 1080×1920, pixels upright, no tag, and the default VP9 WebM conversion did the same.
ffmpeg -i upload.mp4 -c:v libx264 -preset veryfast -crf 26 -c:a copy normalized.mp4
The flags that bit me were the ones that opt out of this: -noautorotate, -c copy, and anything that edits the tag by hand. One encode per upload is a cost I can live with, because everything downstream then sees one kind of portrait.
I also wanted a second opinion from something that isn't my own script. I ran the tagged samples through ImgIng (https://imging.ai/), a browser-based compressor, in Chromium 149 on its smallest-size preset. The 90, 270 and 180 degree files all came back as 608×1080 with upright pixels and no tag, as MP4, and the 90 degree one as WebM too. After import, the page already listed the 90 degree file as 1080 × 1920. These were 5-second clips, one browser, one preset, so I read it as a check on orientation and nothing more.
What I have not covered: real phone originals, HEVC, Safari on iPhone, anything on Android. A phone may write its tag differently from ffmpeg, and I would rather find that out with one real upload than assume. For now my upload step reads the rotation line, re-encodes whenever there is one, and I keep a tagged sample around to rerun whenever I touch the ffmpeg flags.
Top comments (0)