Screen and Webcam Recording I Never Have to Think About

Posted by Michael S. on March 15, 2026

On my old Mac, I built a pair of tiny daemons that record my screen and webcam at very low frame rates. They use almost no RAM, almost no CPU, and they never pop up in my face. Today I finally moved that setup onto my daily-driver machine.

The whole point is that I want to stop thinking about how much my computer is using. I want every display and my webcam quietly logged in the background, without touching OBS, without fiddling with scenes, and without remembering to hit Record.


The Exact Files This Setup Creates

On this Mac, the screen capture daemon lives in four places:

  1. Configuration file (dynamic framerate and other settings)
    /Users/michael/.config/screen-capture/config.env
  2. LaunchAgent service file (auto-starts at login and restarts on boot)
    /Users/michael/Library/LaunchAgents/com.michael.screen-capture.plist
  3. Main screen capture daemon script (enumerates displays and runs ffmpeg)
    /usr/local/bin/screen-capture-daemon.sh
  4. Daily stitching script (merges segments into one video per day)
    /usr/local/bin/daily-stitch.sh

Those four files together give me always-on, low-framerate screen capture across all monitors, with daily rollups. The binaries themselves do the heavy lifting; the scripts just glue together ffmpeg, LaunchAgents, and a simple config.env.

For the full implementation, including the actual shell scripts and LaunchAgent plist, I pushed everything to GitHub:


Matching Webcam Daemon

I also run an identical setup for my webcam: a LaunchAgent-managed daemon that uses ffmpeg to capture the default camera (usually FaceTime HD) at 1 FPS into daily folders. Same pattern:

  • a ~/.config/... directory for configuration,
  • a LaunchAgent plist in ~/Library/LaunchAgents,
  • a tiny shell script in /usr/local/bin that wraps ffmpeg.

Between the two, I get a full visual log: screens plus webcam, stitched into one file per display and one file for the camera per day. It all runs in the background, and my RAM graph barely notices compared to running OBS at full tilt.


Update (March 19, 8:30 p.m.): The -threads 1 Optimization

A few days in, I asked Gemini 3.1 Pro a simple question: "Can you optimize (minimize) RAM usage?" Turns out it had originally set up ffmpeg without forcing a single thread, and the default libx264 behavior is to spin up one encoding thread per CPU core. At 2 FPS, that multi-threading does absolutely nothing useful, but each thread still allocates its own memory buffer.

The fix is one flag: -threads 1.

screen-capture-daemon.sh (diff)
  ffmpeg -f avfoundation -framerate "$FRAMERATE" -capture_cursor 1 -i "${DEVICE}:none" \
    -vf "${FPS_FILTER},scale=1920:-2" \
-   -c:v libx264 -crf 28 -preset ultrafast \
+   -c:v libx264 -crf 28 -preset ultrafast -threads 1 \
    -movflags frag_keyframe+empty_moov \
    -t "$DURATION" \
    -y "$OUTFILE"

The same change applies to the webcam daemon (webcam-capture-daemon.sh), which was also defaulting to multi-threaded encoding for no reason at 1 FPS.

A quick note on the process: Gemini didn't figure this out on its own. I asked the right question, it realized the issue, and we fixed it together. It might have noticed eventually if I kept it in the same conversation long enough, but it wasn't instant, and if I had started a new chat it would never have known. This is still very much a human-steered workflow.

Results

After restarting both daemons with the new flag, I checked RAM usage with ps:

Before (-threads default) After (-threads 1)
Screen daemon ~480 MB ~17 MB
Webcam daemon ~72 MB ~22 MB
Combined ~554 MB ~39 MB

That is a 93% reduction in memory usage. The daemons are now virtually invisible to the system. Both repos on GitHub have been updated with the second commit.


Goodbye, OBS

With this running, I can finally delete the "Refresh OBS Video Source" AppleScript app I built to keep OBS's capture alive. The only thing OBS still has over this setup is audio recording. I will set that up as its own daemon when I need it. Ideally it would run at the hardware level rather than the software level, so it stays on as long as the kernel has power.


Update (Friday, March 20, 1:00 a.m.): Speaking of storage and OneDrive. I just started deleting files so OneDrive would sync properly again. Deleted most of my Zed source folder (I had wanted to fix an issue I ran into, which can be bypassed with Ctrl+`. I created the PR, but I have my workaround for now, so it doesn't bother me too much). But the target/ and tooling/ folders refused to delete themselves. And anyway... 27 GIGABYTES??? The target folder, where Rust builds the app... Finder can't even figure out how big it is, but du -sh told me right away. 27 GB. No wonder I'm running out of storage. Deleting now. 25,000+ files, 25+ GB.

Enjoyed this post?

Get notified when I publish something new. No spam, unsubscribe anytime.