1. Architecture Overview
OpenScreen is a desktop screen recording and video editing application built on Electron with a React/TypeScript renderer. The application follows a two-process architecture: the Electron main process manages windows, system tray, file I/O, and OS permissions, while the renderer process handles the UI, recording logic, and video export pipeline.
Architecture Diagram
┌──────────────────────────────────────────────────────┐
│ Electron Main Process │
│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ main.ts │ │windows.ts│ │ ipc/handlers.ts │ │
│ │ (app │ │ (window │ │ (file I/O, source │ │
│ │ entry, │ │ factory │ │ selection, cursor │ │
│ │ tray, │ │ + HUD, │ │ telemetry, project│ │
│ │ menus) │ │ editor, │ │ persistence) │ │
│ │ │ │ picker) │ │ │ │
│ └──────────┘ └──────────┘ └────────────────────┘ │
│ │ │ │ │
│ └──────────────┴───────────────┘ │
│ │ IPC (contextBridge) │
│ ┌──────┴──────┐ │
│ │ preload.ts │ │
│ └──────┬──────┘ │
└──────────────────────┼───────────────────────────────┘
│
┌──────────────────────┼───────────────────────────────┐
│ Renderer Process (Vite + React) │
│ ┌─────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ App.tsx │ │ LaunchWindow │ │ VideoEditor │ │
│ │ (router) │ │ (HUD overlay)│ │ (full editor) │ │
│ └─────────┘ └──────────────┘ └───────┬────────┘ │
│ │ │
│ ┌──────────────────────────────────────┤ │
│ │ hooks/ contexts/ lib/ │ │
│ │ useScreen I18nContext shortcuts │
│ │ Recorder Shortcuts userPrefs │
│ │ useCamera Context customFonts │
│ │ Devices compositeLayout │
│ │ useEditor recordingSession │
│ │ History │
│ └──────────────────────────────────────┤ │
│ │ │
│ ┌──────────────────────────────────────┘ │
│ │ lib/exporter/ │
│ │ VideoExporter → FrameRenderer → VideoMuxer │
│ │ GifExporter │
│ │ StreamingVideoDecoder / VideoFileDecoder │
│ │ AudioProcessor │
│ └───────────────────────────────────────────────────│
└──────────────────────────────────────────────────────┘
Component Inventory
| Component | Path | Role |
|---|---|---|
| Electron Main | electron/main.ts |
App lifecycle, tray, menus, window management |
| IPC Handlers | electron/ipc/handlers.ts |
All main↔renderer communication (28+ handlers) |
| Window Factory | electron/windows.ts |
HUD overlay, editor, source selector window creation |
| Preload Bridge | electron/preload.ts |
contextBridge.exposeInMainWorld("electronAPI", ...) |
| Main i18n | electron/i18n.ts |
Main-process translation (menus, dialogs) |
| App Router | src/App.tsx |
Routes by windowType query param |
| LaunchWindow | src/components/launch/LaunchWindow.tsx |
HUD overlay recording controls |
| SourceSelector | src/components/launch/SourceSelector.tsx |
Screen/window source picker |
| VideoEditor | src/components/video-editor/VideoEditor.tsx |
Full editing interface |
| Timeline | src/components/video-editor/timeline/ |
Timeline rendering and interaction |
| Export Pipeline | src/lib/exporter/ |
Video/GIF encoding and muxing |
| Recording Hook | src/hooks/useScreenRecorder.ts |
MediaRecorder management |
| Editor History | src/hooks/useEditorHistory.ts |
Undo/redo state machine |
| i18n System | src/i18n/ |
Translation loading and interpolation |
| UI Components | src/components/ui/ |
shadcn/ui component library (Radix-based) |
Technology Stack
| Layer | Technology |
|---|---|
| Runtime | Electron 39 (Chromium) |
| Frontend | React 18, TypeScript 5.2 |
| Build | Vite 5 + vite-plugin-electron |
| Styling | Tailwind CSS 3.4, CSS Modules |
| UI Library | shadcn/ui (Radix UI primitives, Lucide icons) |
| Video Processing | WebCodecs, MediaRecorder, PixiJS 8, mp4box |
| Animation | GSAP 3.13, Motion (Framer Motion) |
| GIF | gif.js |
| Audio | Web Audio API |
| WebM Fix | fix-webm-duration, @fix-webm-duration/fix |
| Timeline | dnd-timeline |
| Testing | Vitest, Playwright, Testing Library |
| Linting | Biome 2.3 |
| Package | electron-builder |
Note
The renderer-side video pipeline uses PixiJS for GPU-accelerated frame compositing with filters (drop shadow, blur). The
web-demuxerWASM module handles container demuxing in the browser context.
Data Flow: Recording to Export
1. User selects source → desktopCapturer.getSources() → thumbnail previews
2. Start recording → getUserMedia(chromeMediaSource: desktop) → MediaStream
3. Optional: microphone stream → Web Audio API mixer → combined stream
4. Optional: webcam stream → separate MediaRecorder
5. MediaRecorder(stream, {codec, bitrate}) → Blob chunks → fixWebmDuration
6. Stop → storeRecordedSession IPC → main writes files to RECORDINGS_DIR
7. Cursor telemetry: setInterval(100ms) → screen.getCursorScreenPoint()
8. Switch to editor → load video in VideoPlayback component
9. User edits: zoom/trim/speed/annotation regions on timeline
10. Export:
a. StreamingVideoDecoder demuxes source → WebCodecs VideoDecoder
b. FrameRenderer composites each frame (screen + webcam + annotations + effects)
c. VideoMuxer encodes frames → mp4box → MP4 output
d. OR GifExporter encodes frames → gif.js → GIF output
11. save-exported-video IPC → main shows save dialog → writes file
2. System Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| Node.js | 22.22.1 | 22.22.1 (exact match) |
| npm | 10.9.4 | 10.9.4 (exact match) |
| OS | macOS 12+, Windows 10+, Linux (X11) | macOS 14+, Windows 11 |
| RAM | 4 GB | 8+ GB (video export is memory-intensive) |
| GPU | Any (software rendering supported) | Hardware-accelerated GPU for PixiJS compositing |
| Disk | 500 MB for install | 10+ GB for recordings (4K@60fps ≈ 45 Mbps) |
macOS-Specific Requirements
- Screen Recording permission in System Settings → Privacy & Security
- Microphone permission for audio recording
- Camera permission for webcam overlay
- Code signing identity and notary profile for distribution builds (see
scripts/build_macos.sh) - macOS entitlements file:
macos.entitlements
Important
On macOS, OpenScreen disables
MacCatapLoopbackAudioForScreenShareviaapp.commandLine.appendSwitchto use the standard Screen & System Audio Recording permissions instead of the CoreAudio Tap API. This is intentional — CoreAudio Tap requiresNSAudioCaptureUsageDescriptionin the parent app's Info.plist, which breaks terminal/IDE development workflows.
3. Configuration Reference
Environment Variables
| Variable | Context | Default | Description |
|---|---|---|---|
NODE_ENV |
Build | — | Set to "test" to disable vite-plugin-electron-renderer |
APP_ROOT |
Runtime | Computed | Set by main process: path.join(__dirname, "..") |
VITE_DEV_SERVER_URL |
Runtime | — | Set automatically by Vite dev server |
VITE_PUBLIC |
Runtime | Computed | Points to public/ in dev or dist/ in production |
HEADLESS |
Runtime | — | Set to "true" to create windows without showing them (testing) |
Build Configuration Files
| File | Format | Purpose |
|---|---|---|
vite.config.ts |
TypeScript | Vite build config with electron plugin, path aliases, chunk splitting |
tsconfig.json |
JSON | TypeScript compiler options (strict, ESNext, bundler resolution) |
tsconfig.node.json |
JSON | TypeScript config for Node-side files (vite.config.ts) |
biome.json |
JSON | Linter and formatter configuration |
components.json |
JSON | shadcn/ui configuration (new-york style, stone base color) |
tailwind.config.cjs |
CJS | Tailwind CSS configuration |
postcss.config.cjs |
CJS | PostCSS with Tailwind and Autoprefixer |
electron-builder.json5 |
JSON5 | Electron Builder packaging configuration |
playwright.config.ts |
TypeScript | E2E test config (120s timeout, tests/e2e/ dir) |
vitest.config.ts |
TypeScript | Unit test config (jsdom environment) |
vitest.browser.config.ts |
TypeScript | Browser test config (Playwright Chromium with SwiftShader) |
.nvmrc |
Text | Node.js version pinning |
.editorconfig |
INI | Editor formatting preferences |
.env.example |
Text | Template for environment variables (macOS build signing) |
Vite Build Configuration
The Vite config (vite.config.ts) defines:
- Plugins:
@vitejs/plugin-react,vite-plugin-electron/simple(main entry:electron/main.ts, preload:electron/preload.ts) - Path alias:
@→src/ - Build target:
esnext - Minification: Terser with
drop_console,drop_debugger, pure function removal - Manual chunks:
pixi—pixi.jsreact-vendor—react,react-domvideo-processing—mediabunny,mp4box,@fix-webm-duration/fix
- Chunk size warning limit: 1000 KB
Tip
The manual chunk configuration ensures PixiJS (large) and video processing libraries are loaded separately from the main React bundle, improving initial load time.
Biome Linter Configuration
Biome is configured with strict rules in biome.json:
- Formatter: Tab indentation, double quotes, LF line endings, 100-char line width
- Linter: No
recommendedpreset — rules are individually enabled acrosscomplexity,correctness,style, andsuspiciouscategories - TypeScript overrides: Several
correctnessrules disabled for.ts/.tsx(handled by the TS compiler) - Assist: Auto-organize imports enabled
Electron Builder Configuration
Distribution builds are configured in electron-builder.json5. The macOS build script (scripts/build_macos.sh) extends this with:
- Dual-architecture builds: arm64 and x64
- Code signing via Apple Developer identity
- Notarization via
xcrun notarytool - Entitlements from
macos.entitlements - DMG output to
release/<version>/
i18n Configuration
Defined in src/i18n/config.ts:
DEFAULT_LOCALE = "en"
SUPPORTED_LOCALES = ["en", "zh-CN", "es", "fr", "tr", "ko-KR"]
I18N_NAMESPACES = ["common", "dialogs", "editor", "launch", "settings", "shortcuts", "timeline"]
LOCALE_STORAGE_KEY = "openscreen-locale"
Translation files live at src/i18n/locales/<locale>/<namespace>.json. The loader uses import.meta.glob for eager loading at build time.
4. Deployment & Scaling
Development Setup
git clone https://github.com/siddharthvaddem/openscreen.git
cd openscreen
nvm use # switches to Node 22.22.1 via .nvmrc
npm install
npm run dev # starts Vite dev server + Electron
Production Build
The build pipeline compiles TypeScript, bundles with Vite, and packages with electron-builder:
npm run build # all platforms
npm run build:mac # macOS DMG
npm run build:win # Windows installer
npm run build:linux # Linux AppImage + deb
Build artifacts structure:
├── dist/ # Vite renderer output
│ └── index.html
├── dist-electron/ # Compiled main process
│ ├── main.js
│ └── preload.mjs
└── release/<version>/ # Packaged distributables
macOS Distribution
The scripts/build_macos.sh script automates the full macOS release process:
- Validates prerequisites (Node.js, npm, signing identity, notary profile, entitlements)
- Cleans previous build artifacts
- Runs TypeScript compilation and Vite build
- Builds for both arm64 and x64 architectures
- Signs the
.appbundle with hardened runtime - Notarizes via Apple's notary service
- Staples the notarization ticket
- Produces DMG files in
release/<version>/
Required .env variables for macOS builds:
| Variable | Description |
|---|---|
APP_NAME |
Application display name |
SIGN_IDENTITY |
Apple Developer signing identity |
NOTARY_PROFILE |
Keychain profile for notarization |
APPLE_ID |
Apple Developer account email |
TEAM_ID |
Apple Developer Team ID |
Caution
The macOS build script requires a valid Apple Developer account with code signing certificates installed. Running without proper credentials will fail at the signing step.
5. Operations
Window Management
OpenScreen uses three window types, each created by factory functions in electron/windows.ts:
| Window | Size | Properties | Purpose |
|---|---|---|---|
| HUD Overlay | 600×160 (fixed) | Frameless, transparent, always-on-top, skip taskbar | Recording controls overlay |
| Editor | 1200×800 (starts maximized) | Hidden title bar on macOS, resizable | Video editing interface |
| Source Selector | 620×420 | Frameless, transparent, always-on-top, centered | Screen/window picker |
All windows use contextIsolation: true and nodeIntegration: false. The editor window additionally sets webSecurity: false to allow loading local video files via file:// URLs.
Note
On macOS, HUD and source selector windows use
setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })so they follow the user across Spaces (virtual desktops).
System Tray
The tray icon changes based on recording state:
- Idle: Default OpenScreen icon with context menu: Open, Quit
- Recording: Red recording icon with tooltip showing source name, context menu: Stop Recording
File System Layout
Runtime directories (in Electron userData):
<userData>/
├── recordings/ # Video recordings
│ ├── recording-<timestamp>.webm # Screen recording
│ ├── recording-<timestamp>-webcam.webm # Webcam recording (optional)
│ ├── recording-<timestamp>.session.json # Session manifest
│ ├── recording-<timestamp>.webm.cursor.json # Cursor telemetry
│ └── *.openscreen # Project files
└── shortcuts.json # Custom keyboard shortcuts
Recording Session Management
Each recording produces a session manifest (*.session.json) containing:
{
"screenVideoPath": "/path/to/recording-1234.webm",
"webcamVideoPath": "/path/to/recording-1234-webcam.webm",
"createdAt": 1234567890123
}
The IPC handler maintains state for the current recording session and current project path. Session manifests enable the editor to locate both screen and webcam files from a single reference.
Cursor Telemetry
During recording, cursor position is sampled at 10Hz (100ms intervals) via screen.getCursorScreenPoint(). Samples are normalized to the source display's bounds as (cx, cy) fractions in [0, 1].
| Setting | Value |
|---|---|
| Sample interval | 100ms |
| Max samples | 36,000 (1 hour at 10Hz) |
| Storage format | JSON: { version: 1, samples: [{timeMs, cx, cy}, ...] } |
| File naming | <video-path>.cursor.json |
Tip
Cursor telemetry enables cursor-following zoom effects. When multiple displays are connected, the cursor position is normalized to the source display's bounds, not the global screen coordinate space.
6. Performance
Recording Bitrate Scaling
| Resolution | Base Bitrate | With 60fps Boost (1.7×) |
|---|---|---|
| 4K (3840×2160+) | 45 Mbps | 76.5 Mbps |
| QHD (2560×1440+) | 28 Mbps | 47.6 Mbps |
| Below QHD | 18 Mbps | 30.6 Mbps |
Audio bitrate: 192 kbps (system audio) or 128 kbps (microphone only).
Codec Selection Priority
1. video/webm;codecs=av1 (best compression)
2. video/webm;codecs=h264 (wide compatibility)
3. video/webm;codecs=vp9 (good compression)
4. video/webm;codecs=vp8 (legacy fallback)
5. video/webm (bare minimum)
Export Pipeline Performance
The export pipeline processes each frame sequentially:
- Decode:
StreamingVideoDecoderdemuxes viaweb-demuxerWASM and decodes via WebCodecsVideoDecoder - Render:
FrameRenderercomposites screen + webcam + annotations + effects using Canvas 2D (with PixiJS for filters) - Encode: WebCodecs
VideoEncoder→mp4boxmuxer
For GIF export, gif.js handles frame quantization and LZW encoding.
Note
The
AsyncVideoFrameQueuemanages backpressure between the decoder and renderer with a configurable queue depth, preventing memory exhaustion on long videos.
Build Optimization
- Terser: Production builds drop
console.log,console.debug, anddebuggerstatements - Manual chunks: PixiJS, React, and video processing libraries are split into separate bundles
- Background throttling: Disabled (
backgroundThrottling: false) for both HUD and editor windows to ensure recording and playback aren't paused when unfocused
7. Security
IPC Security Model
OpenScreen follows Electron's security best practices:
- Context isolation:
contextIsolation: trueon all windows - No node integration:
nodeIntegration: falseon all windows - Preload bridge: All renderer↔main communication goes through
contextBridge.exposeInMainWorld
File System Access Control
The IPC handler implements a path allowlist for file reads:
- Default allowed directory:
RECORDINGS_DIR(ElectronuserData/recordings/) - Runtime-approved paths: Added when the user explicitly selects files via OS file picker dialogs
- Project-scoped approval: When loading a project, only paths within the project's directory or
RECORDINGS_DIRare auto-approved
Warning
The
read-binary-fileIPC handler validates paths against the allowlist before reading. Paths outside allowed directories are rejected with "Access denied: path outside allowed directories". This prevents malicious project files from reading arbitrary filesystem locations.
Recording File Name Validation
The resolveRecordingOutputPath() function enforces:
- File names cannot be empty
- Path traversal segments (
..) are rejected - Absolute paths are rejected
- Directory separators (
/,\) are rejected - Output is always within
RECORDINGS_DIR
Project File Trust Model
When loading .openscreen project files:
- Media paths are validated against trusted directories (project's own directory +
RECORDINGS_DIR) - Only files with allowed video extensions (
.webm,.mp4,.mov,.avi,.mkv) are approved for read access file://URLs are normalized to filesystem paths before validation- The
currentProjectPathis used to determine if "Save" should overwrite without prompting
Permission Handling
| Permission | Platform | Handling |
|---|---|---|
| Screen recording | All | Via desktopCapturer (Chromium-managed) |
| Microphone | All | navigator.mediaDevices.getUserMedia + macOS systemPreferences.askForMediaAccess |
| Camera | macOS | systemPreferences.getMediaAccessStatus("camera") + askForMediaAccess("camera") |
| Camera | Other | Browser-level getUserMedia (auto-granted in Electron) |
Caution
The editor window sets
webSecurity: falseto allow loading local video files viafile://URLs. This disables same-origin policy for the editor renderer process. This is necessary for the video playback pipeline but means the editor window should not load untrusted remote content.
Unsaved Changes Protection
The editor tracks unsaved changes via editorHasUnsavedChanges flag. On close:
- If no unsaved changes → window closes immediately
- If unsaved changes → native dialog with three options: Save & Close, Discard & Close, Cancel
- Save & Close sends
request-save-before-closeto renderer, waits forsave-before-close-doneconfirmation before closing
8. Developer Internals
Repository Structure
openscreen/
├── electron/ # Electron main process
│ ├── main.ts # App entry, tray, menus, window lifecycle
│ ├── preload.ts # contextBridge API surface
│ ├── windows.ts # Window factory (HUD, editor, source selector)
│ ├── i18n.ts # Main-process translations
│ └── ipc/
│ └── handlers.ts # All IPC handler registrations (28+)
├── src/ # Renderer process (React)
│ ├── main.tsx # React entry point
│ ├── App.tsx # Window type routing
│ ├── components/
│ │ ├── launch/ # HUD overlay + source picker
│ │ ├── ui/ # shadcn/ui components (19 components)
│ │ └── video-editor/ # Core editor (20 files, 277K chars)
│ │ ├── VideoEditor.tsx # Main editor component
│ │ ├── VideoPlayback.tsx # Video preview + webcam composite
│ │ ├── PlaybackControls.tsx
│ │ ├── SettingsPanel.tsx # Wallpaper, padding, aspect ratio, webcam
│ │ ├── AnnotationOverlay.tsx
│ │ ├── AnnotationSettingsPanel.tsx
│ │ ├── BlurSettingsPanel.tsx
│ │ ├── CropControl.tsx
│ │ ├── ExportDialog.tsx # Format, quality, GIF options
│ │ ├── FormatSelector.tsx
│ │ ├── GifOptionsPanel.tsx
│ │ ├── KeyboardShortcutsHelp.tsx
│ │ ├── ShortcutsConfigDialog.tsx
│ │ ├── TutorialHelp.tsx
│ │ ├── AddCustomFontDialog.tsx
│ │ ├── ArrowSvgs.tsx # Arrow annotation SVG components
│ │ ├── projectPersistence.ts # Save/load project state
│ │ ├── types.ts # Editor type definitions
│ │ ├── timeline/ # Timeline components (7 files)
│ │ └── videoPlayback/ # Playback utilities (9 files)
│ ├── contexts/
│ │ ├── I18nContext.tsx # Translation provider
│ │ └── ShortcutsContext.tsx # Keyboard shortcut provider
│ ├── hooks/
│ │ ├── useScreenRecorder.ts # MediaRecorder lifecycle (1250 lines)
│ │ ├── useEditorHistory.ts # Undo/redo state machine
│ │ ├── useCameraDevices.ts # Camera enumeration
│ │ ├── useMicrophoneDevices.ts # Microphone enumeration
│ │ └── useAudioLevelMeter.ts # Real-time audio level visualization
│ ├── lib/
│ │ ├── compositeLayout.ts # Webcam overlay layout computation
│ │ ├── customFonts.ts # Google Fonts management
│ │ ├── shortcuts.ts # Shortcut binding system
│ │ ├── userPreferences.ts # localStorage preferences
│ │ ├── recordingSession.ts # Session/project media types
│ │ ├── requestCameraAccess.ts
│ │ ├── frameStep.ts # Frame-accurate navigation
│ │ ├── assetPath.ts # Dev vs. production asset resolution
│ │ ├── webcamMaskShapes.ts # CSS/Canvas clip path generation
│ │ ├── utils.ts # cn() Tailwind merge utility
│ │ └── exporter/ # Export pipeline (17 files, 121K chars)
│ │ ├── videoExporter.ts # Orchestrates full MP4 export
│ │ ├── gifExporter.ts # GIF export with gif.js
│ │ ├── frameRenderer.ts # Canvas 2D frame compositing
│ │ ├── annotationRenderer.ts # Text/blur/arrow rendering
│ │ ├── muxer.ts # mp4box wrapper
│ │ ├── streamingDecoder.ts # WebCodecs video decoder
│ │ ├── videoDecoder.ts # File-based video decoder
│ │ ├── audioEncoder.ts # Audio re-encoding
│ │ ├── asyncVideoFrameQueue.ts # Backpressure queue
│ │ ├── gradientParser.ts # CSS gradient to canvas
│ │ └── types.ts # Export config types
│ ├── i18n/ # Internationalization
│ │ ├── config.ts # Locale/namespace definitions
│ │ ├── loader.ts # Translation file loader
│ │ └── locales/ # 6 locale directories × 7 namespaces
│ └── utils/ # Utility functions
│ ├── aspectRatioUtils.ts # Aspect ratio calculations
│ ├── platformUtils.ts # macOS detection
│ ├── timeUtils.ts # Time formatting
│ └── getTestId.ts # Test ID generation
├── tests/
│ ├── e2e/ # Playwright E2E tests
│ │ └── gif-export.spec.ts
│ └── fixtures/
│ └── sample.webm
├── public/ # Static assets
│ ├── wallpapers/ # 18 background wallpapers
│ └── wasm/
│ └── web-demuxer.wasm # Video demuxer WASM module
├── icons/ # App icons (mac .icns, win .ico, PNG set)
└── scripts/
├── build_macos.sh # macOS signing + notarization
└── i18n-check.mjs # Translation key coverage checker
Module Reference
electron/ipc/handlers.ts (28K chars)
The largest single file, implementing all IPC communication. Key handler groups:
Source management:
get-sources— wrapsdesktopCapturer.getSources()and serializes thumbnails to data URLsselect-source/get-selected-source— store and retrieve the selected screen/window
Recording:
set-recording-state— toggles cursor telemetry capturestore-recorded-session— writes screen + webcam video blobs to disk with session manifeststore-recorded-video— legacy single-file store (delegates to session store)get-recorded-video-path— returns current session video or latest recording
File operations:
read-binary-file— reads files within allowed directories onlysave-exported-video— shows save dialog for MP4/GIF exportopen-video-file-picker— shows open dialog with video format filterreveal-in-folder—shell.showItemInFolder()with directory fallback
Project persistence:
save-project-file— trusted-path write or save dialogload-project-file/load-current-project-file— open and validate project JSON- Project media paths undergo
approveReadableVideoPath()validation
Window management:
switch-to-editor/switch-to-hud— window transitionsstart-new-recording— clears session state and switches to HUDopen-source-selector— opens/focuses source picker window
System:
request-camera-access— macOS-specificsystemPreferences.askForMediaAccessget-platform— returnsprocess.platformget-asset-base-path— resolves packaged vs. development asset pathsset-locale— updates main-process locale for menus/dialogsget-shortcuts/save-shortcuts— reads/writesshortcuts.jsonget-cursor-telemetry— loads cursor samples from.cursor.jsonopen-external-url—shell.openExternal()
src/hooks/useScreenRecorder.ts (1250 lines)
The recording lifecycle hook managing:
- Stream acquisition:
getUserMediawithchromeMediaSource: "desktop", constraint negotiation for 4K/60fps - Audio mixing: Web Audio API
AudioContextcombining system audio + microphone with gain boost - MediaRecorder: Codec selection (AV1 → H.264 → VP9 → VP8), adaptive bitrate
- Webcam: Separate
MediaRecorderfor webcam stream at 720p/30fps - Pause/Resume: Accumulates duration across segments for accurate timing
- Finalization:
fixWebmDurationcorrection, session storage via IPC - Restart: Discards current recording and starts a new one
src/lib/exporter/frameRenderer.ts (FrameRenderer class)
Core rendering class that composites each export frame:
- Maintains three canvases: composite (output), raster (screen content), shadow (effects)
- Applies crop region, padding, background wallpaper
- Renders webcam overlay with mask shapes (rectangle, rounded, circle, square)
- Renders annotations (text, image, figure/arrow, blur) via
annotationRenderer.ts - Applies zoom regions with keyframe interpolation
- Handles motion blur overlay
src/lib/exporter/videoExporter.ts (VideoExporter class)
Orchestrates the full export pipeline:
- Initializes
StreamingVideoDecoder,FrameRenderer,VideoMuxer, andAudioProcessor - Decodes source frames, applies editor state transformations
- Handles trim regions (skip frames) and speed regions (adjust timing)
- Reports progress via callback
- Supports cancellation
src/lib/compositeLayout.ts
Computes webcam overlay positioning for three layout modes:
- Picture-in-picture: Webcam sized as fraction of geometric mean of canvas dimensions, positioned with margin or at custom
(cx, cy)coordinates - Vertical stack: Webcam full-width at bottom, screen fills remaining space (cover mode)
- Dual frame: 2:1 horizontal split within max content bounds
Shape-specific behavior: circle and square masks force equal width/height; rounded masks apply 30% border radius.
src/hooks/useEditorHistory.ts
Undo/redo implementation using an immutable state stack:
- MAX_HISTORY: 80 states
- pushState: Creates a checkpoint and clears the future stack
- updateState: Live updates during slider drags — first call creates checkpoint, subsequent calls update in-place
- commitState: Marks the end of a live-update series
- EditorState: Includes zoom/trim/speed/annotation regions, crop, wallpaper, shadow, blur, border radius, padding, aspect ratio, webcam settings
src/lib/shortcuts.ts
Keyboard shortcut system with:
- 8 configurable actions:
addZoom,addTrim,addSpeed,addAnnotation,addBlur,addKeyframe,deleteSelected,playPause - 9 fixed shortcuts (undo, redo, cycle annotations, delete, pan/zoom timeline, frame step)
- Conflict detection across configurable and fixed bindings
- Platform-aware display formatting (Cmd vs. Ctrl, macOS symbols)
- Persistence via Electron IPC to
shortcuts.json
Code Patterns and Conventions
State management: React hooks with useState/useRef for local state. Context providers (I18nContext, ShortcutsContext) for cross-component state. No Redux or external state library.
IPC pattern: All communication uses ipcMain.handle() / ipcRenderer.invoke() (async request-response) or ipcMain.on() / ipcRenderer.send() (fire-and-forget for state notifications like set-has-unsaved-changes).
Error handling: Try-catch with { success: boolean, error?: string } return objects for IPC handlers. Toast notifications (sonner) for user-facing errors. Console logging for developer diagnostics.
Type safety: Strict TypeScript throughout. Union types for configuration enums (AspectRatio, ExportFormat, WebcamMaskShape). as const for literal arrays with derived types.
Styling: Tailwind CSS with cn() utility (clsx + tailwind-merge). CSS Modules for component-scoped styles (.module.css). Dark theme by default.
File naming: Components use PascalCase (.tsx), utilities use camelCase (.ts), tests use .test.ts or .spec.ts suffix.
Exports: Barrel exports via index.ts files in video-editor/, videoPlayback/, and exporter/ directories.
Testing Strategy
| Test Type | Tool | Config | Scope |
|---|---|---|---|
| Unit | Vitest | vitest.config.ts (jsdom) |
Hooks, utilities, layout computation |
| Browser | Vitest + Playwright | vitest.browser.config.ts |
Export pipeline (requires WebGL via SwiftShader) |
| E2E | Playwright | playwright.config.ts |
GIF export end-to-end |
npm scripts:
npm test # Unit tests (single run)
npm run test:watch # Unit tests (watch mode)
npm run test:browser # Browser tests with headless Chromium
npm run test:e2e # Playwright E2E tests
Browser test configuration: Uses Playwright with --enable-unsafe-swiftshader and --use-gl=swiftshader flags to enable software WebGL for PixiJS in headless CI. Test timeout: 120 seconds. Hook timeout: 30 seconds.
Note
The
tests/fixtures/sample.webmfile serves as the input for E2E GIF export tests. Browser tests include pattern:src/**/*.browser.test.{ts,tsx}.
Pre-commit: Husky + lint-staged runs biome check --no-errors-on-unmatched on staged TypeScript, JavaScript, and JSON files.
Known Issues (from Community)
| Issue | Status | Description |
|---|---|---|
| #157 | Open | Slow rendering/export on Linux (potentially Windows), 23 comments |
| #276 | Open | Video decode ended early |
| #376 | Open | Background not shown in exported video, 11 comments |
| #411 | Open | Force closing and not starting |
| #428 | Open | Can't export video on Windows |
| #433 | Open | Audio misaligned with speed + trim regions |
| #437 | Open | Blurry screen display |
| #439 | Open | Very slow export for 8-minute video at High quality |
| #379 | Open | Fail to read binary file |
Potential Improvements
- CLI / Agent interface (issue #349): Open feature request for headless CLI and agent API
- Stream Deck support (issue #440): Hardware control integration
- Transition controls (issue #446): Zoom and annotation transition easing
- Webcam overlay editing (issue #412): In-place webcam overlay manipulation
- Auto-hide toolbar (issue #435): Toolbar hiding during recording
- Zoom speed adjustment (issue #265): Configurable zoom animation speed
9. Integration Points
Electron APIs Used
| API | Purpose |
|---|---|
desktopCapturer |
Screen and window source enumeration |
systemPreferences |
macOS permission checks (microphone, camera) |
dialog |
File open/save dialogs |
shell |
Open URLs, reveal files in explorer |
Tray |
System tray icon and context menu |
Menu |
Application menu bar |
nativeImage |
Tray icon creation and resizing |
screen |
Display information, cursor position |
session |
Permission handlers for media access |
app |
Lifecycle, paths, command-line switches |
Browser APIs Used
| API | Purpose |
|---|---|
MediaRecorder |
Recording media streams |
navigator.mediaDevices.getUserMedia |
Stream acquisition |
navigator.mediaDevices.enumerateDevices |
Camera/microphone enumeration |
AudioContext / Web Audio API |
Audio mixing, level metering |
VideoDecoder / VideoEncoder (WebCodecs) |
Frame-level video processing |
OffscreenCanvas |
GPU frame rendering |
CSS Font Loading API |
Custom font verification |
localStorage |
User preferences, custom fonts, locale |
External Dependencies (Key)
| Package | Version | Purpose |
|---|---|---|
pixi.js |
8.14 | GPU-accelerated rendering, drop shadow filters |
mp4box |
2.2 | MP4 container muxing |
gif.js |
0.2 | GIF encoding with web workers |
mediabunny |
1.25 | Media processing utilities |
web-demuxer |
4.0 | WASM-based video container demuxing |
fix-webm-duration |
1.0 | WebM duration header correction |
gsap |
3.13 | Animation library |
dnd-timeline |
2.2 | Drag-and-drop timeline component |
react-rnd |
10.5 | Resizable/draggable annotation overlays |
emoji-picker-react |
4.16 | Emoji picker for annotations |
Contributing
Linting and formatting:
npm run lint # Check with Biome
npm run lint:fix # Auto-fix linting issues
npm run format # Auto-format with Biome
i18n coverage check:
npm run i18n:check # Verify all locales have matching translation keys
Code style: The project uses Biome (not ESLint/Prettier). Tab indentation, double quotes, 100-character line width. See biome.json for full rules.
PR guidelines: The CONTRIBUTING.md file exists in the repository root (contents not in briefing). See the User Guide for basic usage context.