-
Notifications
You must be signed in to change notification settings - Fork 0
features overlays
Active contributors: elwina
Capto draws several kinds of overlays: things burned into the recording file and things shown live on screen during a take. Burned-in content is the webcam picture-in-picture, which the Rust compositor stitches into every frame before it reaches the encoder. Live, in-the-moment content is the click ripples and keystroke chips rendered by a transparent click-through window on top of the capture region; it is captured only if that area is part of the shot, it is never added to the file by FFmpeg. Text, image, and elapsed overlays exist in the config model and the layout preview so the shapes and defaults are settled, but the recording burn-in for them is not yet wired in.
The overlay model lives in crates/capto-overlay/src/lib.rs (OverlayConfig and its sub-structs), is embedded in the settings blob (crates/capto-core/src/settings.rs) and in each RecordRequest (crates/capto-core/src/ffmpeg_args.rs), and is edited from the UI panels apps/desktop/src/components/OverlayPanel.tsx, apps/desktop/src/components/OverlayPreview.tsx, and apps/desktop/src/components/WebcamPanel.tsx.
Two independent lanes: in-process compositing for burned-in content, and an input-hook event feed for live on-screen overlays.
graph TD
CFG[OverlayConfig - crates/capto-overlay/src/lib.rs]
SET[AppSettings.overlays - crates/capto-core/src/settings.rs]
REQ[RecordRequest.overlays - crates/capto-core/src/ffmpeg_args.rs]
BOOT[boot_pipeline - crates/capto-core/src/session.rs]
WC[webcam.rs take_webcam_for_record + WebcamFrameSlot]
PUMP[DxgiRecordPump - record_dxgi.rs]
COMP[composite.rs composite_webcam_pip]
FF[FFmpeg rawvideo stdin]
HOOK[capto-hooks WindowsInputHook<br/>WH_MOUSE_LL / WH_KEYBOARD_LL]
RT[record_overlay.rs pump_events + emit]
RUN[RecordOverlayRuntime.tsx - click ripples + key chips]
SET --> REQ
REQ --> BOOT
CFG --> SET
BOOT --> WC
WC --> PUMP
PUMP --> COMP
REQ -->|"webcam WebcamPip"| COMP
COMP --> FF
HOOK --> RT
RT -->|"overlay://click / overlay://key"| RUN
The compositor path ends in burned-in pixels. The hook path ends in a live window that is visible beside the capture for the benefit of the camera person and the audience only if that region is recorded.
When webcam.enabled is set, boot_pipeline in crates/capto-core/src/session.rs warms the Media Foundation camera with take_webcam_for_record (before the encode clock starts) so PiP frames exist from frame 0. The DxgiRecordPump then calls composite_webcam_pip in crates/capto-capture/src/composite.rs, which blits the camera frame onto each capture frame per the WebcamPip layout (position, size, corner radius, mirror). Because this happens in-process before FFmpeg, FFmpeg never opens dshow. See Webcam PiP.
apps/desktop/src-tauri/src/record_overlay.rs opens one transparent, always-on-top, click-through webview window sized to the capture bounds (OverlayBounds::from_region_or_screen). A low-level mouse hook (WH_MOUSE_LL in crates/capto-hooks/src/lib.rs) forwards left/right/middle button-downs that land inside those bounds. pump_events builds a ClickPayload (button, position, the configured color per button, radius, id) and emits overlay://click. apps/desktop/src/components/RecordOverlayRuntime.tsx listens and renders a ripple that fades after roughly 550 ms.
The same window receives key events from the keyboard hook (WH_KEYBOARD_LL). crates/capto-hooks/src/lib.rs dedupes OS auto-repeat (only the first KEYDOWN per held key), builds a display label such as Ctrl+C, and emits overlay://key. RecordOverlayRuntime.tsx renders a chip (font size, foreground and background colors from the config) and replaces chips with the same label instead of stacking repeats, expiring each after about 1.8 s.
OverlayConfig.texts and OverlayConfig.images (crates/capto-overlay/src/lib.rs) define text and logo boxes with an id, position, font/color or size/opacity, and an enabled flag. apps/desktop/src/components/OverlayPreview.tsx renders them in a mock frame so the layout is visible in the settings UI. Today the recording pipeline does not burn them in: build_record_args in crates/capto-core/src/ffmpeg_args.rs only emits scale and (for GIF) palette filters, no drawtext= or overlay= filtergraphs. escape_filter_path in crates/capto-overlay/src/lib.rs exists to make such filters safe (escape \, :, and ') when that path lands.
OverlayConfig.elapsed is marked deprecated in crates/capto-overlay/src/lib.rs and is kept only for settings-JSON compatibility. It is never burned into recordings. The live elapsed readout during a take is instead drawn by the in-window recording control overlay discussed below.
hide_app_while_recording (edited in apps/desktop/src/App.tsx) hides the main window at record start and restores it at stop (apps/desktop/src-tauri/src/session_svc.rs). The live screen preview blacks out the Capto window rectangle via Frame::blackout_rect in apps/desktop/src-tauri/src/lib.rs (capture_preview), so the UI never paints a video of itself.
Window picker and region selector both use the same helper open_overlay_windows in apps/desktop/src-tauri/src/lib.rs. It opens one transparent webview window per physical monitor (a single window spanning the virtual desktop breaks on secondary or mixed-DPI screens). Each window is labelled {kind}-{gen}-{i} where kind is picker or region-picker, gen is a value from the process-global OVERLAY_GENERATION counter bumped on every open (so labels stay unique after close — Windows refuses to reuse a recycled label), and i is the monitor index. The overlay under the cursor is focused. close_all_selection_overlays closes both kinds and then retries in a loop (up to 25 iterations of 20 ms) until the labels are actually gone, because window destroy is asynchronous on Windows and leftover always-on-top pickers were blocking the next pick. Region selection results arrive on picker://region-selected and window results on picker://window-selected (apps/desktop/src/App.tsx).
RecordOverlayController (apps/desktop/src-tauri/src/record_overlay.rs) also drives the elapsed/state display during a take. It is started by apps/desktop/src-tauri/src/session_svc.rs after a successful record start, paused with pause_recording, resumed with resume_recording, and closed with stop_recording. Emits are directed at the single record-overlay webview window to avoid double-firing from broadcasts.
The overlay settings are persisted as OverlayConfig and edited with dotted-path patches from the React UI (apps/desktop/src/overlays.ts mirrors the shape with optional fields). Key defaults:
| Field | Default | Meaning |
|---|---|---|
mouseClicks.enabled |
true | Show live click ripples |
mouseClicks.leftColor / rightColor / middleColor
|
#FF5252 / #448AFF / #69F0AE
|
Ripple color per button |
mouseClicks.radius |
18 | Ripple radius in pixels |
keystrokes.enabled |
true | Show live keystroke chips |
keystrokes.position |
bottom-left, x 0.05, y 0.9 | Chip anchor and fine-tune |
keystrokes.fontSize / color / background
|
28 / #FFFFFF / #000000AA
|
Chip typography |
texts / images
|
empty | Text and image layout boxes (config + preview only today) |
elapsed |
disabled | Deprecated, never burned in |
webcam |
disabled, 320x240, mirrored, corner 12 | Webcam PiP, see Webcam PiP |
resolve_pixel_position and OverlayAnchor in crates/capto-overlay/src/lib.rs give every anchored box a normalized 0..1 adjustment; the anchored positions get a ±40 px fine-tune ((pos.x - 0.5) * 40.0) at composite time.
-
apps/desktop/src-tauri/src/lib.rsowns the picker overlays (open_window_picker,open_region_picker,open_overlay_windows,close_all_selection_overlays,OVERLAY_GENERATION) and returnscapto_overlay::OverlayConfigdefaults viaget_overlay_defaults. -
apps/desktop/src-tauri/src/session_svc.rswiresRecordOverlayControllerinto the record lifecycle (start/pause/resume/stop) and hides/shows the main window for hide-app. -
crates/capto-core/src/session.rsembedsOverlayConfigin every recording throughRecordRequest.overlaysand readswebcamfor the PiP duringboot_pipeline. - The input hooks come from
crates/capto-hooks/src/lib.rs(create_input_hook,InputHook,InputEvent); see capto-hooks.
- Change click/key overlay rendering:
apps/desktop/src-tauri/src/record_overlay.rs(event filtering, payloads) andapps/desktop/src/components/RecordOverlayRuntime.tsx(CSS/chips). - Change overlay geometry math:
resolve_pixel_positionandOverlayAnchorincrates/capto-overlay/src/lib.rs. - Add text/image burn-in: build the
drawtext=/overlay=filtergraph inbuild_record_argsincrates/capto-core/src/ffmpeg_args.rs, reusingescape_filter_path, and drive it fromRecordRequest.overlays. - Tune picker window behavior:
open_overlay_windows,OVERLAY_GENERATION, andclose_all_selection_overlaysinapps/desktop/src-tauri/src/lib.rs.
| File | What to look for |
|---|---|
crates/capto-overlay/src/lib.rs |
OverlayConfig, OverlayPosition, WebcamPip, resolve_pixel_position, escape_filter_path
|
crates/capto-core/src/settings.rs |
Where OverlayConfig is embedded in AppSettings.overlays
|
crates/capto-core/src/ffmpeg_args.rs |
RecordRequest.overlays, build_record_args (no drawtext today) |
crates/capto-core/src/session.rs |
boot_pipeline webcam PiP wiring |
crates/capto-capture/src/composite.rs |
composite_webcam_pip burned-in PiP blit |
apps/desktop/src-tauri/src/record_overlay.rs |
RecordOverlayController, click/key pumps, overlay window |
apps/desktop/src-tauri/src/lib.rs |
Picker overlays, OVERLAY_GENERATION, close_all_selection_overlays
|
apps/desktop/src-tauri/src/session_svc.rs |
Record lifecycle overlay + hide-app integration |
apps/desktop/src/components/OverlayPanel.tsx |
Overlay settings panel |
apps/desktop/src/components/OverlayPreview.tsx |
Live mock layout preview |
apps/desktop/src/components/RecordOverlayRuntime.tsx |
Click ripple + keystroke chip rendering |
apps/desktop/src/overlays.ts |
Frontend overlay-layout types |