Skip to content

Instantly share code, notes, and snippets.

@colbyn
Created June 24, 2025 17:19
Show Gist options
  • Save colbyn/c9fad72c03e64fef94532f7b5af29a7d to your computer and use it in GitHub Desktop.
Save colbyn/c9fad72c03e64fef94532f7b5af29a7d to your computer and use it in GitHub Desktop.
My Automated FFMPEG hardware recoding setup (desktop screen capture + camera + multiple audio sources for redundancy) all muxed into a single master copy

πŸŽ₯ Clean Multi-Input Capture on macOS

A set of dead-simple FFmpeg helper scripts for recording high-quality, synchronized video + audio streams directly into a clean, editable .mkv master file on macOS. Designed for screen walkthroughs, project documentation, and clean archival capture using built-in or external hardware.

πŸŽ™οΈ Why Record and Mux Multiple Raw Streams?

This project is built around a "clean master capture" philosophy:

πŸ“¦ 1. Everything in One File (Synchronized)

All video and audio sources are recorded simultaneously and muxed into a single .mkv container. This guarantees perfect sync and makes file management easierβ€”no need to juggle loose video and audio files later.


🎯 2. Full Post-Production Flexibility

Recording each input (screen, webcam, mics) as independent streams gives you full control during editing:

  • Switch feeds dynamically (e.g., show webcam only when you're speaking).
  • Mix audio streams later, or discard noisy ones.
  • Choose which mic sounds best after the fact.
  • Apply effects, compression, and cuts per stream, not globally.

You’re not locked into decisions made during the recordingβ€”everything can be reprocessed non-destructively.


🎧 3. Multiple Audio Inputs = Creative + Practical Options

Different mics capture different characteristics:

  • A USB mic may sound richer, but pick up more room noise.
  • A headset mic might be clearer, but have occasional artifacts.
  • Your MacBook mic may be the fallbackβ€”good enough to recover a segment if others fail.

By recording them all, you get:

  • Redundancy against failure or interference.
  • Options for voice blending, denoising, or segment replacement.
  • A way to analyze or compare mic performance over time.

🧼 4. β€œUnadulterated” Master: No Filters, No Overlays

The master .mkv is raw in layout but clean in compression:

  • No overlays (e.g., webcam-in-corner)
  • No burned-in effects or crops
  • No baked audio mixing

This ensures:

  • Maximum editing flexibility
  • Re-usable base for different publishing formats (e.g., full walkthrough vs short clip)

πŸ—‚οΈ TL;DR:

The master recording is your source of truth: a compact, hardware-encoded, timestamp-aligned archive of everything you captured. You can cut, convert, compress, remix, or reframe as neededβ€”without re-recording or losing fidelity.


πŸ› οΈ Scripts Overview

βœ… check-hardware.sh

Checks whether your FFmpeg installation supports hardware acceleration for:

  • hevc_videotoolbox (GPU-accelerated H.265)
  • aac_at (hardware-accelerated AAC audio)

Run this once to confirm support:

./check-hardware.sh

πŸŽ›οΈ list-capture-devices.sh

Lists all available video and audio input devices recognized by FFmpeg via AVFoundation (macOS only).

./list-capture-devices.sh

You'll see output like:

AVFoundation video devices:
[0] FaceTime HD Camera
[1] iPhone Camera
[2] Capture screen 0

AVFoundation audio devices:
[0] Corsair Wireless Headset
[1] USB Mic
[2] MacBook Pro Microphone

Use these indices to configure your recording script.


🎬 record-video.sh

Records:

  • βœ… Full desktop screen capture
  • βœ… Webcam (FaceTime or external)
  • βœ… Multiple audio sources (up to 3 mics shown, easily extensible)
  • βœ… Cursor + mouse click highlights
  • βœ… Timestamped filename for archival
  • βœ… Per-stream metadata (for easier inspection/editing later)
./record-video.sh

This script muxes all video + audio inputs into a single .mkv master file, with semi-lossless quality (HEVC video + high bitrate AAC). You'll see something like:

master_2025-06-24_15-32-12.mkv

You can later process this into overlays, cropped exports, etc.


πŸŽ₯ Output File Details

The .mkv file will contain multiple labeled streams:

Track Description
v:0 Screen capture
v:1 Webcam (FaceTime HD)
a:0 Corsair wireless mic
a:1 USB mic (device 100009002)
a:2 MacBook Pro microphone

Inspect the result with:

ffprobe -show_streams -pretty master_*.mkv

Or view in VLC > ⌘I > Codec Details.


🧱 Requirements

  • macOS (tested on 2019 MacBook Pro)
  • FFmpeg with --enable-videotoolbox --enable-audiotoolbox
  • Install via Homebrew:
brew install ffmpeg

πŸ”’ Why MKV?

  • Supports multiple streams cleanly (more than MP4)
  • Doesn’t finalize the file until recording ends (safe on crash)
  • Easy to remux to MP4 later:
ffmpeg -i master.mkv -c copy output.mp4

πŸ’‘ Tips

  • You can toggle -capture_cursor or -capture_mouse_clicks if you want a clean screen.
  • Add more audio streams with more -map lines and -metadata titles.
  • Sync is guaranteed as long as streams are captured live and muxed together.
#!/usr/bin/env bash
set -euo pipefail
ffmpeg -encoders | grep -E 'hevc_videotoolbox|aac_at'
#!/usr/bin/env bash
set -euo pipefail
ffmpeg -f avfoundation -list_devices true -i ""
#!/usr/bin/env bash
set -euo pipefail
# record-video.sh β€” Capture screen + webcam + multiple audio inputs (macOS, FFmpeg)
#
# Records screen (with cursor & click highlights), webcam, and 3 audio streams into a
# synchronized .mkv master using hardware-accelerated H.265 video and high-quality AAC audio.
# Perfect for project walkthroughs, voiceover recordings, or archival self-demos.
#
# See README.md for device setup and stream labels.
# ── Device indices ───────────────────────────────────────────────
SCREEN_IDX="2:none" # Screen capture
WEBCAM_IDX="0" # FaceTime HD Camera
CORSAIR_IDX=":0" # Corsair wireless headset mic
USB_IDX=":1" # Mystery mic (100009002)
INT_MIC_IDX=":2" # MacBook Pro Mic
OUT="master_$(date +%Y-%m-%d_%H-%M-%S).mkv"
ffmpeg \
-capture_cursor 1 -capture_mouse_clicks 1 \
-f avfoundation -framerate 30 -video_size 1920x1080 -i "$SCREEN_IDX" \
-f avfoundation -framerate 30 -i "$WEBCAM_IDX" \
-f avfoundation -i "$CORSAIR_IDX" \
-f avfoundation -i "$USB_IDX" \
-f avfoundation -i "$INT_MIC_IDX" \
\
# ── Video streams ──────────────────────────────────────────────
-map 0:v -c:v:0 hevc_videotoolbox -pix_fmt yuv420p10le -b:v:0 20M \
-metadata:s:v:0 title="Screen Capture" \
-map 1:v -c:v:1 hevc_videotoolbox -pix_fmt yuv420p10le -b:v:1 8M \
-metadata:s:v:1 title="Webcam (FaceTime HD)" \
\
# ── Audio streams ──────────────────────────────────────────────
-map 2:a -c:a:0 aac_at -b:a:0 256k \
-metadata:s:a:0 title="Corsair Wireless Mic" \
-map 3:a -c:a:1 aac_at -b:a:1 256k \
-metadata:s:a:1 title="USB Mic (100009002)" \
-map 4:a -c:a:2 aac_at -b:a:2 256k \
-metadata:s:a:2 title="MacBook Pro Mic" \
\
-metadata title="Pilot Recording $(date +%Y-%m-%d)" \
-f matroska "$OUT"
@colbyn
Copy link
Author

colbyn commented Jun 24, 2025

This has been a total nightmare

@colbyn
Copy link
Author

colbyn commented Jun 24, 2025

It was such an elegant idea on paper

@colbyn
Copy link
Author

colbyn commented Jun 24, 2025

So for future reference, rec (brew install sox) is great, but AFAIK I can't specify the input device, it works with the device default at the global system level... Not ideal but I can live with that for now and just record a single audio track.

Perhaps it'll setup rec to stream to a named pipe and configure such as a source for FFmpeg to then mux...

Next, screen capture isn't great, still working on that, camera feed is acceptable so far but I'm still having problems getting A/V properly time synced... When ffmpeg is packaging multiple live feeds into a single container, it's like each individual feed is being processed asynchronously and won't necessarily be time synced as expected which almost defeats the point. I could just as well use standalone tools for each source type and use ffmpeg in a post-processing batch mode based workflow.

Here is a dumb of everything so far.

old-scripts/audio/mic-clean.sh:

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

RAW_IN=$(ls -t mic_raw_*.wav | head -n1) || { echo "❌ No raw mic files found."; exit 1; }
BASE="${RAW_IN%.wav}"
CLEAN_OUT="${BASE/raw/cleaned}.m4a"

echo "🧼 Cleaning $RAW_IN β†’ $CLEAN_OUT"

ffmpeg \
  -hide_banner -loglevel warning \
  -i "$RAW_IN" \
  -af "highpass=f=100,afftdn=nf=-25,aresample=resampler=soxr" \
  -c:a aac_at -b:a 192k \
  "$CLEAN_OUT"

echo "βœ… Cleaned audio written: $CLEAN_OUT"

old-scripts/audio/mic-record-raw.sh:

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

MIC_IDX="${1:-:0}"
TIMESTAMP=$(date "+%Y-%m-%d--%H-%M-%S")
OUT="mic_raw_${TIMESTAMP}.wav"

echo "πŸŽ™οΈ Recording to $OUT β€” Ctrl+C to stop."

ffmpeg \
  -hide_banner -loglevel warning \
  -f avfoundation -i "$MIC_IDX" \
  -c:a pcm_s16le -ar 48000 -ac 1 \
  "$OUT"

old-scripts/audio/record-mic.sh:

#!/bin/bash
set -euo pipefail

MIC_IDX=":0"  # USB mic index
TIMESTAMP=$(date "+%Y-%-m-%-d--%-I:%M%p" | sed 's/AM/am/;s/PM/pm/')
OUT="mic_recording_${TIMESTAMP}.m4a"

echo "⏺️  Recording mic to $OUT β€” press Ctrl+C when done..."

ffmpeg \
  -hide_banner -loglevel warning \
  -f avfoundation -i "$MIC_IDX" \
  -af "afftdn=nf=-25,aresample=resampler=soxr" \
  -sample_fmt s16 \
  -c:a aac_at -b:a 192k -ar 48000 -ac 1 \
  "$OUT"

echo "βœ… Mic recording complete: $OUT"

old-scripts/helpers/check-hardware.sh:

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

ffmpeg -encoders | grep -E 'hevc_videotoolbox|aac_at'

old-scripts/helpers/clean.sh:

#!/bin/bash
set -e

echo "⚠️  This will delete all .m4a and .mkv files in the current directory:"
ls *.m4a *.mkv *.wav 2>/dev/null || echo "(No matching files found)"

read -rp "❓ Are you sure you want to proceed? (y/n): " CONFIRM
if [[ "$CONFIRM" != [yY] ]]; then
  echo "❌ Cleanup cancelled."
  exit 1
fi

rm -v *.m4a *.mkv *.wav

echo "βœ… Cleanup complete."

old-scripts/helpers/list-capture-devices.sh:

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

# ffmpeg -f avfoundation -list_devices true -i ""

ffmpeg -hide_banner -loglevel info -f avfoundation -list_devices true -i "" 2>&1 \
  | grep -E "^\[AVFoundation.*\] \[[0-9]+\]|AVFoundation (video|audio) devices:"

old-scripts/record-video.sh:

#!/bin/bash
set -e  # Exit on any error

# Device indices (check with: ffmpeg -f avfoundation -list_devices true -i "")
SCREEN_IDX="2:none"     # Screen capture
WEBCAM_IDX="0"          # FaceTime HD Camera (Built-in)
USB_MIC_IDX=":0"        # 100009002 USB microphone

# Output file with timestamp
TIMESTAMP=$(date "+%Y-%-m-%-d--%-I:%M%p")
OUT="recording_${TIMESTAMP}.mkv"

# Run FFmpeg
ffmpeg \
  -hide_banner -loglevel warning \
  \
  -thread_queue_size 512 \
  -f avfoundation -framerate 30 \
  -capture_cursor 1 -capture_mouse_clicks 1 \
  -i "$SCREEN_IDX" \
  \
  -thread_queue_size 512 \
  -f avfoundation -framerate 30 -pixel_format uyvy422 -video_size 1280x720 \
  -i "$WEBCAM_IDX" \
  \
  -itsoffset 0.3 \
  -thread_queue_size 512 \
  -f avfoundation -i "$USB_MIC_IDX" \
  \
  -map 0:v \
    -c:v:0 h264_videotoolbox -b:v:0 10M \
    -pix_fmt yuv420p \
    -metadata:s:v:0 title="Screen Capture" \
  \
  -map 1:v \
    -c:v:1 h264_videotoolbox -b:v:1 4M \
    -metadata:s:v:1 title="Webcam (FaceTime HD)" \
  \
  -map 2:a \
    -c:a:0 aac_at -b:a:0 192k -ar 48000 \
    -metadata:s:a:0 title="USB Mic (100009002)" \
  \
  -metadata title="Recording $TIMESTAMP" \
  -f matroska "$OUT"

echo "βœ… Recording complete: $OUT"

old-scripts/video/extract-webcam.sh:

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

# ── Usage ─────────────────────────────────────────────────────────
# ./extract-webcam.sh <input_file.mkv> [video_stream_index] [audio_stream_index]
#
# Defaults:
#   video_stream_index = 1    (Webcam)
#   audio_stream_index = 0    (Corsair Mic)
#   Output: webcam_extract_<timestamp>.mkv
# ───────────────────────────────────────────────────────────────────

FILE="${1:-}"
VID_IDX="${2:-1}"
AUD_IDX="${3:-0}"
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
OUT="webcam_extract_${TIMESTAMP}.mkv"

if [[ -z "$FILE" ]]; then
  echo "❌ No input file provided."
  echo "Usage: ./extract-webcam.sh <input_file.mkv> [video_stream_index] [audio_stream_index]"
  exit 1
fi

if [[ ! -f "$FILE" ]]; then
  echo "❌ File not found: $FILE"
  exit 1
fi

echo "πŸŽ₯ Extracting from: $FILE"
echo "   β€’ Video stream: 0:v:$VID_IDX"
echo "   β€’ Audio stream: 0:a:$AUD_IDX"
echo "   β†’ Output: $OUT"

ffmpeg -i "$FILE" \
  -map 0:v:$VID_IDX -map 0:a:$AUD_IDX \
  -c copy "$OUT"

echo "βœ… Done: $OUT"

old-scripts/video/headset-mode-record-video.sh:

#!/bin/bash
set -e

# Device indices (from `ffmpeg -f avfoundation -list_devices true -i ""`)
SCREEN_IDX="2:none"   # Screen capture
WEBCAM_IDX="0"        # FaceTime HD Camera (Built-in)
CORSAIR_IDX=":0"      # Corsair Wireless Mic (external audio device)

# Output file with timestamp
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
OUT="recording_${TIMESTAMP}.mkv"

# Run FFmpeg
ffmpeg \
  -hide_banner -loglevel warning \
  \
  -thread_queue_size 512 \
  -f avfoundation -framerate 30 \
  -capture_cursor 1 -capture_mouse_clicks 1 \
  -i "$SCREEN_IDX" \
  \
  -thread_queue_size 512 \
  -f avfoundation -framerate 30 -pixel_format uyvy422 -video_size 1280x720 \
  -i "$WEBCAM_IDX" \
  \
  -itsoffset 0.3 \
  -thread_queue_size 512 \
  -f avfoundation \
  -i "$CORSAIR_IDX" \
  \
  -map 0:v \
    -c:v:0 h264_videotoolbox -b:v:0 10M \
    -pix_fmt yuv420p \
    -metadata:s:v:0 title="Screen Capture" \
  \
  -map 1:v \
    -c:v:1 h264_videotoolbox -b:v:1 4M \
    -metadata:s:v:1 title="Webcam (FaceTime HD)" \
  \
  -map 2:a \
    -c:a:0 aac_at -b:a:0 192k -ar 48000 \
    -metadata:s:a:0 title="Corsair Wireless Mic" \
  \
  -metadata title="Recording $TIMESTAMP" \
  -f matroska "$OUT"

echo "βœ… Recording complete: $OUT"

old-scripts/video/preview-stream.sh:

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

FILE="$1"
STREAM_IDX="${2:-1}"

# ffmpeg -i "$FILE" -map 0:v:$STREAM_IDX -c copy -f matroska - | ffplay -

ffmpeg -i master_2025-06-24_14-03-00.mkv -map 0:v:1 -map 0:a:0 -c copy -f matroska - | ffplay -

run.sh:

ffmpeg \
  -f avfoundation -i "none:MacBook Pro Microphone" \
  -c:a pcm_s16le -ar 96000 -ac 1 \
  output/audio/test_macbook_mic.wav

scripts/audio/post-process.sh:

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

INPUT="${1:-}"

if [[ -z "$INPUT" ]]; then
  echo "❌ No input file provided."
  echo "Usage: ./scripts/audio/post-process.sh path/to/mic_raw_<timestamp>.wav"
  exit 1
fi

if [[ ! -f "$INPUT" ]]; then
  echo "❌ File not found: $INPUT"
  exit 1
fi

BASENAME="$(basename "$INPUT" .wav)"
TIMESTAMP="${BASENAME#mic_raw_}"
OUT="output/audio/mic_cleaned_${TIMESTAMP}.m4a"

mkdir -p "$(dirname "$OUT")"
echo "🧼 Cleaning $INPUT β†’ $OUT"

ffmpeg \
  -hide_banner -loglevel warning \
  -i "$INPUT" \
  -af "highpass=f=100,afftdn=nf=-25,aresample=resampler=soxr" \
  -c:a aac_at -b:a 192k \
  "$OUT"

echo "βœ… Cleaned file written: $OUT"

scripts/audio/record-raw.sh:

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

MIC_IDX="${1:-:0}"
TIMESTAMP=$(date "+%Y-%m-%d--%H-%M-%S")
OUT="output/audio/mic_raw_${TIMESTAMP}.wav"

mkdir -p "$(dirname "$OUT")"
echo "πŸŽ™οΈ Recording to $OUT β€” press Ctrl+C to stop."

ffmpeg \
  -hide_banner -loglevel warning \
  -f avfoundation -i "$MIC_IDX" \
  -c:a pcm_s16le -ar 48000 -ac 1 \
  "$OUT"

echo "βœ… Raw recording saved: $OUT"

scripts/utilities/get-mic-index.sh:

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

QUERY="${1:-USB}"

ffmpeg -f avfoundation -list_devices true -i "" 2>&1 \
  | grep -E "^\[AVFoundation.*\] \[[0-9]+\]" \
  | grep -i "$QUERY" \
  | sed -E 's/.*\[(.*)\] (.*)/\1\t\2/'

scripts/utilities/list-devices.sh:

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

ffmpeg -f avfoundation -list_devices true -i "" 2>&1 \
  | awk '
    /AVFoundation video devices:/     { print "Video Devices:"; next }
    /AVFoundation audio devices:/     { print "\nAudio Devices:"; next }
    /^\[AVFoundation.*\] \[[0-9]+\]/  { sub(/^.*\] \[/, "["); print }
  '

@colbyn
Copy link
Author

colbyn commented Jun 24, 2025

Note: at the system level I set my display to 1440 Γ— 810 to better match the target aspect ratio for publication...

@colbyn
Copy link
Author

colbyn commented Jun 25, 2025

I’m definitely learning way more about ffmpeg than I would prefer to know… ffmpeg is supposed to just work and without requiring too much cognitive overhead

@colbyn
Copy link
Author

colbyn commented Jun 25, 2025

NOTE: See the Sox cheat sheet

Turns out it supports specifying the input device type.

➜  YouTubeProjects sox -V -t coreaudio null -n 2>&1 | grep "Found Audio" | cut -d'"' -f2
Colbyn's iPhone Microphone
100009002
MacBook Pro Microphone
MacBook Pro Speakers
Microsoft Teams Audio

@colbyn
Copy link
Author

colbyn commented Jun 25, 2025

Okay amazing idea but av foundation is terrible I’d rather just use a dumb Linux server to capture and process everything… Although it raises new questions if I’m showcasing my iOS / macOS specific work but perhaps I can think of something clever or maybe just write my own macOS tooling one day… For now im probably just gonna resign to OBS Studio because duck this…

@colbyn
Copy link
Author

colbyn commented Jun 25, 2025

They say real men only use TUIs …

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment