ffmpegvideo-encodingpodcasting

Convert MP3 to MP4 with FFmpeg: Cover Image, YouTube-Ready

·Javid Jamae·10 min read
Convert MP3 to MP4 with FFmpeg: Cover Image, YouTube-Ready

You have a folder of podcast episodes and a cover image, and YouTube only takes video. The command everyone copies off Stack Overflow starts encoding and then never stops, or it dies on the cover art, or it hands you a 400 MB file for what is one static picture and some audio.

Quick answer: To convert mp3 to mp4 with FFmpeg, loop a still image as the video track and cut the render when the audio ends: ffmpeg -loop 1 -framerate 1 -i cover.jpg -i episode.mp3 -map 0:v:0 -map 1:a:0 -c:v libx264 -tune stillimage -pix_fmt yuv420p -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -c:a aac -b:a 192k -shortest -movflags +faststart episode.mp4. Without -shortest FFmpeg writes frames until the disk fills, and without -pix_fmt yuv420p the MP4 won't play in QuickTime or Safari. If you'd rather not tie up a laptop every time an episode drops, FFmpeg Micro runs the same render as one API call with no servers to run.

Why the looping-image command never finishes

The -loop 1 input flag turns a single JPEG into an infinite video stream. FFmpeg has no reason to stop, because one of its two inputs never ends, so it happily encodes past the end of your audio and keeps going until you hit Ctrl+C or the volume fills up. People read the frozen progress line as a hang. It isn't hanging, it's working exactly as told.

There are two ways to give it an ending. -shortest tells FFmpeg to finish when the shortest input stream runs out, which is the audio. -t 3120 sets an explicit duration in seconds, which you'd pull from ffprobe -v error -show_entries format=duration -of csv=p=0 episode.mp3 first. Use -shortest unless you deliberately want a clip that's shorter than the episode.

One ordering detail trips people up: -loop 1 is an input option, so it has to sit before -i cover.jpg. Put it after and it applies to the wrong input or gets ignored outright.

The flags that turn a working command into a usable file

Four flags do the real work in the command above, and each one exists because of a specific failure you'd otherwise hit after the render finishes.

FlagWhat breaks without it
`-shortest`The render never ends. Infinite image stream, no stop condition.
`-pix_fmt yuv420p`A PNG with alpha encodes as yuv444p or RGB. It plays in VLC and nowhere else.
`-tune stillimage`x264 spends its bit budget on motion estimation for a picture that never moves.
`-framerate 1`You encode 93,600 identical frames instead of 3,120.

-movflags +faststart is the fifth one worth adding by default. It moves the moov atom to the front of the file so a browser can start playback before the whole thing downloads. It costs one extra pass over the output and saves you a support ticket.

The divisible-by-2 error comes from the cover art, not the audio

height not divisible by 2 means your cover image has an odd pixel dimension and you asked for yuv420p. That pixel format subsamples chroma 2x2, so both dimensions have to be even, and x264 refuses rather than guessing. Podcast cover art is usually a clean 3000x3000, but screenshots and cropped exports are where the odd numbers come from.

The fix that works on any input is -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2", which adds at most one row and one column of black to round up. If you're already scaling to a target size, scale=1920:-2 gets you there too, because -2 means "preserve aspect ratio and round to the nearest even number."

One frame per second is the whole file-size fix

A 52-minute episode is 3,120 seconds. At FFmpeg's default 25 fps for image inputs that's 78,000 frames of an image that never changes, and x264's default keyframe interval of 250 frames means roughly 312 full intra-coded frames of your 1920x1080 cover. At -framerate 1 you get 3,120 frames and about 12 keyframes. The video track drops from tens of megabytes to a couple, and the encode goes from minutes to seconds.

Set it on the input (-framerate 1 before -i cover.jpg), not the output. Input framerate means FFmpeg only ever generates 3,120 frames. An output -r 1 makes it generate frames at the default rate and then throw most of them away.

The honest trade-off: seeking granularity is one second, and a handful of tools dislike sub-24fps video on import. YouTube, Vimeo, and the podcast players that accept video are fine with it. If a target platform rejects it, bump to -framerate 24 and accept a larger file.

What an audio-only upload needs to survive YouTube

YouTube re-encodes everything you send it, so the goal is giving its transcoder clean input rather than matching its output. These are the settings that avoid a rejected upload or a mangled result.

  • Container MP4, video H.264 (libx264), audio AAC-LC
  • 1920x1080 or 1280x720, even dimensions, yuv420p
  • Audio 128 to 320 kbps stereo, 48 kHz preferred (-ar 48000)
  • -movflags +faststart so the file streams while it uploads

On the audio side you have a real choice. -c:a copy muxes the original MP3 bitstream straight into the MP4 with zero quality loss and zero encode time. MP3-in-MP4 is legal, but QuickTime and some mobile players handle it unevenly, and YouTube will transcode it anyway. Re-encoding to AAC at 192k costs you one generation of lossy-to-lossy loss that nobody hears through a podcast mic. Take the AAC unless you're archiving.

Note that -c:a copy and a -vf filter can coexist here because the filter only touches video. Mixing stream copy and filtering on the same stream is what fails, which is covered in filtering and streamcopy cannot be used together.

