kinrec

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2026 License: MIT Imports: 13 Imported by: 0

README

kinrec

Mac screen + audio → mp4. One binary, go install, no Electron.

kinrec is an open-source screen recorder for macOS. It's pure Go, no cgo, no app bundle, no Electron, no subscription. One command:

go install github.com/LocalKinAI/kinrec/cmd/kinrec@latest
kinrec record -o demo.mp4 --audio --duration 30s

Go Reference


Why

Recording your Mac screen today looks like this:

Tool Problem
QuickTime (built-in) No hotkeys, no system audio, no shortcuts
screencapture -v CLI Can't record system audio
OBS 200 MB download, Mac is a second-class citizen
CleanShot X $29, closed source
Screen Studio $89, closed source
Loom Subscription, login required
Kap (open source) Abandoned since 2022, Electron-based
aperture-node Abandoned, macOS 15+ broken

Everyone recording a screencast either pays, juggles OBS, or watches their recorder break with each macOS update.

kinrec is what happens when you start from scratch in 2026:

  • One command: go install + kinrec record -o x.mp4 --audio.
  • 5 MB binary, not a 200 MB Electron bundle.
  • Hardware H.264 / HEVC via VideoToolbox — no ffmpeg needed.
  • System audio via ScreenCaptureKit's native audio path (macOS 14+; skips the feedback loop from your own process).
  • macOS 15 / 26 ready — built on the modern SCStream + AVAssetWriter pipeline, not the deprecated CGDisplayCreateImage path that every other Go recorder (and many commercial ones) used.
  • Pure Go downstream: the ObjC dylib is embedded via //go:embed and auto-extracted. Your users never need clang, CGO_ENABLED, or a Mac developer toolchain.
  • Built on sckit-go — the same low-level ScreenCaptureKit bindings power both libraries.

Install

go install github.com/LocalKinAI/kinrec/cmd/kinrec@latest

That's it. No Homebrew cask, no .dmg, no signing ceremony.

First run triggers macOS permission prompts. Grant "Screen Recording" in System Settings → Privacy & Security → Screen Recording. If you use --mic, also grant "Microphone" on first use. Both permissions are per-binary and persist.


Usage

Record until Ctrl+C
kinrec record -o demo.mp4
Timed recording
kinrec record -o tutorial.mp4 --duration 5m
With system audio (what apps on screen play)
kinrec record -o meeting.mp4 --audio --duration 30m

Captures what Chrome / Spotify / Zoom / any app sends to the speakers. Your microphone is not recorded — use --mic for that.

With microphone (your voice — for tutorials)
kinrec record -o tutorial.mp4 --mic --duration 5m

Captures from the default input device. Triggers a one-time macOS "Microphone" permission prompt on first use.

Short-video preset (--shorts)
kinrec record --shorts -o clip.mp4 --duration 60s

One flag applies the X / TikTok / YT Shorts / Reels defaults: 1080×1920 vertical, 30 fps, --audio + --mic both on (mixed into a single track via AVAudioEngine). Any explicit --fps / --resolution / --audio / --mic you pass still wins.

Cursor highlight + click ripple (--click-highlight)
kinrec record --click-highlight -o tutorial.mp4 --mic --duration 2m

Draws a subtle white ring around the cursor at all times and a yellow expanding ripple every time you left-click. Matches the effect Screen Studio ($89) built its brand on. Requires no Accessibility permission.

Current v0.1 limits: works on Display-target capture at the display's native resolution. --click-highlight --shorts or --click-highlight --resolution WxH will misalign the overlay until coordinate mapping is generalized in v0.2.

Pick a specific mic
kinrec list mics
kinrec record -o demo.mp4 --mic --mic-device=<unique-id>
Mix: system audio + mic together (the Loom default)
kinrec record -o tutorial.mp4 --audio --mic --duration 5m

Both sources flow into an AVAudioEngine pipeline (two player nodes → main mixer → tap), and the mixed PCM is encoded to a single AAC track. Use this for:

  • Tutorials where you narrate over a video playing on screen
  • Zoom recordings that capture both yourself and the other side
  • Podcasts that include desktop-app audio
