A Go package for screen capture with a unified io.Reader interface that yields raw BGRA frames.
Supports Linux (xdg-desktop-portal + PipeWire), macOS (ScreenCaptureKit), and Windows (Windows Graphics Capture).
- Cross-Platform: Native OS dialogs or direct capture APIs on Linux, macOS 12.3+ (audio on 13+), and Windows 10+.
- High-Performance Linux Backend: Uses
cgoandlibpipewire-0.3to access PipeWire streams with low-overhead buffering for real-time capture. - Unified
io.ReaderInterface: Pipe rawBGRAframes directly intoffmpegor any standard Go stream. - Audio Capture: Optional system audio capture (48kHz, 16-bit, stereo) on supported platforms.
- Graceful Fallback: Dynamically loads the PipeWire C library at runtime (
dlopen). - Robust Lifecycle Handling: Idempotent close paths, auto-cleanup when ffmpeg exits, and first-frame startup timeouts on desktop backends.
- xdg-desktop-portal
- PipeWire running
- DBus session bus
libpipewire-0.3-dev(Only required at build time for C headers)
- macOS 12.3 or later
- CGO enabled
- System audio capture requires macOS 13.0 or later
- Windows 10 (1809) or later
- CGO enabled with a C/C++ compiler (e.g., MSYS2/MinGW-w64)
- Uses Windows Graphics Capture (WinRT + D3D11). Windows 8.1 and older are not supported and may fail at runtime even if the application starts.
# Ubuntu / Debian build requirements (Linux only)
sudo apt install libpipewire-0.3-dev pkg-config
# Get the Go package
go get go2tv.app/screencastThis minimal example shows how to open a capture stream and pipe raw BGRA frames directly to FFmpeg.
package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"go2tv.app/screencast/capture"
)
func main() {
// Default open enables audio and captures StreamIndex 0.
stream, err := capture.Open(nil)
if err != nil {
log.Fatalf("Failed to open capture stream: %v", err)
}
defer stream.Close()
if stream.Audio != nil {
fmt.Println("Audio capture enabled (draining in background)")
go func() {
_, _ = io.Copy(io.Discard, stream.Audio)
}()
}
frameRate := stream.FrameRate
if frameRate == 0 {
frameRate = 60
}
width, height := stream.Width, stream.Height
// Pipe the io.Reader directly into FFmpeg
cmd := exec.Command("ffmpeg",
"-y", "-re",
"-f", "rawvideo",
"-pix_fmt", "bgra",
"-s", fmt.Sprintf("%dx%d", width, height),
"-r", strconv.FormatUint(uint64(frameRate), 10),
"-i", "pipe:0", // Read from stdin
"-c:v", "libx264",
"-preset", "ultrafast",
"-pix_fmt", "yuv420p",
"output.mp4",
)
cmd.Stdin = stream
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Println("Recording... Press Ctrl+C to stop.")
if err := cmd.Run(); err != nil {
log.Fatalf("FFmpeg exited with error: %v", err)
}
}For app-driven selection, enumerate displays first and pass the chosen index to capture.Open.
displays, err := capture.ListDisplays()
if err != nil {
// On unsupported platforms (e.g. Linux), this returns capture.ErrNotImplemented.
}
for _, d := range displays {
fmt.Printf("[%d] %s (%dx%d) primary=%t\n", d.Index, d.Name, d.Width, d.Height, d.Primary)
}
stream, err := capture.Open(&capture.Options{
StreamIndex: 1, // user-selected display index
IncludeAudio: true,
})The same index can be forwarded to either output format:
session, err := hls.Start(&hls.Options{
FFmpegPath: "ffmpeg",
IncludeAudio: true,
StreamIndex: 1, // same display index selection
})Both formats sit on the same capture, encoder selection and A/V sync machinery, and differ only in how the encoded result is handed back.
hls.Startwrites a segmented playlist into a temp directory, for serving over HTTP. UseSession.Dir()withhls.NewDirectoryHandler.ts.Startmuxes one continuous MPEG-TS stream and hands it back as a singleio.ReadCloser. Nothing in it speaks a transport protocol, so it suits a DLNA renderer, a Chromecast, a raw socket or a local player.
session, err := ts.Start(&ts.Options{
FFmpegPath: "ffmpeg",
IncludeAudio: true,
})
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Closing the stream is a no-op: the session owns the ffmpeg process, so hand
// it to as many consumers as the transport needs without racing the encoder.
_, _ = io.Copy(w, session.Stream())The stream is muxed in m2ts mode (192-byte packets with a 4-byte timestamp
prefix), which is what the DLNA AVC_TS_MP_HD_AAC_MULT5 profile and the
video/vnd.dlna.mpeg-tts media type require. That profile promises an AAC
track, so only advertise it with IncludeAudio set.
ts.Start does not return until it has read enough of ffmpeg's output to prove
it is really a well-formed m2ts stream - a PAT and the PMT it points at - so a
renderer is only told to play once there is something valid to play. Those
startup bytes are handed on to the first reads rather than discarded.
go run examples/capture/main.goSCREENCAST_STREAM_INDEX=1 go run examples/capture/main.gogo run examples/hls/main.goSCREENCAST_STREAM_INDEX=1 go run examples/hls/main.goHLS example output is served at http://127.0.0.1:8080/playlist.m3u8 by default.
Optional environment variables:
SCREENCAST_FFMPEG=/path/to/ffmpeg(default:ffmpeg)SCREENCAST_HLS_PORT=8080SCREENCAST_HLS_AUDIO=1(set0to disable audio)SCREENCAST_STREAM_INDEX=0(set monitor/display index for both examples)- Video encoder is auto-selected per host by probing the configured ffmpeg binary and running a short real encode test on hardware candidates (
h264_nvenc,h264_amf,h264_qsv,h264_vaapi,h264_videotoolbox). If none pass, it falls back tolibx264.
- Linux: Implemented (
xdg-desktop-portal+ PipeWire) - macOS: Implemented (ScreenCaptureKit)
- Windows: Implemented (Windows Graphics Capture)
- Limitation: requires Windows 10 version 1809 or newer.
Set one environment variable to enable debug logging across capture, the encode pipeline, and ffmpeg paths:
SCREENCAST_DEBUG=1Optional: write debug logs to a file (recommended for GUI apps on Windows/macOS):
SCREENCAST_DEBUG=1 SCREENCAST_DEBUG_FILE=/tmp/screencast-debug.logWhat SCREENCAST_DEBUG=1 enables:
- Capture lifecycle logs on all platforms (Linux/macOS/Windows), including first-frame timing.
- Windows/macOS async callback queue diagnostics, including slow pipe writes and dropped video frames under pressure.
- PipeWire internal stream debug logs (Linux backend).
- ffmpeg command printing and ffmpeg stderr capture in
hls.Startandts.Start. - ffmpeg
-loglevel debuginhls.Startandts.Start. - Shared pipeline diagnostics (encoder probe, frame pacer, audio pre-roll), logged under
screencast/pipeline. - HLS HTTP directory handler debug logs in
hls.NewDirectoryHandler.
What SCREENCAST_DEBUG_FILE=/path/to/log does:
- Routes capture debug logs to the file on all platforms (Linux/macOS/Windows).
- Routes HLS, MPEG-TS and shared pipeline debug logs to the file on all platforms (Linux/macOS/Windows).
- Routes PipeWire debug logs to the same file on Linux.
- If your app provides custom log handlers/writers, debug output is still mirrored to this file.
Legacy variables still supported:
SCREENCAST_CAPTURE_DEBUG=1SCREENCAST_CAPTURE_DEBUG_FILE=/path/to/fileSCREENCAST_PIPEWIRE_DEBUG=1SCREENCAST_PIPEWIRE_DEBUG_FILE=/path/to/file
These apply to both hls.Session and ts.Session, which share the pipeline underneath.
- Session cleanup is idempotent (
Close()can be called multiple times safely). - If ffmpeg exits unexpectedly, session resources are now auto-cleaned.
- When audio is requested but platform capture audio is unavailable, the pipeline injects paced synthetic silence so the audio track remains present.
- ffmpeg derives timestamps for both raw inputs from byte/frame counts, so both are anchored to the wall clock: video is frame-paced and audio is trimmed/padded to match elapsed real time. Audio captured before ffmpeg attaches is discarded as pre-roll (
SCREENCAST_DEBUG=1reportsaudio_preroll_dropped). - If ffmpeg falls more than a couple of seconds behind, the pacer gives up on the frames it can never emit and the audio relay drops the same span, so the two stay in sync.
Session.DroppedFrames()reports the running total: a count that keeps climbing means the capture is too big or too fast for the machine, and the caller should lower the frame rate or resolution. - Default startup timeout is 60s to reduce transient initialization failures under load.
- Desktop backends use a first-frame timeout to avoid indefinite startup hangs.
- For diagnostics after failure, use
Session.StderrTail(n).
- go2tv - Cast videos, music, and images from Linux to your TV.
- go2tv - Cast videos, music, and images from Linux to your TV.
- mcp-beam - MCP server (stdio transport) for casting local files and media URLs to Chromecast and DLNA/UPnP devices on your LAN.
MIT