5 min read

Shrinking a 61‑Hour Stream Recording with FFMPEG on MacOS

Using ffmpeg to resize a huge video file in-place. The technical story of how I got that monster file down to a size that actually fits—plus a copy‑and‑paste shell script you can reuse.
Shrinking a 61‑Hour Stream Recording with FFMPEG on MacOS
screenshot from one of my DJ specials

I really enjoy creating and streaming live visuals while I DJ over on twitch.tv/funka7ron. Every show is a one‑off performance, so I hit record in OBS to keep an archive. The downside? A single marathon set left me with a 61‑hour video that weighed in at 720 GB. My external drive groaned in protest.

Below is the technical story of how I got that monster file down to a size that actually fits—plus a copy‑and‑paste shell script you can reuse.


Install ffmpeg first

To follow along on your own computer, you'll need ffmpeg. If ffmpeg isn’t on your Mac yet, install it with Homebrew:

brew install ffmpeg

After install, check that the Apple hardware encoder is present:

ffmpeg -encoders | grep hevc_videotoolbox

A line beginning with V..... hevc_videotoolbox confirms it can encode HEVC using Apple Silicon speedups.


Meet the Monster File

  • File: 2024‑06‑25_22‑04‑18.mkv
  • Length: 61 hours, 7 minutes
  • Video: 1440 × 810 at 60 frames per second (fps)
  • Audio: Six separate stereo tracks (I only needed one)
  • Size on disk: 720 GB

In short: great visual quality—but far too large to shuffle around or keep unchanged.

You might ask:

  • Why not just move it to another drive? Copying 720 GB takes hours and I didn’t have that much spare space on any SSD.
  • Why not let OBS record at a lower quality? Because I want a pixel-for-pixel copy of what viewers saw live. I can always trim or down‑sample later, but I can’t regain lost detail.
  • Why no fancy editors? Any workflow that generates huge temporary files, which almost all do, was off the table—the temp files would be bigger than the free space I had.

So the plan was clear: shrink the file in place with a streaming encoder that writes only the final MP4 and nothing else.

My First Try (and Why It Failed)

I used ffmpeg—the Swiss‑army knife for video—to convert the file to MP4 and target the same quality settings I normally use in HandBrake:

ffmpeg -i big.mkv \
  -map 0:v:0 -map 0:a:0 \
  -c:v hevc_videotoolbox -b:v 16.8M \
  -c:a aac -b:a 166k \
  big.mp4

It ran for 28 hours, then quit with “No space left on device.” Oops. Here’s the math I forgot to do:

File size ≈ Bitrate × Length ÷ 8 (to convert from bits to bytes)

16.8 megabits per second × 220 058 seconds ≈ 452 GB

My drive simply didn’t have that much free space.

Two easy wins

  1. Cut the frame‑rate in half. For screen recordings, 30 fps looks fine and instantly halves the data.
  2. Lower the bitrate. I settled on 8 megabits per second (Mb s‑1). Still crisp for 30 fps video.

Quick check:

8 Mb s‑1 × 220 058 s ÷ 8 ≈ 215 GB

That would fit.

(A gentle reminder I often need: 1 megabit per second is about 0.125 megabytes per second.)

The Command That Worked

ffmpeg -i "2024-06-25_22-04-18.mkv" \
  -map 0:v:0 -map 0:a:0 \
  -vf fps=30 \                          # drop to 30 fps
  -c:v hevc_videotoolbox -b:v 8M \      # built‑in Mac hardware encoder
  -c:a aac -b:a 128k \                  # single stereo track
  "2024-06-25_22-04-18.mp4"

Output file: ≈ 215 GB—still big, but a 70 % savings, and it fits.

A Reusable Script

Save the snippet below as encode_hevc.sh, make it executable (chmod +x encode_hevc.sh), and call it whenever OBS leaves you with a giant file. This is a much nicer version of the command above, but with input validation and such.

#!/usr/bin/env bash
set -euo pipefail

# Default values
BITRATE="8M"
FPS="30"
NOTIFICATION=true
SOUND=true

# Function to display help message
show_help() {
    echo "Video Converter - Convert OBS MKV recordings to MP4"
    echo ""
    echo "Usage: $0 [options] <input-file>"
    echo ""
    echo "Options:"
    echo "  -h, --help           Show this help message"
    echo "  -b, --bitrate RATE   Set video bitrate (default: 8M)"
    echo "  -f, --fps RATE       Set frames per second (default: 30)"
    echo "  -o, --output FILE    Specify output filename (default: same as input with .mp4 extension)"
    echo "  --no-sound           Disable completion sound"
    echo "  --no-notification    Disable completion notification"
    echo ""
    echo "Example:"
    echo "  $0 --bitrate 10M --fps 60 recording.mkv"
    echo "  $0 -b 5M -o custom_name.mp4 recording.mkv"
    exit 0
}