Custom framerate + resolution
kinrec record -o demo.mp4 --fps 30 --resolution 1280x720
HEVC for ~30% smaller files
kinrec record -o demo.mov --codec hevc --format mov
Pick a specific display
kinrec list displays
kinrec record -o ext.mp4 --display 2
Full flag reference
kinrec record [flags]

  -o, --output PATH      Output path (required)
      --duration D       Stop after this long (30s, 5m, 1h); 0 = Ctrl+C
      --fps N            Target frame rate (default 60)
      --resolution WxH   Output size (default: display native)
      --audio            Capture system audio (what apps play)
      --mic              Capture microphone (your voice)
      --mic-device ID    Mic AVCaptureDevice uniqueID (see `kinrec list mics`)
      --codec h264|hevc  Video codec (default h264)
      --format mp4|mov   Container (default mp4)
      --display ID       Specific display ID (default: main)
      --shorts           Vertical 1080x1920 + 30fps + audio + mic preset
      --click-highlight  Overlay cursor ring + click ripple (for demos)

Use as a library

package main

import (
    "context"
    "log"
    "time"

    "github.com/LocalKinAI/kinrec"
)

func main() {
    ctx := context.Background()
    err := kinrec.Record(ctx, 10*time.Second,
        kinrec.WithOutput("/tmp/demo.mp4"),
        kinrec.WithAudio(true),
        kinrec.WithFrameRate(60),
    )
    if err != nil {
        log.Fatal(err)
    }
}

For fine-grained control (start/stop pairs, live stats):

r, err := kinrec.NewRecorder(ctx,
    kinrec.WithOutput("demo.mp4"),
    kinrec.WithAudio(true),
)
r.Start(ctx)

go func() {
    for range time.Tick(time.Second) {
        s := r.Stats()
        log.Printf("frames=%d audio_bufs=%d", s.Frames, s.AudioBuffers)
    }
}()

<-someSignalToStop
r.Stop(ctx)

Output

All recordings are real mp4 / mov files with:

  • Video: H.264 High profile or HEVC, yuv420p, hardware-encoded via VideoToolbox (≈ 5 bits per pixel per 30fps-second baseline bitrate).
  • Audio (when --audio): AAC-LC, 48 kHz stereo, 128 kbps.
  • Container: mp4 (default, web-compatible) or mov.
  • Playable in QuickTime, Safari, Chrome, VLC, Premiere, DaVinci, iMovie.

Verified with ffprobe on a 5-second --audio capture:

Stream #0: Video: h264 (High), yuv420p, 1920x1080, 894 kb/s, 30 fps
Stream #1: Audio: aac (LC), 48000 Hz, stereo, 128 kb/s

Roadmap

v0.1.0 — Ship it (this week)
  • Display recording with system audio
  • Microphone recording (default input + device-selectable)
  • --audio --mic mixed via AVAudioEngine into a single track
  • kinrec list mics to enumerate input devices
  • H.264 + HEVC + mp4 + mov
  • Duration timer + Ctrl+C stop + live frame/audio-buffer counters
  • Hardware encoding via VideoToolbox
  • //go:embed universal dylib
  • README + CHANGELOG polished
  • v0.1.0 GitHub release
v0.2.0 — Quality of life
  • Window / app / region recording (via sckit-go target pass-through)
  • Progress bar with estimated file size
  • Automatic file-size limit (--max-size 500M)
  • Dual-track mode as an opt-in (--audio --mic --dual-track) — useful for editors who want to adjust the two sources independently
v0.3.0 — The magic features
  • Click highlight / ripple effects (the thing CleanShot charges $29 for)
  • Cursor smoothing
  • Auto-zoom on click (the thing Screen Studio charges $89 for)
  • Keystroke display (show ⌘C on screen when pressed)
  • Webcam picture-in-picture overlay
v0.4.0 — Share + edit
  • R2 / S3 upload with shareable link (--upload --share)
  • GIF export (--gif)
  • Timeline trim post-recording
  • Automatic thumbnail generation
v0.5.0 — UI (optional, if demand exists)
  • Menu bar app (SwiftUI wrapper around the same binary)
  • Global hotkey daemon

