Shrinking a 61‑Hour Stream Recording with FFMPEG on MacOS

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
- Cut the frame‑rate in half. For screen recordings, 30 fps looks fine and instantly halves the data.
- 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! 🎧🎛️
Member discussion