34

Currently I have 80mb movies that I want to use ffmpeg to convert down to say about 10mb or 15mb. I know there will be a quality loss but they will need to have sound. Is there a way to either specify a file size or higher compression than what I have done previously

ffmpeg -i movie.mp4 -b 2255k -s 1280x720 movie.hd.ogv

They are currently about 25mb a piece

Cœur
  • 37,241
  • 25
  • 195
  • 267
justin.esders
  • 1,316
  • 4
  • 12
  • 28

6 Answers6

44

if you are targeting a certain output file size the best way is to use H.264 and Two-Pass encoding.

There is a great example here but it's too large to copy-paste: https://trac.ffmpeg.org/wiki/Encode/H.264#twopass

You calculate your target bitrate using bitrate = target size / duration and you launch ffmpeg two times: one pass analyzes the media and the second does the actual encoding:

ffmpeg -y -i input -c:v libx264 -preset medium -b:v 555k -pass 1 -c:a libfdk_aac -b:a 128k -f mp4 /dev/null && \
ffmpeg -i input -c:v libx264 -preset medium -b:v 555k -pass 2 -c:a libfdk_aac -b:a 128k output.mp4

Edit: H.265 (HEVC) is even better at compression (50% of H.264 size in some cases) but support is not yet widespread so stick with H.264 for now.

aergistal
  • 29,947
  • 5
  • 70
  • 92
37

Inspired by Hashbrown's answer. This version keeps the original audio quality, and resizes to the target size.

NEW

  1. Renamed variables for readability and consistency.
  2. Removed dependency on grep (-P switch not implemented in OSX grep)
  3. The script now exits with a helpful message if the target size would be too small.

Script

#!/bin/bash
#
# Re-encode a video to a target size in MB.
# Example:
#    ./this_script.sh video.mp4 15

T_SIZE="$2" # target size in MB
T_FILE="${1%.*}-$2MB.mp4" # filename out

# Original duration in seconds
O_DUR=$(\
    ffprobe \
    -v error \
    -show_entries format=duration \
    -of csv=p=0 "$1")

# Original audio rate
O_ARATE=$(\
    ffprobe \
    -v error \
    -select_streams a:0 \
    -show_entries stream=bit_rate \
    -of csv=p=0 "$1")

# Original audio rate in KiB/s
O_ARATE=$(\
    awk \
    -v arate="$O_ARATE" \
    'BEGIN { printf "%.0f", (arate / 1024) }')

# Target size is required to be less than the size of the original audio stream
T_MINSIZE=$(\
    awk \
    -v arate="$O_ARATE" \
    -v duration="$O_DUR" \
    'BEGIN { printf "%.2f", ( (arate * duration) / 8192 ) }')

# Equals 1 if target size is ok, 0 otherwise
IS_MINSIZE=$(\
    awk \
    -v size="$T_SIZE" \
    -v minsize="$T_MINSIZE" \
    'BEGIN { print (minsize < size) }')

# Give useful information if size is too small
if [[ $IS_MINSIZE -eq 0 ]]; then
    printf "%s\n" "Target size ${T_SIZE}MB is too small!" >&2
    printf "%s %s\n" "Try values larger than" "${T_MINSIZE}MB" >&2
    exit 1
fi

# Set target audio bitrate
T_ARATE=$O_ARATE


# Calculate target video rate - MB -> KiB/s
T_VRATE=$(\
    awk \
    -v size="$T_SIZE" \
    -v duration="$O_DUR" \
    -v audio_rate="$O_ARATE" \
    'BEGIN { print  ( ( size * 8192.0 ) / ( 1.048576 * duration ) - audio_rate) }')

# Perform the conversion
ffmpeg \
    -y \
    -i "$1" \
    -c:v libx264 \
    -b:v "$T_VRATE"k \
    -pass 1 \
    -an \
    -f mp4 \
    /dev/null \
