DEV Community

hao jia
hao jia

Posted on

Three ffmpeg one-liners that broke a portrait video, and the check that caught each one

A portrait clip plays upright in a <video> element. Then it goes through a transcode job, and what comes out is either a wide, flattened picture or a clip lying on its side. I rebuilt that failure this week with three ffmpeg one-liners. Each of them looks like a reasonable thing to paste into a job definition, each was wrong in a different way, and none of them printed an error.

First, the test file. Portrait video is often stored with the pixels lying sideways, plus a rotation tag: a note in the container that tells the player to turn the picture at playback. My test file was built that way on purpose, and it is not a phone original. I took a public wind turbine clip, drew a red block in the top-left corner and a green one in the top-right so the orientation is readable at a glance, then used ffmpeg to store the pixels sideways at 1920×1080 with a rotation tag. The result is five seconds of H.264 with no audio. ffmpeg 7.1 prints the tag as rotation of -90.00 degrees, and the sign surprised me, because this is the file that needs a clockwise quarter turn to look right. Chromium 149, Chrome 154, Firefox 151 and WebKit 26.5 (macOS) all played it upright and reported videoWidth × videoHeight as 1080×1920. Playback was not where it broke.

ffmpeg -i in.mp4 -vf scale=1280:720 -c:v libx264 -crf 26 -an a.mp4
ffmpeg -i in.mp4 -c copy -metadata:s:v rotate=0 b.mp4
ffmpeg -display_rotation 0 -i in.mp4 -c copy c.mp4
Enter fullscreen mode Exit fullscreen mode

The first line is the usual "make it 720p". The output was 1280×720 with no tag, and the picture was upright but squashed, a tall frame pressed into a wide one. What I had missed is that ffmpeg applies the rotation before my filter runs. The same input re-encoded with no filter at all comes out as 1080×1920 with no tag, so the scale filter was handed an upright portrait frame and told to become 1280×720. Changing the filter to scale=-2:720 gave 406×720 and a normal picture.

Left: the 1280×720 output of the fixed-size scale, squashed. Right: the 406×720 output of scale=-2:720. Both playing in Chrome 154.

Look at the red and green blocks on the left. They were squares in the source, and here they are flat bars, which is the quickest way to tell a squashed frame from a cropped one.

The second line was my attempt to clear the tag without re-encoding. It changed nothing: b.mp4 was still 1920×1080 and still reported -90.00. This one is harmless and misleading at once, because the job goes green and I walk away believing the tag is gone. I only ran ffmpeg 7.1, so I can't say what other versions do with that metadata key.

The third line does remove the tag, and it was the worst of the three. c.mp4 is 1920×1080 with no rotation line. Stream copy never touches pixels, though, so they are still lying sideways, and now nothing tells the player to turn them. All four browsers played it on its side and reported 1920×1080. A metadata check would pass this file, and every browser I tried showed it sideways.

Left: the tagged source playing upright at 1080×1920. Right: the same file after -display_rotation 0 with stream copy, playing sideways at 1920×1080. Both in Chrome 154.

The red block has moved from the top-left to the bottom-left. The top of the picture now points left, which is exactly how the pixels were stored all along.

All three got past me the same way: I judged them by exit code. Now I read the output file back after every command, with two lines.

ffmpeg -hide_banner -i out.mp4
ffmpeg -noautorotate -i out.mp4 -frames:v 1 stored.png
Enter fullscreen mode Exit fullscreen mode

The first prints the stream size and, when a tag exists, the rotation line. For a tagged file that size is the storage size, 1920×1080 here, not what a viewer sees. The second writes one frame without applying the tag, so I get the stored pixels as they are. I keep both because dimensions alone miss cases. My 180-degree sample is stored at 1080×1920 upside down, and its size looks perfectly normal. This machine has no ffprobe, so I haven't compared its output.

To check a route that doesn't involve my own flags, I used ImgIng (https://imging.ai/) in Chromium 149 with the preset that favors smaller files, and the three tagged samples, at 90, 270 and 180 degrees, each came back as 608×1080, stored upright, with no rotation tag. That is one browser, one preset and five-second clips, so I read it as a result for these files only.

There are gaps. The tags were written by ffmpeg rather than by a phone, and I did not test phone originals, HEVC, iPhone Safari or Android. Within those limits, the command that came out right for a tagged portrait source was the plain re-encode with no rotation flags. When I need a smaller size, I fix one side and let ffmpeg compute the other. I look at both read-back lines before I call a job done. The turbine footage is by Grendelkhan on Wikimedia Commons, under CC BY-SA 4.0.

Top comments (0)