A small, native macOS menu-bar screen recorder and screenshot tool. Record a window, a screen or a dragged area and save it as MP4, WebM or GIF, or take a screenshot and mark it up with arrows, text, pixelation and a crop. Everything stays on your Mac.
Requires macOS 15 Sequoia or later. MIT licensed.
Discuss it on Hacker News or leave a review on Product Hunt.
Download the DMG from the latest release, or use Homebrew:
brew install --cask soleo/tap/tinycapTinycap checks for updates itself, so brew upgrade leaves it alone unless you pass --greedy.
Recording (capture, trim, MP4 / WebM / GIF export), screenshots with an annotation editor, and daily update checks are in place.
Capture
- Menu-bar app with no Dock icon: Screenshot Area / Window / Screen, Record Window / Screen / Area, Open Recent, Settings, Report a Bug, Send Feedback, Quit. Opening the app again while it runs shows a small window with the record buttons.
- Area mode: dimmed overlay on every display, drag a rectangle, live W × H in points, move/resize handles, Return to record, Esc to cancel, floating bar with frame rate, cursor, system audio and microphone
- Window mode: hover highlights the window under the cursor, click to record; keeps recording when the window moves
- Screen mode: picks the display straight away, or lets you click one when there are several
- ScreenCaptureKit (
SCRecordingOutput) writes an H.264 master (with sound, Tinycap writes it itself from the video stream and a separate audio stream); 15/30/60 fps, Retina or 1×, cursor on/off - Optional 3-2-1 countdown and click highlights (yellow rings) in every recording mode
- Optional sound: system audio (everything the Mac plays, in every mode) and the microphone, from the input device you pick in the menu, the area bar or Settings. They're recorded as separate tracks; Microphone permission is asked the first time you turn the mic on. The mic goes through macOS voice processing by default, so speaker sound doesn't echo in it (Settings → Recording → Reduce echo from speakers)
- While recording, the menu-bar icon shows the elapsed time; click it to stop
Preview and export
- On stop, a preview window plays the clip
- Trim with start/end handles, or press I / O to set them at the playhead; Space plays and pauses (loops inside the trim)
- MP4 as H.264 or HEVC, quality slider, size Full / Half / fixed width, with output size and an estimated file size
- Sound plays in the preview, with a switch per source; MP4 mixes the sources that are on into one AAC track, or keeps them as separate tracks for video editors (browsers play only the first). WebM and GIF are video only
- WebM (VP9) with a quality slider and size; keeps the recording's real frame timing, tagged BT.709 so browsers show the right colors
- GIF with presets (Small, Balanced, High), frame rate 5–30, size, 32–256 colors, ordered dithering, loop, duplicate-frame dropping, and "Fit under N MB"
- File-size estimate from a short sample encode, for MP4 and GIF
- After export: Copy (paste the file into Slack, Mail, Finder), Show in Finder, or drag the file out
- Closing an unexported recording asks first; recordings left by a quit or crash are offered again at the next launch
Screenshots
- Same overlays as recording; full Retina resolution; window shots keep transparent corners, shadow optional
- After capture: a floating thumbnail (click to edit, drag out, saved if left alone), or open the editor, copy, or save
- Editor: arrows (Shift snaps to 45°, bend from the middle handle), rectangles and ellipses (Shift for a square or circle), text with an optional background, pen, highlighter (Shift draws straight), numbered badges that renumber themselves, pixelate, crop; select, move, resize, delete, undo/redo; six colors plus a picker
- Tool keys: V select, A arrow, R rectangle, O ellipse, T text, D pen, H highlighter, N number, P pixelate, C crop
- Export: ⌘C, ⌘S or drag out; PNG or JPEG at 2× or 1× with the pixel and file size shown; no metadata in the file
Settings
- General: launch at login, save folder, file name pattern, daily update check, editable global shortcuts (defaults ⌃⌥⌘4 screenshot area, ⌃⌥⌘A area, ⌃⌥⌘W window, ⌃⌥⌘S screen, ⌃⌥⌘. stop)
- Recording: frame rate, cursor, Retina, click highlights, countdown
- Export: default format, MP4 codec/quality/size, WebM quality/size, GIF preset, loop and fit target
- Screenshot: after capture, format, scale, window shadow, default annotation style
- About: version, build date and commit, links, Report a Bug, Send Feedback, Copy Diagnostics, Acknowledgements
Next: screenshot self-timer; the App Store.
Tinycap has its own GIF encoder (Sources/TinycapCore/GIFEncoder.swift), written for screen recordings and free of license constraints (gifski is AGPL, which doesn't fit the MIT license or the App Store):
- One palette for the whole clip, built by median cut from frames across the kept range, so colors don't flicker.
- After the first frame, each frame stores only the box that changed, with unchanged pixels transparent.
- Frames where nothing changed are merged into one longer frame.
- Optional ordered dithering, which stays still between frames instead of shimmering.
scripts/build-app.sh # release, universal (arm64 + x86_64) → build/Tinycap.app
CONFIG=debug scripts/build-app.sh
scripts/test.sh # unit tests for TinycapCore + WebM bridge round trip
scripts/export-check.sh # encodes a synthetic recording to MP4, WebM and GIF and verifies the files
scripts/build-vpx.sh # builds libvpx + libwebm from pinned source (run automatically by the others)
open build/Tinycap.appswift build and swift test also work, and Package.swift opens in Xcode (run scripts/build-vpx.sh once first).
WebM export uses libvpx 1.15.0 (VP9) and libwebm 1.0.0.31 (container), both BSD-licensed. scripts/build-vpx.sh downloads the tagged source, checks SHA-256 hashes pinned in the script, and builds static universal libraries into build/vendor (about 1.5 minutes, then cached). Nothing prebuilt is downloaded. Licenses are in THIRD_PARTY_NOTICES.md and in the app under Settings → General → Acknowledgements.
Install nasm (brew install nasm) before building releases. Without it the Intel slice of libvpx is built as portable C, which works but encodes VP9 several times slower on Intel Macs.
The build script calls swiftc directly so it also runs inside restricted shells. If the shell is itself sandboxed and SwiftUI macros fail to expand ("swift-plugin-server produced malformed response"), run with NESTED_SANDBOX=0.
Signing defaults to ad hoc. Ad-hoc builds get a new signature each time, so macOS asks for Screen Recording permission again after every rebuild. To avoid that, sign with a stable identity:
SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" scripts/build-app.shOther variables: BUNDLE_ID (default app.tinycap.Tinycap), VERSION, BUILD_NUMBER, ARCHS.
scripts/make-dmg.sh builds the direct-download release. It:
- builds the universal app, signed with Developer ID and the hardened runtime
- packages it with an Applications shortcut, then signs, notarizes and staples the DMG
- checks the DMG with Gatekeeper and writes its SHA-256 next to it
- copies the DMG to
Tinycap-<version>-update.dmgand writesappcast.xmlpointing at the copy, signed with the Sparkle EdDSA key. The copy is byte-identical; its separate name lets GitHub count in-app updates apart from new downloads. - prints the
gh release createcommand to publish it all
One-time setup on the release Mac:
# Developer ID Application certificate in the login keychain (Xcode → Settings → Accounts)
brew install nasm # release builds require it, so the Intel slice of libvpx gets SIMD
# notarization credentials, stored in the login keychain
xcrun notarytool store-credentials tinycap-notary --apple-id you@example.com --team-id TEAMID
# update-signing key: created in the keychain, public half written to Resources/sparkle-public-key.txt (commit it)
scripts/make-appcast.sh --setup-key
# back up the private key to 1Password, then delete the file
build/vendor/Sparkle/bin/generate_keys -x sparkle-private-key.txtWithout the Sparkle private key, existing installs can't verify future updates, so keep the backup.
Each release:
-
On a branch, bump
VERSION(for example0.4.0) and writeReleaseNotes/<version>.mdfor users. The notes are converted to HTML for the update window and reused as the GitHub release notes. Open a PR and merge it. -
On an up-to-date, clean
main, build, notarize and sign the feed (a few minutes):git switch main && git pull SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" NOTARY_PROFILE=tinycap-notary \ scripts/make-dmg.sh
Output goes to
build/release/. The build number is the commit count, so build from the merged commit. -
Run the
gh release createcommand the script printed. It tags the commit the DMG was built from and attaches the DMG, its.sha256, the update copy andappcast.xml. The appcast must be on the latest release: the app readsreleases/latest/download/appcast.xml. -
Check that the feed serves the new version:
curl -sL https://github.com/soleo/tinycap/releases/latest/download/appcast.xml | grep shortVersionString -
Update the Homebrew cask in soleo/homebrew-tap. The script takes the checksum from the published release:
scripts/bump-cask.sh ../homebrew-tap # sets version and sha256 in Casks/tinycap.rb brew audit --cask --online soleo/tap/tinycap git -C ../homebrew-tap commit -am "tinycap <version>" && git -C ../homebrew-tap push
(On Homebrew 7, run
brew trust --cask soleo/tap/tinycaponce before auditing.) -
Update the landing page on the
gh-pagesbranch (git worktree add ../tinycap-web gh-pages): the two download links, the version and the DMG size. Searchindex.htmlfor the old version number.
scripts/download-stats.sh prints downloads, in-app updates and update checks per release.
Test DMG: scripts/make-dmg.sh --unsigned makes an ad-hoc signed, un-notarized DMG for local testing. Gatekeeper will warn on other Macs, and it has no appcast.
Two known, harmless messages appear during the release build:
ld: warning: no platform load command found in '…libvpx.a[x86_64](….asm.o)': nasm doesn't tag its object files with a macOS version, and the linker correctly assumes macOS.hdiutil: WARNING: … is deprecated:hdiutil createstill works; the script will move todiskutil imageonce it's verified on a release machine.
scripts/make-appstore.sh builds the Mac App Store package. It's the same app with CHANNEL=appstore: no Sparkle, no update check and no network entitlement, signed with an Apple Distribution certificate and the App Store provisioning profile, then wrapped in a signed installer package.
SIGN_IDENTITY="Apple Distribution: Your Name (TEAMID)" \
INSTALLER_IDENTITY="3rd Party Mac Developer Installer: Your Name (TEAMID)" \
PROVISIONING_PROFILE=~/path/Tinycap_App_Store.provisionprofile \
scripts/make-appstore.sh # → build/appstore/Tinycap-<version>.pkgUpload the package with Apple's Transporter app, try it through TestFlight, then submit the build for review in App Store Connect. Each upload needs a higher build number than the last; the commit count gives one per merged commit.
The direct build embeds Sparkle 2.10.0 (scripts/fetch-sparkle.sh, pinned by SHA-256) and checks the appcast once a day. The App Store build (CHANNEL=appstore) has no updater and no network access: the App Store updates it. The direct build shows a dot on the menu-bar icon, an "Update to Tinycap X.Y…" menu item and one quiet notification per version, and nothing appears while you select, record or export. A direct build made without Resources/sparkle-public-key.txt has no updater.
The version comes from the VERSION file (or VERSION=1.2.3). The build number is the commit count, for local builds too.
Resources/AppIcon.icns and Resources/AppIcon-1024.png are generated from the reference artwork TinyCapIcon.png:
scripts/make-icon.sh [path/to/TinyCapIcon.png]The script removes the light backdrop around the tile, centers it on Apple's 824 px icon grid and writes every size from 16 to 1024 px. The menu-bar glyph (MenuBarIcon.swift) is a pixel-art template image drawn from the same motif: capture corners around a window.
| Path | What it holds |
|---|---|
Sources/TinycapCore |
Pure logic with unit tests: geometry, file naming, settings, size sampling, GIF palette and encoder |
Sources/Tinycap/App |
Entry point, menu-bar item, settings, onboarding, save folder |
Sources/Tinycap/Selection |
Area, window and screen pickers; recording outline |
Sources/Tinycap/Capture |
ScreenRecorder (ScreenCaptureKit) |
Sources/Tinycap/Recording |
RecordingCoordinator: select → record → stop → preview |
Sources/Tinycap/Editor |
Preview window: player, trim scrubber, export controls |
Sources/Tinycap/Export |
Exporter protocol; MP4 (AVAssetReader → AVAssetWriter), WebM (I420 planes → VPXBridge), GIF (vImage → GIFEncoder); size estimates |
Sources/VPXBridge |
C interface over libvpx and libwebm (encoder + muxer, linked into the app) |
Sources/VPXInspect |
Parses and decodes WebM files; used only by the checks |
Sources/Tinycap/Hotkeys |
Global shortcuts via Carbon RegisterEventHotKey, shortcut recorder |
Resources |
Info.plist, entitlements, icon |
Tinycap's only network request is the daily update check, which you can turn off in Settings → General. The direct build fetches the appcast from GitHub; the App Store build asks Apple's public lookup API for the latest version by bundle ID. No telemetry, analytics or system profile is sent. The app is sandboxed with a network client entitlement for that check and no microphone or camera entitlements. Recordings are saved to ~/Movies/Tinycap until you choose another folder in Settings. The only file entitlements are the Movies folder and folders you pick.
MIT. See LICENSE.