The video preview was noticeably blurry/pixelated because PixiJS filters were always attached to the video container, even when inactive (blur=0, no motion). This forced every frame through an intermediate RenderTexture at renderer resolution, downsampling the native video before it even reached the screen.
Now filters are only attached during active zoom transitions when the camera is actually moving, and detached as soon as it settles. Added hysteresis to the motion threshold to prevent filter toggling when intensity hovers near the boundary.
Verifies the selected recording MIME type can also be decoded by the
in-app <video> element via canPlayType(), so that codecs like av1 —
which MediaRecorder.isTypeSupported() reports as supported for encoding
but are not decodable on fresh Windows installs without the AV1 Video
Extension — are not chosen.
Also passes undefined (not an unsupported fallback string) into
new MediaRecorder() when no preferred type is supported, letting the
browser pick a default instead of throwing NotSupportedError.
Replaces the previous 'as ProcessedDesktopSource' cast with a properly
typed LINUX_PORTAL_SOURCE constant.
Co-authored-by: meiiie <meiiie@users.noreply.github.com>
- electron/main.ts: treat missing sourceId on linux as portal sentinel
so the request handler never calls getSources() (which itself opens
an extra portal dialog) on fresh sessions.
- useScreenRecorder.ts: persist the synthesized portal sentinel via
selectSource() so main has the source set before getDisplayMedia.
Extract acquireLinuxPortalStream() helper to dedupe the three
duplicated getDisplayMedia constraint blocks.
- LaunchWindow.tsx: hide the screen-source selector button (and its
separator) on Linux so users cannot trigger an extra portal dialog
via the dropdown.
Previously on Linux/Wayland, starting a fullscreen recording required three
separate picker interactions: an in-app source dropdown plus two xdg-desktop-portal
dialogs. The duplicate portal dialogs were caused by:
1. resolveBrowserCaptureSource() calling desktopCapturer.getSources() in the
renderer, which itself triggers the portal on Wayland.
2. setDisplayMediaRequestHandler() in main also calling getSources(), which
triggers another portal.
3. Returning a pre-enumerated source id to Chromium, which on Wayland is stale
and forces Chromium to re-prompt via the portal during MediaStream creation.
Additionally, the editor window failed to appear after recording on some
Wayland sessions because 'ready-to-show' did not fire reliably.
Changes:
- LaunchWindow: skip the in-app source dropdown on Linux and start recording
directly; the OS portal becomes the source picker.
- useScreenRecorder: introduce a 'screen:linux-portal' sentinel source. When
set, route capture through navigator.mediaDevices.getDisplayMedia() so the
portal handles selection in a single dialog. Skip resolveBrowserCaptureSource
for the sentinel to avoid an extra getSources() call.
- electron/main: in setDisplayMediaRequestHandler, when the sentinel is set,
skip desktopCapturer.getSources() entirely and return a synthetic source so
Chromium opens the portal exactly once for the actual capture.
- electron/windows: in createEditorWindow, also call win.show() from
did-finish-load as a fallback for Linux/Wayland where ready-to-show may
not fire.
- Change ?? 99 fallback to ?? 100 in finalizing progress label
- Add editor.export.processingAudioEdits i18n key to all 5 locales
- Tighten sourceTimeToOutputTime return type to number (was number|null)
- Remove now-unused totalOutputDurationMs param from scheduleRegionForChunk
- Remove ?? fallbacks at call sites (function always returns a number)
- Add Linux GPU config: use EGL (better Wayland compat), disable VAAPI
video decoder/encoder (many distros ship broken drivers causing
'vaInitialize failed' and preventing renderer from loading)
- Add ready-to-show fallback for HUD window: if did-finish-load never
fires (GPU failure), show the window after 500ms via ready-to-show
Ref #261
- Fall back to video container duration when mainBuffer is null but
audioRegions are present (prevents empty output)
- Use data.numberOfChannels as deinterleave stride instead of capped
dataChannels (fixes garbled audio for 5.1+ sources)
- Fix .coderabbit.yaml: '*' is not valid regex, use '.*'
- Add encoderError check after awaiting pendingMuxing to prevent
finalization of corrupt exports (both exporters)
- Remove unreachable synchronous fast-path in getMediaDurationSec
- Set preload='metadata' before src for correct browser behavior
- Localize audio processing status text with t()
Major rewrite of the audio export pipeline for stability and memory efficiency:
## Streaming Decode
- Primary decode path uses WebDemuxer + AudioDecoder (WebCodecs streaming)
- Falls back to bulk decodeAudioData if streaming fails
- Avoids holding full compressed file + decoded PCM simultaneously
## Chunked Offline Rendering
- Processes timeline in 30-second OfflineAudioContext chunks
- Memory bounded to ~30s of PCM per chunk regardless of recording length
## Chunked WAV Writing
- Writes PCM in ~256KB chunks instead of single massive ArrayBuffer
- Eliminates OOM for long recordings on the native/FFmpeg export path
## Chunked Encoding
- Single AudioEncoder kept alive across all chunks (clean AAC stream)
- Proper backpressure and error propagation per chunk
## scheduleBufferThroughTimeline chunk windowing
- Clips source nodes to chunk boundaries for correct cross-chunk audio
- Backwards compatible defaults when no chunk params provided
## Other export fixes
- Progress cap: 99% -> 100% in both exporters and UI
- Muxing errors: propagated instead of silently swallowed
- Mac recording: enhanced warning when no audio files for muxing
- Export timeout: eliminated by removing real-time rendering
- IPC get-local-media-url now checks both fs.realpath and path.resolve forms
against approvedLocalReadPaths, so symlinks approved via approveUserPath work
- Extract duplicated sync correction parameters into shared constants in
audioEncoder.ts (SYNC_SEEK_THRESHOLD_SEC, SYNC_PLAYBACK_RATE_OPTIONS)
- Replace setInterval in recording handler with startCursorSampling() so
the drift-compensating scheduler is used for actual recordings (was missed)
- Pass realpath-resolved path to buildMediaUrl for symlink consistency
- Use resolvedWebcamVideoUrl in thumbnail/export configs with file:// fallback
- Fix telemetry drift reset: use nextExpectedMs - now for delay instead
of stale drift value after baseline reset (avoids 1ms rapid sample)
- Use RECORDER_TIMESLICE_MS for mic fallback recorder (was hardcoded 1000)
- Return requested timeMs in extensionHost getCursorAt boundary clamps
- Remove premature null of webcam URL to prevent flicker on path change
- Use fs.realpath in IPC get-local-media-url to match media server check
- Replace setInterval with drift-compensating recursive setTimeout for
cursor sampling. Under CPU load setInterval bunches or skips callbacks,
creating irregular gaps in telemetry data.
- Add binary search + linear interpolation to export cursor lookup
(modernFrameRenderer) matching the playback path. Was using O(n)
nearest-neighbor which caused visible cursor jumping in exports.
- Apply same interpolation fix to extension API getCursorAt().
- Always route audio through aresample=async=1:first_pts=0 filter during
muxing on both macOS and Windows, even when duration delta is small.
Previously skipped when delta ≤50ms, which left progressive clock drift
(from CPU load) completely uncorrected.
- Lower sync detection threshold from 50ms to 20ms so tempo correction
kicks in earlier.
- Use explicit 48kHz AudioContext sample rate in both browser recording
and export rendering to prevent sample rate mismatch drift.
- Reduce MediaRecorder timeslice from 1000ms to 250ms to reduce chunk
loss under CPU pressure.
- Tighten export audio sync: seek threshold 300ms→150ms, tolerance
15ms→8ms, correction window 2s→0.5s, max adjustment ±8%→±12%.
- Lower companion audio start delay threshold from 50ms to 25ms.
- Default preference to 'system' so first-run users get OS preference detection
- Set data-theme attribute on documentElement alongside .dark class toggle
- Create missing ThemeContext.tsx (build-breaking)
- Fix text contrast on blue accent backgrounds (use text-white on bg-[#2563EB])
- Fix PlaybackControls border to use border-white/10 (dark overlay)
- Fix ExportSettingsMenu/FormatSelector active state text for light mode
- Fix ProjectBrowserDialog placeholder text on dark gradient
- Fix SettingsPanel dot preview border, accent text, hover text, X button text
- Fix TimelineEditor tooltip text and custom aspect input borders/bg
- Fix TutorialHelp malformed CSS class and kbd backgrounds
- Fix KeyboardShortcutsHelp kbd backgrounds for light mode
- Fix ExtensionManager badge text on blue pill
- Fix AnnotationSettingsPanel upload button hover text
- Fix index.css light mode timeline colors (was identical to dark)
- Use videoSourcePath instead of fromFileUrl(videoPath) for cursor
telemetry — fixes broken path when videoPath is an HTTP media-server URL
- Validate filePath against approvedLocalReadPaths in get-local-media-url
IPC handler before returning a URL
- Use fs.realpath() instead of path.resolve() to close symlink bypass
- Fix range parser to handle suffix ranges (bytes=-500) and guard
against NaN values
- Add CORS headers (Access-Control-Allow-Origin) to media server
responses to prevent canvas tainting when using video frames
- Handle OPTIONS preflight requests
- Clear stale resolvedWebcamVideoUrl before resolving new URL to
prevent flash of stale content
Add a complete light mode theme with a ThemeContext and CSS custom properties
for all UI surfaces. Replaces hardcoded dark colors with theme-aware
equivalents across 32 files including:
- Tailwind config extended with theme color tokens
- CSS custom properties for light/dark palettes in index.css
- ThemeContext provider in App.tsx with system preference detection
- All editor panels, dialogs, timeline, and settings updated to use
theme-aware classes (text-foreground, bg-surface, border-border, etc.)
- Timeline glass effects and module CSS updated for both modes
- UI primitives (accordion, switch, slider, sonner) themed
On Windows, the packaged app serves the renderer from http://127.0.0.1:PORT,
which causes Chromium to block file:// URLs in <video> elements — even with
webSecurity disabled. This affects all users running the packaged build.
Add a local media HTTP server (random port) that streams approved video files
with range request support for seeking. The renderer now resolves video paths
through this server instead of using file:// URLs directly.
- electron/mediaServer.ts: HTTP server with path validation against
approvedLocalReadPaths and Content-Range support
- IPC handler 'get-local-media-url' converts file paths to HTTP URLs
- resolveVideoUrl() in renderer falls back to file:// if server unavailable
- Improved <video> onError to log actual MediaError details
Fixes#244
The AND gate broke access to user-selected files outside the allowlist
(wallpapers, user videos, etc). Keep isPathInsideDirectory normalization fix,
revert the existsSync AND guard back to the original OR behavior.
- Remove dead helperExists local in recording/windows.ts
- Hoist fs/promises import out of close handler in recording/ffmpeg.ts
- Guard fs.readdir with mkdir in recording/prune.ts (ENOENT resilience)
- Derive companion audio suffixes from COMPANION_AUDIO_LAYOUTS in prune.ts
- Guard mousemove hook registration to Linux only in cursor/interaction.ts
- Replace dynamic require('electron') with static import in cursor/monitor.ts
- Wrap nodeRequire in try/catch in ffmpeg/binary.ts for fallback safety
- Fix hardcoded timeOffsetMs: 0 in project/session.ts (use normalizer)
- Fix isPathInsideDirectory to normalize candidatePath in project/manager.ts
- Fix isAllowedLocalReadPath security: require path to be in allowlist (AND not OR)
- Derive extension regex from constants in project/manager.ts
- Consolidate duplicate Duration parsers in recording/diagnostics.ts
- Refactor ensureReadableFile to use options object instead of description string
- Make swiftc compilation async (execFile) in paths/binaries.ts
- Add socket timeout to httpsGet in captions/whisper.ts