# Parse command line arguments
INPUT=""
OUTPUT=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            show_help
            ;;
        -b|--bitrate)
            if [[ $# -lt 2 ]]; then
                echo "Error: Option $1 requires an argument" >&2
                exit 1
            fi
            BITRATE="$2"
            shift 2
            ;;
        -f|--fps)
            if [[ $# -lt 2 ]]; then
                echo "Error: Option $1 requires an argument" >&2
                exit 1
            fi
            FPS="$2"
            shift 2
            ;;
        -o|--output)
            if [[ $# -lt 2 ]]; then
                echo "Error: Option $1 requires an argument" >&2
                exit 1
            fi
            OUTPUT="$2"
            shift 2
            ;;
        --no-sound)
            SOUND=false
            shift
            ;;
        --no-notification)
            NOTIFICATION=false
            shift
            ;;
        -*)
            echo "Error: Unknown option: $1" >&2
            echo "Use --help to see available options" >&2
            exit 1
            ;;
        *)
            if [[ -z "$INPUT" ]]; then
                INPUT="$1"
            else
                echo "Error: Multiple input files specified. Only one input file is supported." >&2
                echo "Use --help to see correct usage" >&2
                exit 1
            fi
            shift
            ;;
    esac
done

# Validate input file
if [[ -z "$INPUT" ]]; then
    echo "Error: No input file specified" >&2
    echo "Use --help to see correct usage" >&2
    exit 1
fi

if [[ ! -f "$INPUT" ]]; then
    echo "Error: Input file '$INPUT' does not exist" >&2
    exit 1
fi

# Validate bitrate format (should end with K or M)
if ! [[ "$BITRATE" =~ ^[0-9]+[KMkm]$ ]]; then
    echo "Warning: Bitrate '$BITRATE' doesn't follow the recommended format (e.g., 8M, 500K)" >&2
    read -p "Continue anyway? (y/N): " CONFIRM
    if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
        echo "Operation cancelled"
        exit 0
    fi
fi

# Validate FPS (should be a positive number)
if ! [[ "$FPS" =~ ^[0-9]+(\.[0-9]+)?$ ]] || [[ "$FPS" == "0" || "$FPS" == "0.0" ]]; then
    echo "Error: FPS must be a positive number" >&2
    exit 1
fi

# Set default output filename if not specified
if [[ -z "$OUTPUT" ]]; then
    OUTPUT="${INPUT%.*}.mp4"
fi

# Check if output file already exists
if [[ -e "$OUTPUT" ]]; then
    read -p "Output file '$OUTPUT' already exists. Overwrite? (y/N): " CONFIRM
    if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
        echo "Operation cancelled"
        exit 0
    fi
fi

echo "Converting '$INPUT' to '$OUTPUT'"
echo "Settings: Bitrate=$BITRATE, FPS=$FPS"
echo "Starting ffmpeg..."

# Run ffmpeg with progress display
ffmpeg -hide_banner -stats -i "$INPUT" \
  -map 0:v:0 -map 0:a:0 \
  -vf fps=${FPS} \
  -c:v hevc_videotoolbox -b:v "$BITRATE" -pix_fmt yuv420p \
  -c:a aac -b:a 128k \
  "$OUTPUT"

echo "Conversion complete: '$OUTPUT'"
# use the full path when reporting
echo "Newly encoded file created at $(realpath "$OUTPUT"); size: $(du -h "$OUTPUT" | cut -f1)"
echo "Original file: $(realpath "$INPUT"); size: $(du -h "$INPUT" | cut -f1)"

# Play sound notification if enabled
if [[ "$SOUND" == true ]]; then
    afplay /System/Library/Sounds/Submarine.aiff
fi

# Display system notification if enabled
if [[ "$NOTIFICATION" == true ]]; then
    osascript -e 'display notification "Encoding complete." with title "ffmpeg done"'
fi

encode_hevc.sh

What do these extra commands do?

Command What it does
set -euo pipefail -e stops on errors, -u treats unset variables as errors, -o pipefail makes a pipeline fail if any step fails. In short, the script exits immediately on most mistakes.
afplay /System/Library/Sounds/Submarine.aiff Plays the built‑in macOS Submarine sound so you hear when the job finishes.
osascript -e 'display notification …' Uses AppleScript to show a desktop notification in Notification Center.

Usage:

./encode_hevc.sh big.mkv        # 8 Mb s‑1, 30 fps
./encode_hevc.sh big.mkv 6M 25  # 6 Mb s‑1, 25 fps

Takeaways for Fellow Streamers

  • Do math first (ugh): bitrate × length ÷ 8 gives you the size of the file in bytes.
  • 30 fps is a better dollar value than 60 fps for most tutorial or DJ‑cam footage.
  • MacOS's built‑in HEVC encoder is an order of magnitude faster than software encoders, but it only does 8‑bit and needs a fixed bitrate.
  • Playing a system sound and a desktop pop‑up mean you can hit run, go message an insult to your podcast cohost, and come back when the encoder is done.

Happy glitching, and see you in my next Twitch set! 🎧🎛️