&& \
ffmpeg \
    -i "$1" \
    -c:v libx264 \
    -b:v "$T_VRATE"k \
    -pass 2 \
    -c:a aac \
    -b:a "$T_ARATE"k \
    "$T_FILE"

NOTES

  • See comments for a possible Windows version.

SOURCES

Hashbrown's answer (in this thread)

Two Pass method

stephenwade
  • 1,057
  • 2
  • 20
  • 37
Marian Minar
  • 1,344
  • 10
  • 25
  • 5
    `$1` should be quoted on lines 4 and 5 in case the file path contains spaces. On a related note, "Edits must be at least 6 characters" is a dumb rule for a programming site. – 9072997 Apr 17 '20 at 03:36
  • `$1`s now quoted thanks 9072997. Or is it 907 for short? – Marian Minar Apr 17 '20 at 17:13
  • If your input contains multiple audio streams you may need to change `-select_streams a` to `-select_streams a:0` on both of the ffprobe lines. – FLeX Jun 29 '20 at 17:59
  • 2
    it shouldnt need to be an integer, I didn't run yours, but my original takes "7.5" so I think yours might/could. This is handy since the algorithm seems to overshoot (8 generates an 8.5mB file, which discord wont like, but 7.8 is pretty dead on without dropping down to 7-flat) – Hashbrown Jul 10 '20 at 04:45
  • Where does `1.048576` come from? – Florian Cargoet Mar 08 '21 at 17:45
  • @FlorianCargoet good question. kiB - kB conversion. However, now that I look back at this answer, I'm not sure I know why I thought I needed to do this. – Marian Minar Mar 08 '21 at 22:30
  • @MarianMinar wouldn't it be 1024 / 1000 = 1.024 ? – Florian Cargoet Mar 10 '21 at 06:06
  • @FlorianCargoet sorry I meant miB - mB. – Marian Minar Mar 11 '21 at 18:19
  • Very nice. Not as straight forward to convert to usage for Windows CMD as you made it sound; here is my implementation with credits to you and this answer: https://gist.github.com/svArtist/e1b0c8526abaffc9f6ffc63d232b53d5 – Ben Philipp Sep 26 '21 at 18:10
  • @BenPhilipp haha, yes, you're very right - thanks for the CMD version. I added a note in my answer - it's tempting to get rid of that part altogether. – Marian Minar Sep 27 '21 at 15:18
  • 4
    Eliminate `grep` with `origin_duration_s=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$1")`. Note that `origin_audio_bitrate_kbit_s` won't work for all formats, notable Matroska (mkv) inputs. But you can also remove `grep` from that too: `origin_audio_bitrate_kbit_s=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of csv=p=0 "$1")` – llogan Sep 27 '21 at 16:24
  • @llogan This is great, also because grep's `-P` option doesn't seem to be supported on MacOS by default (see [stackoverflow.com/a/45534127/7669319](https://stackoverflow.com/a/45534127/7669319)) – Felix Jassler Sep 28 '21 at 13:48
  • I'm not an awk wizard; how would you modify this script to include an optional resize step in a single operation? Also note that the current script will reincode the audio to its original bitrate, which is overkill in the (common) case the audio is AAC-encoded already. – RJVB Jul 24 '22 at 18:25
10

Here's a way to do it automatically with a bash script
Just call like ./script.sh file.mp4 15 for 15mB

bitrate="$(awk "BEGIN {print int($2 * 1024 * 1024 * 8 / $(ffprobe \
    -v error \
    -show_entries format=duration \
    -of default=noprint_wrappers=1:nokey=1 \
    "$1" \
) / 1000)}")k"
ffmpeg \
    -y \
    -i "$1" \
    -c:v libx264 \
    -preset medium \
    -b:v $bitrate \
    -pass 1 \
    -an \
    -f mp4 \
    /dev/null \
&& \
ffmpeg \
    -i "$1" \
    -c:v libx264 \
    -preset medium \
    -b:v $bitrate \
    -pass 2 \
    -an \
    "${1%.*}-$2mB.mp4"

NB I'm cutting audio out

Hashbrown
  • 12,091
  • 8
  • 72
  • 95
  • 1
    How to not cut audio? – Samie Bencherif Mar 05 '20 at 01:50
  • 2
    OK. Solution is to remove lines with `-an` – Samie Bencherif Mar 05 '20 at 01:53
  • Mm, I just didn't try because it will no longer match the size you want to make. To include audio AND have a filesize-ceiling you need to decide a bitrate ratio and so reduce audio qualtiy, not just `-b:v`, and also make sure these two add up right. – Hashbrown Mar 05 '20 at 04:36
  • If you want to preserve the audio "as is" (which I usually want), then you can of course use something like ffprobe to determine the size of the audio track. Subtract that size from the target size to obtain the remaining size that can be used for video track. I find you need to subtract 2Mb from that for the MP4 container. A recent example, downsizing a 200Mb, 720p video to a 640x360 version aiming for 64Mb (got 63.9Mb): `ffmpeg -i in.mp4 -filter:v scale=640:-1 -c:a copy -b:v 630k out.mp4`; the `-c:a copy` gives me the original audio in the output video. – RJVB Apr 20 '21 at 09:02
  • AWK? Seriously? `$(( $2 * 1024 * 1024 * 8 / $( ... ) ))`. This is not even bash, this is POSIX shell and will work even in dash, ash, etc. – Mecki May 28 '23 at 03:02
  • @Mecki did you even try your code? Don't come in here acting all high and mighty when you don't even know that a decimal (eg `$2 = 7.5`) will break your precious POSIX as a syntax error – Hashbrown May 29 '23 at 06:12
  • @Hashbrown Even then you'd use `printf` to cut of the decimal (`printf '%.0f' $( ffprobe ...`) or use `bc` if you want float calculations. Using a pattern matching language to perform a simple math calculation is ridiculous. – Mecki May 30 '23 at 09:08
2

Reduce video to pre-determined file size using Windows 10, cmd and ffmpeg.

Use H.264 and Two-Pass encoding.

Calculate your bitrate using bitrate = target file size / duration

Target file size in kilobits. Duration in seconds. 1 MB = 8192kb

Split the bitrate between video and audio, about 3/4 video, 1/4 audio. The sum of the audio and video bitrate must not exceed the calculated bitrate.

-c:a libfdk_aac does not work for me.

ffmpeg -y -i input -c:v libx264 -b:v CALCULATED BITRATE HERE -pass 1 -an -f null nul && ^ffmpeg -y -i input -c:v libx264 -b:v CALCULATED BITRATE HERE -pass 2 -c:a aac -b:a CALCULATED BITRATE HERE output.mp4

ffmpeg -y -i 1.mp4 -c:v libx264 -b:v 555k -pass 1 -an -f null nul && ^ffmpeg -y -i 1.mp4 -c:v libx264 -b:v 555k -pass 2 -c:a aac -b:a 128k output.mp4

See https://trac.ffmpeg.org/wiki/Encode/H.264#twopass

Community
  • 1
  • 1
somebadhat
  • 744
  • 1
  • 5
  • 17
0

A great way to slash video size to a fraction if prioritizing audio, by reducing resolution.

-filter:v "scale=-1:ih*1/2:force_original_aspect_ratio=decrease" -max_muxing_queue_size 1024

fieldlab
  • 1
  • 3
  • Reducing frames per second doesn't really work because of the way video is compressed, only the differences from frame to frame are encoded. If you reduce the frame rate a lot, then every frame basically becomes a big key frame. You don't end up saving that much. But reducing resolution is very effective. – fieldlab Jul 15 '22 at 01:47
-4

Just decreasing the frame rate - if you do not need to have a high frame rate, like in a tutorial - you will decrease the size of the video file significantly, and with this filter you want lose the audio sync

ffmpeg -i input.mp4 -filter:v fps=fps=10 output.mp4

PythonProgrammi
  • 22,305
  • 3
  • 41
  • 34