Batching a back catalog

The single-file command becomes a shell loop once you have more than three episodes. This version scales any cover art onto a centered 1080p canvas, falls back to a default image when an episode has none, and names the output after the source.

#!/usr/bin/env bash
for mp3 in episodes/*.mp3; do
  base="${mp3%.mp3}"
  cover="${base}.jpg"
  [ -f "$cover" ] || cover="default-cover.jpg"

  ffmpeg -y -loop 1 -framerate 1 -i "$cover" -i "$mp3" \
    -map 0:v:0 -map 1:a:0 \
    -c:v libx264 -tune stillimage -crf 23 -preset veryfast \
    -pix_fmt yuv420p \
    -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
    -c:a aac -b:a 192k -ar 48000 \
    -shortest -movflags +faststart "${base}.mp4"
done

force_original_aspect_ratio=decrease keeps a square 3000x3000 cover from stretching, and the pad centers it with black bars. Because the pad target is 1920x1080, the odd-dimension problem is already solved for you.

That loop is fine for a one-time catalog conversion. It stops being fine when the render needs to happen on every new episode, on a schedule, from a server that also has to do other things. That's the point where the job belongs somewhere else: FFmpeg Micro takes the same still-image-plus-audio composition as one API call, and you can try the exact command shape in the playground before wiring it into n8n, Make, or Zapier. For long episodes, hand it a webhook instead of polling, using the patterns in webhooks for long-running video jobs.

Pitfalls that only show up on real files

Every one of these has a distinct symptom, and none of them are obvious from the error text.

  1. Your MP3 has embedded album art. FFmpeg sees a second video stream and its default stream selection picks the highest-resolution one, which may be the 3000x3000 embedded art instead of your cover. Worse, that stream is a single attached picture, so -shortest cuts the output at a fraction of a second. Always pass -map 0:v:0 -map 1:a:0 explicitly.
  2. A transparent PNG cover. Alpha pushes the encode to yuv444p or RGB unless you force -pix_fmt yuv420p, and the resulting MP4 plays in VLC but shows a black frame in Safari and Chrome.
  3. An output that's zero bytes. If the image path is wrong, FFmpeg can produce a file with no encoded frames rather than a hard failure. The diagnosis is in output file is empty, nothing was encoded.
  4. VBR MP3 duration drift. Variable-bitrate MP3s without a Xing header report a wrong duration, so ffprobe and -t disagree with reality. -shortest reads the actual stream end, which is one more reason to prefer it over a hardcoded duration.
  5. Re-running the loop overwrites finished work. -y in the batch script means yes-to-overwrite. Drop it, or guard with [ -f "${base}.mp4" ] && continue, if the loop might run twice.

When a still image is the wrong output

A looping cover image is the right answer when the audio is the product and the video is a container: podcast episodes, DJ sets, audiobook chapters, conference talk audio. It is the wrong answer when the visual is supposed to carry attention.

If you want a moving waveform instead of a static card, FFmpeg generates one directly with -filter_complex "[0:a]showwaves=s=1920x1080:mode=line:rate=25[v]", and that render is genuinely 25 fps of changing pixels, so expect a normal video-sized file and a normal video-length encode. If you want animated backgrounds, per-segment visuals, or synced captions over the audio, you're building a composition and a template-driven render service is a better shape than a single FFmpeg invocation.

FAQ

Can I convert MP3 to MP4 without a cover image?

Yes. Generate a solid color video track with FFmpeg's lavfi source instead of loading a file: ffmpeg -f lavfi -i color=c=black:s=1280x720:r=1 -i episode.mp3 -c:v libx264 -tune stillimage -pix_fmt yuv420p -c:a aac -b:a 192k -shortest out.mp4. YouTube requires a video stream, not an interesting one.

Does converting MP3 to MP4 reduce audio quality?

Converting MP3 to MP4 reduces audio quality only if you re-encode the audio. Using -c:a copy puts the original MP3 bitstream into the MP4 container untouched. Using -c:a aac -b:a 192k adds one generation of lossy transcoding, which is inaudible for speech but is a real loss for music masters.

How big should the finished MP4 be?

A one-hour episode with a static 1080p cover at -framerate 1 lands close to the size of the source audio, because the video track compresses down to a few megabytes. A 60 MB MP3 becomes roughly a 65 MB MP4. If yours came out at 400 MB, the frame rate is the cause, not the audio.

What resolution should the cover image be?

Use 1920x1080 for a 16:9 upload, or supply your square 3000x3000 podcast artwork and let scale plus pad letterbox it onto a 1080p canvas. Both dimensions of the final frame must be even, which the pad=1920:1080 target guarantees.

Why does FFmpeg keep encoding after the audio ends?

FFmpeg keeps encoding because -loop 1 creates an image stream with no end, and FFmpeg stops only when every input is exhausted. Adding -shortest makes it end with the audio track.

Once these renders stop being a one-time catalog job and start being something that has to happen every time an episode publishes, running the encoder on your own machine is the part that breaks. Sign up free and put the same command behind one API call your publishing workflow can trigger on a schedule.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free