Architecture

  Go code
    │
    │  purego.RegisterLibFunc (no cgo)
    ▼
  libkinrec_writer.dylib  (~190 KB universal arm64+x86_64, //go:embed'd)
    │
    ├─── SCStream (video + audio capture)
    └─── AVAssetWriter (H.264 / HEVC + AAC mux)
                 ↓
              out.mp4

The dylib is ~400 lines of ObjC (objc/kinrec_writer.m). It wraps ScreenCaptureKit's async delivery with dispatch_semaphore + a SCStreamOutput delegate, forwards each CMSampleBuffer to an AVAssetWriterInput, and blocks on finishWritingWithCompletionHandler when the caller stops.

Hardware encoding is provided by AVAssetWriter selecting VideoToolbox automatically based on the codec key (AVVideoCodecTypeH264 / AVVideoCodecTypeHEVC). No external ffmpeg dependency.


Requirements

  • macOS 14 (Sonoma) or newer — SCStreamConfiguration.capturesAudio lands in macOS 13 but the full API matures in 14.
  • Xcode Command Line Tools (contributors only; downstream users do not need them thanks to //go:embed).
  • Go 1.22+.

Development

git clone https://github.com/LocalKinAI/kinrec
cd kinrec

make help           # list targets
make dylib          # build universal libkinrec_writer.dylib
make cli            # build ./kinrec
make verify         # build + vet + record a 3-second clip
make install-cli    # go install ./cmd/kinrec to $GOBIN
Layout
kinrec/
├── kinrec.go           # Load + Recorder + Record convenience
├── options.go          # functional options
├── cmd/kinrec/
│   ├── main.go         # subcommand dispatch
│   ├── record.go       # `kinrec record`
│   ├── list.go         # `kinrec list displays`
│   └── version.go
├── objc/
│   └── kinrec_writer.m # SCStream → AVAssetWriter pipeline
└── internal/dylib/
    ├── dylib.go        # //go:embed
    └── libkinrec_writer.dylib

License

MIT — see LICENSE.

Built by LocalKin AI on top of sckit-go. Dogfood of the primitive capture library — proves that sckit-go is strong enough to build full products on.

Documentation

Overview

Package kinrec records the macOS screen (and optionally system audio) into an H.264/HEVC .mp4 or .mov file.

Built on top of ScreenCaptureKit + AVAssetWriter via a small embedded ObjC dylib. Pure Go on the calling side (no cgo needed downstream).

Quick start:

ctx := context.Background()
r, _ := kinrec.NewRecorder(ctx,
    kinrec.WithOutput("/tmp/demo.mp4"),
    kinrec.WithAudio(true),
    kinrec.WithFrameRate(60),
)
r.Start(ctx)
time.Sleep(10 * time.Second)
r.Stop(ctx)

For duration-bounded recordings use the blocking helper:

kinrec.Record(ctx, 10*time.Second,
    kinrec.WithOutput("/tmp/demo.mp4"),
    kinrec.WithAudio(true),
)

Requirements

macOS 14 (Sonoma) or newer. First run triggers the "Screen Recording" TCC prompt (and "Microphone" if --mic is set in a future release).

Index

Constants

View Source
const Version = "0.1.0"

Version is the semver tag for this build. Updated per release.

Variables

View Source
var DylibPath = ""

DylibPath optionally overrides the embedded dylib. Leave empty (default) to use the bundled copy auto-extracted to the user cache.

View Source
var ErrAlreadyStarted = errors.New("kinrec: recorder already started")

ErrAlreadyStarted is returned when Start is called twice.

View Source
var ErrNotStarted = errors.New("kinrec: recorder not started")

ErrNotStarted is returned when Stop is called before Start.

View Source
var ErrPermissionDenied = errors.New("kinrec: screen recording permission denied")

ErrPermissionDenied is returned when macOS Screen Recording (and, for future mic capture, Microphone) permission has not been granted.

Functions

func Load

func Load() error

Load explicitly loads the companion dylib. Idempotent; the first public call triggers it implicitly.

func Record

func Record(ctx context.Context, duration time.Duration, opts ...Option) error

Record captures for the given duration and finalizes the output file. Equivalent to NewRecorder + Start + time.Sleep + Stop; returns only when the file is fully on disk.

func ResolvedDylibPath

func ResolvedDylibPath() string

ResolvedDylibPath returns the filesystem path Dlopen'd by the current process. Empty until the first Load call.

Types

type Codec

type Codec int

Codec selects the video encoder.

const (
	// CodecH264 is broadly compatible baseline. Default.
	CodecH264 Codec = 0
	// CodecHEVC (H.265) is ~30% smaller at equivalent quality, but not
	// every consumer plays it without extras.
	CodecHEVC Codec = 1
)

type Container

type Container int

Container selects the output file format.

const (
	// ContainerMP4 is the web-friendly default.
	ContainerMP4 Container = 0
	// ContainerMOV is Apple's native container; slightly better metadata
	// support and better for long captures that might be edited later.
	ContainerMOV Container = 1
)

type Display

type Display struct {
	ID     uint32
	Width  int
	Height int
}

Display describes an attached display.

func ListDisplays

func ListDisplays(ctx context.Context) ([]Display, error)

ListDisplays enumerates attached displays.

type Mic

type Mic struct {
	UniqueID  string // pass to WithMicDevice to force selection
	Name      string // user-visible name, e.g. "MacBook Pro Microphone"
	IsDefault bool
}

Mic describes an available audio input device.

func ListMics

func ListMics(ctx context.Context) ([]Mic, error)

ListMics enumerates built-in and external microphones visible to AVCaptureDeviceDiscoverySession.

type Option

type Option func(*config)

Option configures a recorder. Options follow the functional-options pattern and compose in left-to-right order (later overrides earlier).

func WithAudio

func WithAudio(enabled bool) Option

WithAudio toggles capture of system audio (application audio via SCStreamConfiguration.capturesAudio). Excludes the recording process's own output to prevent feedback loops.

Default: false.

In v0.1, WithAudio(true) and WithMic(true) are mutually exclusive. v0.2 will mix both into a single track.

func WithCodec

func WithCodec(codec Codec) Option

WithCodec selects the video codec. Default: CodecH264.

HEVC files are roughly 30% smaller at equivalent quality but require modern decoders (macOS 10.13+, Windows with the HEVC extension, modern browsers). For distribution to unknown audiences, stick with H.264.

func WithContainer

func WithContainer(container Container) Option

WithContainer selects the output file format. Default: ContainerMP4.

func WithCursorHighlight

func WithCursorHighlight(enabled bool) Option

WithCursorHighlight overlays a subtle ring around the cursor and a yellow expanding "ripple" on every left-button click, making mouse activity unmistakable in screen recordings. Useful for tutorials and UI demos.

Currently supports Display-target capture at the display's native resolution. Window / App / Region targets and --resolution scaling will misalign the overlay until coordinate mapping is generalized in a future release.

Does NOT require Accessibility permission: uses NSEvent.mouseLocation and CGEventSourceButtonState.

Default: false.

func WithDisplay

func WithDisplay(id uint32) Option

WithDisplay selects a specific display by CGDirectDisplayID. Zero (default) auto-picks the first attached display.

func WithFrameRate

func WithFrameRate(fps int) Option

WithFrameRate caps the output frame rate. Default: 60.

func WithMic

func WithMic(enabled bool) Option

WithMic toggles capture of the default microphone (or the device selected by WithMicDevice). Triggers a macOS Microphone permission prompt on first use.

Default: false.

func WithMicDevice

func WithMicDevice(uniqueID string) Option

WithMicDevice selects a specific microphone by its AVCaptureDevice uniqueID. Use ListMics to enumerate. When unset, the system's default input device is used.

func WithOutput

func WithOutput(path string) Option

WithOutput sets the destination path. Required — recorders without WithOutput fail to construct.

The file extension determines nothing by itself; use WithContainer to choose .mp4 vs .mov. WithOutput simply sets the filename.

func WithResolution

func WithResolution(w, h int) Option

WithResolution forces the output frame size in pixels. Zero values keep the target's native resolution. Typical use: downsample a 4K display to 1080p to save bitrate.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder is a live macOS screen + audio capture session that writes to a file. Create via NewRecorder; call Start then Stop.

func NewRecorder

func NewRecorder(ctx context.Context, opts ...Option) (*Recorder, error)

NewRecorder creates a configured recorder. Capture starts only when Start is called. The output file is created (or truncated) at this point, not at Start.

func (*Recorder) Start

func (r *Recorder) Start(ctx context.Context) error

Start begins capture. Blocks until the SCStream is delivering (typically ~150ms).

func (*Recorder) Stats

func (r *Recorder) Stats() Stats

Stats returns the current frame + audio buffer counts for progress UI. Safe to call from any goroutine.

func (*Recorder) Stop

func (r *Recorder) Stop(ctx context.Context) error

Stop flushes and finalizes the output file. Blocks until the mp4 is fully written (typically <500ms for short recordings, longer for multi-minute ones while the writer drains its AAC encoder).

type Stats

type Stats struct {
	Frames       int64
	AudioBuffers int64
}

Stats is live progress information about a running recorder.

Directories

Path Synopsis
cmd
kinrec command
kinrec — record your Mac screen (video + audio) to mp4 / mov, from the command line.
kinrec — record your Mac screen (video + audio) to mp4 / mov, from the command line.
internal
dylib
Package dylib embeds the ObjC companion library so downstream users can `go install github.com/LocalKinAI/kinrec/cmd/kinrec` without building C code locally.
Package dylib embeds the ObjC companion library so downstream users can `go install github.com/LocalKinAI/kinrec/cmd/kinrec` without building C code locally.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL