From 72688f4fcbcc7588fa7ba6976d8e746464a77606 Mon Sep 17 00:00:00 2001 From: young Date: Tue, 25 Aug 2026 19:04:38 +1000 Subject: [PATCH 1/6] Show Recordly in the macOS Dock --- electron/main.ts | 21 +++++++++++++++++---- electron/windows.ts | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 38f4333ff..7a81199e2 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -528,6 +528,12 @@ function createTray() { tray.on("double-click", () => focusOrCreateMainWindow()); } +function shouldUseTray() { + // macOS exposes Recordly as a regular Dock app. Windows and Linux keep the + // tray entry because they do not have the macOS Dock lifecycle. + return process.platform !== "darwin"; +} + function getPublicAssetPath(filename: string) { return path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename); } @@ -1002,9 +1008,14 @@ app.whenReady().then(async () => { } }, 100); }); + if (process.platform === "darwin" && app.dock) { + await app.dock.show(); + } syncDockIcon(); - createTray(); - updateTrayMenu(); + if (shouldUseTray()) { + createTray(); + updateTrayMenu(); + } setupApplicationMenu(); // Ensure recordings directory exists await ensureRecordingsDir(); @@ -1031,8 +1042,10 @@ app.whenReady().then(async () => { (recording: boolean, sourceName: string) => { selectedSourceName = sourceName; setHudOverlayRecordingActive(recording); - if (!tray) createTray(); - updateTrayMenu(recording); + if (shouldUseTray()) { + if (!tray) createTray(); + updateTrayMenu(recording); + } if (recording) { reassertHudOverlayMouseState(); } diff --git a/electron/windows.ts b/electron/windows.ts index 8a10981f2..e0fb5be23 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -693,7 +693,12 @@ export function createUpdateToastWindow(): BrowserWindow { win.setAlwaysOnTop(true, "status"); } - win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + // Keep Recordly a foreground application so macOS does not temporarily + // remove its Dock icon while showing an overlay window. + skipTransformProcessType: process.platform === "darwin", + }); updateToastWindow = win; win.on("closed", () => { @@ -1001,7 +1006,12 @@ export function createCountdownWindow(): BrowserWindow { countdownWindow = win; - win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + // Keep Recordly a foreground application so macOS does not temporarily + // remove its Dock icon while showing the countdown. + skipTransformProcessType: process.platform === "darwin", + }); win.webContents.on("did-finish-load", () => { if (!win.isDestroyed()) { From 20596b6a304f602c79348e8dddd9ce0686a97e4e Mon Sep 17 00:00:00 2001 From: young Date: Tue, 25 Aug 2026 19:05:34 +1000 Subject: [PATCH 2/6] Keep cursor size and sharpness consistent in previews --- src/components/video-editor/VideoPlayback.tsx | 4 ++++ .../videoPlayback/cursorRenderer.ts | 19 ++++++++----------- .../videoPlayback/cursorScale.test.ts | 14 ++++++++++++++ .../video-editor/videoPlayback/cursorScale.ts | 5 +++++ ...rnVideoExporter.nativeStaticLayout.test.ts | 6 +++--- src/lib/exporter/modernVideoExporter.ts | 3 ++- 6 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 src/components/video-editor/videoPlayback/cursorScale.test.ts create mode 100644 src/components/video-editor/videoPlayback/cursorScale.ts diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 7490f454e..5aebd7ecb 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1084,6 +1084,7 @@ const VideoPlayback = forwardRef( motionBlurFilter.resolution = filterResolution; zoomBlurFilter.resolution = filterResolution; + cursorOverlayRef.current?.setFilterResolution(filterResolution); videoEffectsContainer.filterArea = new Rectangle(0, 0, stageWidth, stageHeight); }, []); @@ -2173,6 +2174,9 @@ const VideoPlayback = forwardRef( sway: cursorSwayRef.current, }); cursorOverlayRef.current = cursorOverlay; + cursorOverlay.setFilterResolution( + app.renderer.resolution || window.devicePixelRatio || 1, + ); cursorContainer.addChild(cursorOverlay.container); } else { cursorOverlayRef.current = null; diff --git a/src/components/video-editor/videoPlayback/cursorRenderer.ts b/src/components/video-editor/videoPlayback/cursorRenderer.ts index c1346ba52..c10b915d7 100644 --- a/src/components/video-editor/videoPlayback/cursorRenderer.ts +++ b/src/components/video-editor/videoPlayback/cursorRenderer.ts @@ -16,6 +16,7 @@ import { DEFAULT_CURSOR_STYLE, normalizeCursorClickEffectColor, } from "../types"; +import { getCursorViewportScale } from "./cursorScale"; import { computeCursorSwayRotation } from "./cursorSway"; import { type CursorViewportRect, projectCursorPositionToViewport } from "./cursorViewport"; import { @@ -110,8 +111,7 @@ export interface CursorRenderConfig { style: CursorStyle; } -const REFERENCE_WIDTH = 1920; -const MIN_CURSOR_VIEWPORT_SCALE = 0.55; +const MIN_CURSOR_VIEWPORT_SCALE = 0; const CURSOR_MOTION_BLUR_BASE_MULTIPLIER = 0.08; const CURSOR_TIME_DISCONTINUITY_MS = 100; const CURSOR_SWAY_SMOOTHING_MULTIPLIER = 0.7; @@ -807,13 +807,6 @@ function findLatestStableCursorType(samples: CursorTelemetryPoint[], timeMs: num return findLatestSample(samples, timeMs)?.cursorType ?? "arrow"; } -function getCursorViewportScale( - viewport: CursorViewportRect, - minViewportScale = MIN_CURSOR_VIEWPORT_SCALE, -) { - return Math.max(minViewportScale, viewport.width / REFERENCE_WIDTH); -} - function getCursorSwaySpringConfig(smoothingFactor: number, springTuning: CursorSpringTuning) { const baseConfig = getCursorSpringConfig( Math.min( @@ -1231,6 +1224,10 @@ export class PixiCursorOverlay { } } + setFilterResolution(resolution: number) { + this.cursorMotionBlurFilter.resolution = Math.max(1, resolution); + } + setClickBounce(clickBounce: number) { this.config.clickBounce = Math.max(0, clickBounce); } @@ -1346,7 +1343,7 @@ export class PixiCursorOverlay { const h = this.config.dotRadius * - getCursorViewportScale(viewport, this.config.minViewportScale); + getCursorViewportScale(viewport.width, this.config.minViewportScale); const { cursorType, clickSample, clickBounceProgress, clickProgress } = getCursorVisualState( samples, @@ -1642,7 +1639,7 @@ export function drawCursorOnCanvas( const px = viewport.x + smoothedState.x * viewport.width; const py = viewport.y + smoothedState.y * viewport.height; - const h = config.dotRadius * getCursorViewportScale(viewport, config.minViewportScale); + const h = config.dotRadius * getCursorViewportScale(viewport.width, config.minViewportScale); const { cursorType, clickSample, clickBounceProgress, clickProgress } = getCursorVisualState( samples, timeMs, diff --git a/src/components/video-editor/videoPlayback/cursorScale.test.ts b/src/components/video-editor/videoPlayback/cursorScale.test.ts new file mode 100644 index 000000000..ccd2c3add --- /dev/null +++ b/src/components/video-editor/videoPlayback/cursorScale.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { getCursorViewportScale } from "./cursorScale"; + +describe("cursor preview/export scale", () => { + it("preserves the same cursor-to-video ratio at preview and export sizes", () => { + const baseCursorHeight = 28 * 2.5; + const previewWidth = 720; + const exportWidth = 2940; + const previewCursorHeight = baseCursorHeight * getCursorViewportScale(previewWidth); + const exportCursorHeight = baseCursorHeight * getCursorViewportScale(exportWidth); + + expect(previewCursorHeight / previewWidth).toBeCloseTo(exportCursorHeight / exportWidth, 8); + }); +}); diff --git a/src/components/video-editor/videoPlayback/cursorScale.ts b/src/components/video-editor/videoPlayback/cursorScale.ts new file mode 100644 index 000000000..878901d3e --- /dev/null +++ b/src/components/video-editor/videoPlayback/cursorScale.ts @@ -0,0 +1,5 @@ +export const CURSOR_REFERENCE_VIEWPORT_WIDTH = 1920; + +export function getCursorViewportScale(viewportWidth: number, minimumScale = 0): number { + return Math.max(minimumScale, Math.max(0, viewportWidth) / CURSOR_REFERENCE_VIEWPORT_WIDTH); +} diff --git a/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts b/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts index f0e57d13d..cdc62d12d 100644 --- a/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts +++ b/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts @@ -236,12 +236,12 @@ describe("ModernVideoExporter native static-layout eligibility", () => { ).toBeNull(); }); - it("scales native static-layout cursor size with a minimum visible floor", () => { + it("preserves the cursor-to-video ratio at every native static-layout size", () => { const exporter = createExporter({ cursorSize: 3, cursorStyle: "tahoe" }); expect(exporter.getNativeStaticLayoutCursorSize(1920)).toBeCloseTo(84, 6); - expect(exporter.getNativeStaticLayoutCursorSize(960)).toBeCloseTo(46.2, 6); - expect(exporter.getNativeStaticLayoutCursorSize(480)).toBeCloseTo(46.2, 6); + expect(exporter.getNativeStaticLayoutCursorSize(960)).toBeCloseTo(42, 6); + expect(exporter.getNativeStaticLayoutCursorSize(480)).toBeCloseTo(21, 6); }); it("skips native static-layout when cursor click effects are enabled", () => { diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index c77ba9648..a5e4e8fed 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -25,6 +25,7 @@ import { SNAP_TO_EDGES_RATIO_AUTO, } from "@/components/video-editor/videoPlayback/cursorFollowCamera"; import { buildNativeCursorAtlas } from "@/components/video-editor/videoPlayback/cursorRenderer"; +import { getCursorViewportScale } from "@/components/video-editor/videoPlayback/cursorScale"; import { computePaddedLayout, scalePreviewBorderRadius, @@ -2089,7 +2090,7 @@ export class ModernVideoExporter { private getNativeStaticLayoutCursorSize(contentWidth: number) { const cursorStyle = this.config.cursorStyle ?? "tahoe"; - const viewportScale = Math.max(0.55, contentWidth / 1920); + const viewportScale = getCursorViewportScale(contentWidth); return ( 28 * (this.config.cursorSize ?? 3) * From 7da7e22ebef274934a763059b89c7250b8ce13a5 Mon Sep 17 00:00:00 2001 From: young Date: Tue, 25 Aug 2026 19:05:46 +1000 Subject: [PATCH 3/6] Clean up preview edges and blur controls --- src/components/video-editor/VideoPlayback.tsx | 59 +++---------------- src/i18n/locales/de/settings.json | 2 +- src/i18n/locales/en/settings.json | 2 +- src/i18n/locales/es/settings.json | 2 +- src/i18n/locales/fr/settings.json | 2 +- src/i18n/locales/it/settings.json | 2 +- src/i18n/locales/ko/settings.json | 2 +- src/i18n/locales/nl/settings.json | 2 +- src/i18n/locales/pt-BR/settings.json | 2 +- src/i18n/locales/ru/settings.json | 2 +- src/i18n/locales/zh-CN/settings.json | 2 +- src/i18n/locales/zh-TW/settings.json | 2 +- src/lib/exporter/frameRenderer.ts | 6 +- src/lib/exporter/modernFrameRenderer.ts | 13 +++- 14 files changed, 34 insertions(+), 66 deletions(-) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 5aebd7ecb..de81b01fc 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -171,9 +171,7 @@ import { SNAP_TO_EDGES_RATIO_AUTO, } from "./videoPlayback/cursorFollowCamera"; import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; -import { - layoutVideoContent as layoutVideoContentUtil, -} from "./videoPlayback/layoutUtils"; +import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers"; import { getWebcamMediaTargetTimeSeconds, shouldSeekWebcamMedia } from "./videoPlayback/webcamSync"; @@ -1909,53 +1907,6 @@ const VideoPlayback = forwardRef( }); }, [pixiReady, videoReady, layoutVideoContent, cropRegion]); - useEffect(() => { - const previewFrame = previewFrameRef.current; - if (!previewFrame) { - return; - } - let frameId: number | null = null; - - const applyPreviewFrameSquircle = () => { - const width = previewFrame.offsetWidth; - const height = previewFrame.offsetHeight; - if (width <= 0 || height <= 0) { - return; - } - - const squirclePath = getSquircleSvgPath({ - x: 0, - y: 0, - width, - height, - radius: 12, - }); - previewFrame.style.clipPath = `path('${squirclePath}')`; - previewFrame.style.setProperty("-webkit-clip-path", `path('${squirclePath}')`); - }; - - applyPreviewFrameSquircle(); - - if (typeof ResizeObserver === "undefined") { - return; - } - - const observer = new ResizeObserver(() => { - if (frameId !== null) { - cancelAnimationFrame(frameId); - } - frameId = requestAnimationFrame(applyPreviewFrameSquircle); - }); - - observer.observe(previewFrame); - return () => { - if (frameId !== null) { - cancelAnimationFrame(frameId); - } - observer.disconnect(); - }; - }, []); - useEffect(() => { if (!pixiReady || !videoReady) return; const container = containerRef.current; @@ -2955,6 +2906,9 @@ const VideoPlayback = forwardRef( : resolvedWallpaperKind === "video" ? {} : { background: resolvedWallpaper || "" }; + // Overscan blurred wallpaper layers so the browser never samples transparent + // pixels beyond the preview bounds, which otherwise looks like a vignette. + const backgroundBlurOverscan = backgroundBlur > 0 ? Math.ceil(backgroundBlur * 2) : 0; const fallbackVideoClassName = pixiRendererError ? "absolute inset-0 h-full w-full object-cover" : "pointer-events-none absolute left-0 top-0 h-px w-px opacity-0"; @@ -2985,7 +2939,8 @@ const VideoPlayback = forwardRef( style={{ width: "100%", aspectRatio: formatAspectRatioForCSS(aspectRatio, nativeAspectRatio), - borderRadius: "12px", + borderRadius: 0, + clipPath: "none", }} > {/* Background layer */} @@ -3000,6 +2955,7 @@ const VideoPlayback = forwardRef( playsInline style={{ filter: backgroundBlur > 0 ? `blur(${backgroundBlur}px)` : "none", + inset: -backgroundBlurOverscan, }} /> ) : ( @@ -3008,6 +2964,7 @@ const VideoPlayback = forwardRef( style={{ ...backgroundStyle, filter: backgroundBlur > 0 ? `blur(${backgroundBlur}px)` : "none", + inset: -backgroundBlurOverscan, }} /> )} diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 706dcc0d8..2f1f57e7c 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "Hintergrundunschärfe", + "backgroundBlur": "Unschärfe", "zoomMotionBlur": "Zoom-Bewegungsunschärfe", "temporalZoomMotionBlur": "Zeitliche Zoom-Bewegungsunschärfe", "temporalZoomMotionBlurDescription": "Steuert das Verschlussfenster und die Bildausschnitte, die vom neueren Zoom-Bewegungsunschärfe-Durchlauf verwendet werden.", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 60aa86221..8f0cfb7b2 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "Background Blur", + "backgroundBlur": "Blur", "zoomMotionBlur": "Zoom Motion Blur", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index d3545d595..d3c3f9fdd 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "Desenfoque de fondo", + "backgroundBlur": "Desenfoque", "zoomMotionBlur": "Desenfoque de movimiento del zoom", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 03d17ba6b..45c223b82 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Tortue" }, - "backgroundBlur": "Flou d’arrière-plan", + "backgroundBlur": "Flou", "zoomMotionBlur": "Flou de mouvement du zoom", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 7158b0daf..3410299ee 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Tartaruga" }, - "backgroundBlur": "Sfocatura sfondo", + "backgroundBlur": "Sfocatura", "zoomMotionBlur": "Motion blur dello zoom", "temporalZoomMotionBlur": "Sfocatura zoom temporale", "temporalZoomMotionBlurDescription": "Controlla la finestra dell'otturatore e i campioni di frame usati dal nuovo passaggio di sfocatura zoom.", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index e33828da2..17c7d6b11 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "배경 블러", + "backgroundBlur": "블러", "zoomMotionBlur": "확대 모션 블러", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json index 8d7269c7a..117620698 100644 --- a/src/i18n/locales/nl/settings.json +++ b/src/i18n/locales/nl/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Schildpad" }, - "backgroundBlur": "Achtergrondvervaging", + "backgroundBlur": "Vervaging", "zoomMotionBlur": "Zoom-bewegingsonscherpte", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 6cea132ea..e295b27f8 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "Desfoque de fundo", + "backgroundBlur": "Desfoque", "zoomMotionBlur": "Desfoque de movimento do zoom", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 15197cab4..dd347bf8b 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Черепаха" }, - "backgroundBlur": "Размытие фона", + "backgroundBlur": "Размытие", "zoomMotionBlur": "Размытие при зуме", "temporalZoomMotionBlur": "Временное размытие зума", "temporalZoomMotionBlurDescription": "Настройка плавности зума по времени и кадрам.", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 424324e9f..ce740260a 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "背景模糊", + "backgroundBlur": "模糊", "zoomMotionBlur": "缩放运动模糊", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 47fbbc381..263e054df 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -43,7 +43,7 @@ "amongus": "Among Us", "turtle": "Turtle" }, - "backgroundBlur": "背景模糊", + "backgroundBlur": "模糊", "zoomMotionBlur": "縮放動態模糊", "temporalZoomMotionBlur": "Temporal Zoom Blur", "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 018bd1764..9891a5188 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -2225,8 +2225,10 @@ export class FrameRenderer { if (this.config.backgroundBlur > 0) { ctx.save(); - ctx.filter = `blur(${this.config.backgroundBlur * 3}px)`; - ctx.drawImage(bgCanvas, 0, 0, w, h); + const blurPx = this.config.backgroundBlur * 3; + const overscan = Math.ceil(blurPx * 2); + ctx.filter = `blur(${blurPx}px)`; + ctx.drawImage(bgCanvas, -overscan, -overscan, w + overscan * 2, h + overscan * 2); ctx.restore(); } else { ctx.drawImage(bgCanvas, 0, 0, w, h); diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 33aad0dde..325bb28fe 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -1380,6 +1380,7 @@ export class FrameRenderer { this.backgroundBlurFilter.blur = this.config.backgroundBlur * 3; this.backgroundBlurFilter.quality = 4; this.backgroundBlurFilter.resolution = this.app?.renderer.resolution ?? 1; + this.backgroundBlurFilter.repeatEdgePixels = true; this.backgroundSprite.filters = [this.backgroundBlurFilter]; } } else if (this.backgroundTextureSource) { @@ -1410,8 +1411,16 @@ export class FrameRenderer { } blurredCtx.save(); - blurredCtx.filter = `blur(${this.config.backgroundBlur * 3}px)`; - blurredCtx.drawImage(sourceCanvas, 0, 0, blurredCanvas.width, blurredCanvas.height); + const blurPx = this.config.backgroundBlur * 3; + const overscan = Math.ceil(blurPx * 2); + blurredCtx.filter = `blur(${blurPx}px)`; + blurredCtx.drawImage( + sourceCanvas, + -overscan, + -overscan, + blurredCanvas.width + overscan * 2, + blurredCanvas.height + overscan * 2, + ); blurredCtx.restore(); return blurredCanvas; From 49fb397c9814d09131a0c28ed2e511edc63e7a62 Mon Sep 17 00:00:00 2001 From: young Date: Tue, 25 Aug 2026 19:06:02 +1000 Subject: [PATCH 4/6] Preserve the correct colour range in video exports --- electron/ipc/nativeVideoExport.test.ts | 33 ++++++++++++++++++++++++ electron/ipc/nativeVideoExport.ts | 25 ++++++++++++++++-- src/lib/exporter/modernVideoExporter.ts | 18 +++++-------- src/lib/exporter/videoColorSpace.test.ts | 13 ++++++++++ src/lib/exporter/videoColorSpace.ts | 18 +++++++++++++ src/lib/exporter/videoExporter.ts | 25 +++++------------- 6 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 src/lib/exporter/videoColorSpace.test.ts create mode 100644 src/lib/exporter/videoColorSpace.ts diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index 5992cadf3..957450cae 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -8,6 +8,7 @@ import { buildNativePrecompositedStaticLayoutArgs, buildNativeStaticBackgroundRenderArgs, buildNativeStaticLayoutChunks, + buildNativeVideoExportArgs, buildTrimmedSourceAudioFilter, createNativeSquircleMaskPgmBuffer, isNativeCudaOutOfMemory, @@ -158,6 +159,38 @@ describe("native static layout command builders", () => { expect(args).toContain("p1"); expect(args).not.toContain("yuv420p"); expect(args).toEqual(expect.arrayContaining(["-ss", "120.000", "-t", "60.000"])); + expect(args).toEqual( + expect.arrayContaining(["-colorspace", "bt709", "-color_range", "tv"]), + ); + }); + + it("converts full-range canvas pixels and tags native H.264 as BT.709 video range", () => { + const args = buildNativeVideoExportArgs( + "h264_videotoolbox", + { + width: 1920, + height: 1080, + frameRate: 30, + bitrate: 30_000_000, + encodingMode: "quality", + }, + "out.mp4", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "-vf", + "vflip,scale=in_range=full:out_range=tv", + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", + ]), + ); }); it("builds the stable CUDA scale plus CPU pad fallback command", () => { diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index 07aa64a91..f15e35d07 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -9,6 +9,17 @@ const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4; const MIN_EDITED_TRACK_TEMPO_SPEED = 0.5; const MAX_EDITED_TRACK_TEMPO_SPEED = 2; +export const FFMPEG_BT709_VIDEO_COLOR_ARGS = [ + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", +] as const; + export type NativeExportEncodingMode = "fast" | "balanced" | "quality"; export type NativeVideoExportAudioMode = "none" | "copy-source" | "trim-source" | "edited-track"; @@ -296,7 +307,7 @@ export function buildNativeVideoExportArgs( "-i", "pipe:0", "-vf", - "vflip", + "vflip,scale=in_range=full:out_range=tv", "-an", "-c:v", encoder, @@ -309,7 +320,14 @@ export function buildNativeVideoExportArgs( args.push(...getLibx264ModeArgs(options.encodingMode)); } - args.push("-pix_fmt", "yuv420p", "-movflags", "+faststart", outputPath); + args.push( + "-pix_fmt", + "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, + "-movflags", + "+faststart", + outputPath, + ); return args; } @@ -338,6 +356,7 @@ export function buildNativeCudaOverlayStaticLayoutArgs( "h264_nvenc", ...getNvencStaticLayoutModeArgs(config.encodingMode), ...getBitrateArgs(config.bitrate), + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, "-movflags", "+faststart", config.outputPath, @@ -371,6 +390,7 @@ export function buildNativeCudaScaleCpuPadStaticLayoutArgs( ...getBitrateArgs(config.bitrate), "-pix_fmt", "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, "-movflags", "+faststart", config.outputPath, @@ -533,6 +553,7 @@ export function buildNativePrecompositedStaticLayoutArgs( ...getBitrateArgs(config.bitrate), "-pix_fmt", "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, "-movflags", "+faststart", config.outputPath, diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index a5e4e8fed..2f889872d 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -93,6 +93,7 @@ import type { ExportRenderBackend, ExportResult, } from "./types"; +import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace"; interface VideoExporterConfig extends ExportConfig { videoUrl: string; @@ -2726,9 +2727,11 @@ export class ModernVideoExporter { if (this.nativeEncoderError) throw this.nativeEncoderError; } const canvas = this.renderer!.getCanvas(); + // @ts-expect-error - colorSpace is supported at runtime but missing from this DOM typing. const frame = new VideoFrame(canvas, { timestamp, duration: frameDuration, + colorSpace: EXPORT_CANVAS_COLOR_SPACE, }); this.nativeH264Encoder.encode(frame, { keyFrame: frameIndex % 300 === 0 }); frame.close(); @@ -2957,12 +2960,7 @@ export class ModernVideoExporter { const exportFrame = new VideoFrame(canvas, { timestamp, duration: frameDuration, - colorSpace: { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }, + colorSpace: EXPORT_CANVAS_COLOR_SPACE, }); while ( @@ -3377,12 +3375,8 @@ export class ModernVideoExporter { try { if (isFirstChunk && this.videoDescription) { // Add decoder config for the first chunk - const colorSpace = this.videoColorSpace || { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }; + const colorSpace = + this.videoColorSpace || ENCODED_H264_COLOR_SPACE_FALLBACK; const metadata: EncodedVideoChunkMetadata = { decoderConfig: { diff --git a/src/lib/exporter/videoColorSpace.test.ts b/src/lib/exporter/videoColorSpace.test.ts new file mode 100644 index 000000000..a19af92b6 --- /dev/null +++ b/src/lib/exporter/videoColorSpace.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace"; + +describe("export colour metadata", () => { + it("does not confuse full-range RGB input with encoded YUV output", () => { + expect(EXPORT_CANVAS_COLOR_SPACE).toMatchObject({ matrix: "rgb", fullRange: true }); + expect(ENCODED_H264_COLOR_SPACE_FALLBACK).toMatchObject({ + matrix: "bt709", + transfer: "bt709", + fullRange: false, + }); + }); +}); diff --git a/src/lib/exporter/videoColorSpace.ts b/src/lib/exporter/videoColorSpace.ts new file mode 100644 index 000000000..69df56bc4 --- /dev/null +++ b/src/lib/exporter/videoColorSpace.ts @@ -0,0 +1,18 @@ +/** The renderer composites into an sRGB canvas, whose pixels are full-range RGB. */ +export const EXPORT_CANVAS_COLOR_SPACE = { + primaries: "bt709", + transfer: "iec61966-2-1", + matrix: "rgb", + fullRange: true, +} as const satisfies VideoColorSpaceInit; + +/** + * H.264 encoders normally convert the canvas to video-range YUV. Use this only + * when the encoder does not report its own output colour metadata. + */ +export const ENCODED_H264_COLOR_SPACE_FALLBACK = { + primaries: "bt709", + transfer: "bt709", + matrix: "bt709", + fullRange: false, +} as const satisfies VideoColorSpaceInit; diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index b99f5f96c..11ea3b1e0 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -8,8 +8,8 @@ import type { CursorStyle, CursorTelemetryPoint, Padding, - SpeedRegion, SourceAudioTrackSettings, + SpeedRegion, TrimRegion, WebcamOverlaySettings, ZoomMotionBlurTuning, @@ -38,6 +38,7 @@ import type { ExportProgress, ExportResult, } from "./types"; +import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace"; const DEFAULT_MAX_ENCODE_QUEUE = 240; const PROGRESS_SAMPLE_WINDOW_MS = 1_000; @@ -825,12 +826,7 @@ export class VideoExporter { const frame = new VideoFrame(canvas, { timestamp, duration: frameDuration, - colorSpace: { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }, + colorSpace: EXPORT_CANVAS_COLOR_SPACE, }); this.nativeH264Encoder.encode(frame, { keyFrame: frameIndex % 300 === 0 }); frame.close(); @@ -1077,12 +1073,7 @@ export class VideoExporter { const exportFrame = new VideoFrame(canvas, { timestamp, duration: frameDuration, - colorSpace: { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }, + colorSpace: EXPORT_CANVAS_COLOR_SPACE, }); while ( @@ -1270,12 +1261,8 @@ export class VideoExporter { try { if (isFirstChunk && this.videoDescription) { // Add decoder config for the first chunk - const colorSpace = this.videoColorSpace || { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }; + const colorSpace = + this.videoColorSpace || ENCODED_H264_COLOR_SPACE_FALLBACK; const metadata: EncodedVideoChunkMetadata = { decoderConfig: { From f874297ad301aa7cef365e6ba57d61abf7bb6af9 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:23:15 +1000 Subject: [PATCH 5/6] Show Recordly in the Windows taskbar --- electron/main.ts | 6 +++--- electron/windows.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 7a81199e2..470fc8243 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -529,9 +529,9 @@ function createTray() { } function shouldUseTray() { - // macOS exposes Recordly as a regular Dock app. Windows and Linux keep the - // tray entry because they do not have the macOS Dock lifecycle. - return process.platform !== "darwin"; + // macOS and Windows expose Recordly through their Dock/taskbar. Keep the + // tray entry only on Linux, where it remains the primary app entry point. + return process.platform === "linux"; } function getPublicAssetPath(filename: string) { diff --git a/electron/windows.ts b/electron/windows.ts index e0fb5be23..d7b38be3d 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -464,7 +464,9 @@ export function createHudOverlayWindow(): BrowserWindow { backgroundColor: "#00000000", resizable: false, alwaysOnTop: true, - skipTaskbar: true, + // The HUD is Recordly's persistent top-level window, so it owns the + // Windows taskbar entry while auxiliary overlays stay hidden there. + skipTaskbar: process.platform !== "win32", hasShadow: false, show: false, focusable: false, From f3b0fdf8c801ed61be9222ff22510a564d4186c0 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:59:28 +1000 Subject: [PATCH 6/6] Address taskbar and rendering review feedback --- electron/hudOverlayWindowOptions.test.ts | 21 +++++++++++++++++++ electron/hudOverlayWindowOptions.ts | 12 +++++++++++ electron/ipc/nativeVideoExport.test.ts | 18 +++++++++------- electron/ipc/nativeVideoExport.ts | 12 +++++++---- electron/windows.ts | 12 ++++++++--- src/components/video-editor/VideoPlayback.tsx | 4 +++- 6 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 electron/hudOverlayWindowOptions.test.ts create mode 100644 electron/hudOverlayWindowOptions.ts diff --git a/electron/hudOverlayWindowOptions.test.ts b/electron/hudOverlayWindowOptions.test.ts new file mode 100644 index 000000000..08f8c6531 --- /dev/null +++ b/electron/hudOverlayWindowOptions.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions"; + +describe("getHudOverlayTaskbarOptions", () => { + it("keeps a focusable HUD in the Windows taskbar", () => { + expect(getHudOverlayTaskbarOptions("win32")).toEqual({ + skipTaskbar: false, + focusable: true, + }); + }); + + it.each([ + "darwin", + "linux", + ] as const)("keeps the HUD non-focusable and out of the taskbar on %s", (platform) => { + expect(getHudOverlayTaskbarOptions(platform)).toEqual({ + skipTaskbar: true, + focusable: false, + }); + }); +}); diff --git a/electron/hudOverlayWindowOptions.ts b/electron/hudOverlayWindowOptions.ts new file mode 100644 index 000000000..27fcd2dea --- /dev/null +++ b/electron/hudOverlayWindowOptions.ts @@ -0,0 +1,12 @@ +export interface HudOverlayTaskbarOptions { + skipTaskbar: boolean; + focusable: boolean; +} + +export function getHudOverlayTaskbarOptions(platform: NodeJS.Platform): HudOverlayTaskbarOptions { + const showInWindowsTaskbar = platform === "win32"; + return { + skipTaskbar: !showInWindowsTaskbar, + focusable: showInWindowsTaskbar, + }; +} diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index 957450cae..b9b7ba1a5 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -11,6 +11,7 @@ import { buildNativeVideoExportArgs, buildTrimmedSourceAudioFilter, createNativeSquircleMaskPgmBuffer, + FFMPEG_BT709_VIDEO_COLOR_ARGS, isNativeCudaOutOfMemory, } from "./nativeVideoExport"; @@ -151,17 +152,15 @@ describe("native static layout command builders", () => { expect(args).toContain("-filter_complex"); expect(args).toContain( - "color=c=0x101010:s=1920x1080:r=60:d=60.000,format=nv12,hwupload_cuda[bg];" + - "[0:v]scale_cuda=w=1536:h=864:format=nv12,fps=60[fg];" + + "color=c=0x101010:s=1920x1080:r=60:d=60.000,format=nv12,setrange=limited,hwupload_cuda[bg];" + + "[0:v]scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,hwupload_cuda[fg];" + "[bg][fg]overlay_cuda=192:108:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=60.000,setpts=PTS-STARTPTS[out]", ); expect(args).toContain("h264_nvenc"); expect(args).toContain("p1"); expect(args).not.toContain("yuv420p"); expect(args).toEqual(expect.arrayContaining(["-ss", "120.000", "-t", "60.000"])); - expect(args).toEqual( - expect.arrayContaining(["-colorspace", "bt709", "-color_range", "tv"]), - ); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); }); it("converts full-range canvas pixels and tags native H.264 as BT.709 video range", () => { @@ -199,12 +198,13 @@ describe("native static layout command builders", () => { expect(args).toEqual( expect.arrayContaining([ "-vf", - "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", "-map", "0:v:0", "-an", ]), ); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); }); it("sanitizes unsupported background colors to the safe dark fallback", () => { @@ -214,7 +214,7 @@ describe("native static layout command builders", () => { }); expect(args).toContain( - "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", ); }); @@ -262,12 +262,14 @@ describe("native static layout command builders", () => { expect(args).toEqual(expect.arrayContaining(["-i", "background.png", "-i", "mask.pgm"])); expect(filterComplex).toContain( - "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,fps=60,format=rgba", + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=full,fps=60,format=rgba", ); expect(filterComplex).toContain("[fgbase][mask]alphamerge[fg]"); expect(filterComplex).toContain("overlay=x=192:y=108:format=auto"); + expect(filterComplex).toContain("scale=in_range=full:out_range=tv,format=yuv420p[out]"); expect(args).toContain("h264_nvenc"); expect(args).toEqual(expect.arrayContaining(["-pix_fmt", "yuv420p"])); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); }); it("creates an opaque PGM mask for square video corners and a partial mask for radius", () => { diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index f15e35d07..c8bacce1e 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -20,6 +20,10 @@ export const FFMPEG_BT709_VIDEO_COLOR_ARGS = [ "tv", ] as const; +const FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER = "scale=in_range=auto:out_range=tv"; +const FFMPEG_AUTO_TO_FULL_RANGE_FILTER = "scale=in_range=auto:out_range=full"; +const FFMPEG_FULL_TO_VIDEO_RANGE_FILTER = "scale=in_range=full:out_range=tv"; + export type NativeExportEncodingMode = "fast" | "balanced" | "quality"; export type NativeVideoExportAudioMode = "none" | "copy-source" | "trim-source" | "edited-track"; @@ -346,7 +350,7 @@ export function buildNativeCudaOverlayStaticLayoutArgs( "-i", config.inputPath, "-filter_complex", - `color=c=${backgroundColor}:s=${config.width}x${config.height}:r=${config.frameRate}:d=${durationSec},format=nv12,hwupload_cuda[bg];[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12,fps=${config.frameRate}[fg];[bg][fg]overlay_cuda=${config.offsetX}:${config.offsetY}:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=${durationSec},setpts=PTS-STARTPTS[out]`, + `color=c=${backgroundColor}:s=${config.width}x${config.height}:r=${config.frameRate}:d=${durationSec},format=nv12,setrange=limited,hwupload_cuda[bg];[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER},fps=${config.frameRate},hwupload_cuda[fg];[bg][fg]overlay_cuda=${config.offsetX}:${config.offsetY}:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=${durationSec},setpts=PTS-STARTPTS[out]`, "-map", "[out]", "-an", @@ -378,7 +382,7 @@ export function buildNativeCudaScaleCpuPadStaticLayoutArgs( "-i", config.inputPath, "-vf", - `scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,fps=${config.frameRate},pad=w=${config.width}:h=${config.height}:x=${config.offsetX}:y=${config.offsetY}:color=${backgroundColor}`, + `scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER},fps=${config.frameRate},pad=w=${config.width}:h=${config.height}:x=${config.offsetX}:y=${config.offsetY}:color=${backgroundColor}`, "-map", "0:v:0", "-an", @@ -534,10 +538,10 @@ export function buildNativePrecompositedStaticLayoutArgs( ); } - const foregroundFilter = `[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,fps=${config.frameRate},format=rgba[fgbase]`; + const foregroundFilter = `[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_FULL_RANGE_FILTER},fps=${config.frameRate},format=rgba[fgbase]`; const maskFilter = useMask ? ";[2:v]format=gray[mask];[fgbase][mask]alphamerge[fg]" : ""; const foregroundLabel = useMask ? "fg" : "fgbase"; - const filterComplex = `${foregroundFilter}${maskFilter};[1:v]format=rgba[bg];[bg][${foregroundLabel}]overlay=x=${config.offsetX}:y=${config.offsetY}:format=auto,trim=duration=${durationSec},setpts=PTS-STARTPTS,format=yuv420p[out]`; + const filterComplex = `${foregroundFilter}${maskFilter};[1:v]format=rgba[bg];[bg][${foregroundLabel}]overlay=x=${config.offsetX}:y=${config.offsetY}:format=auto,trim=duration=${durationSec},setpts=PTS-STARTPTS,${FFMPEG_FULL_TO_VIDEO_RANGE_FILTER},format=yuv420p[out]`; args.push( "-filter_complex", diff --git a/electron/windows.ts b/electron/windows.ts index d7b38be3d..cccb9e243 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -10,6 +10,7 @@ import { resizeHudOverlayFallbackBounds, shouldExpandHudOverlayFallback, } from "./hudOverlayBounds"; +import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions"; import { getPackagedRendererBaseUrl } from "./rendererServer"; const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); @@ -466,10 +467,9 @@ export function createHudOverlayWindow(): BrowserWindow { alwaysOnTop: true, // The HUD is Recordly's persistent top-level window, so it owns the // Windows taskbar entry while auxiliary overlays stay hidden there. - skipTaskbar: process.platform !== "win32", + ...getHudOverlayTaskbarOptions(process.platform), hasShadow: false, show: false, - focusable: false, webPreferences: { preload: path.join(electronWindowsDir, "preload.mjs"), nodeIntegration: false, @@ -484,7 +484,13 @@ export function createHudOverlayWindow(): BrowserWindow { return; } hasShownHudWindow = true; - win.show(); + if (process.platform === "win32") { + // A focusable window is required for a Windows taskbar entry, but the + // always-on-top HUD must not steal focus when Recordly starts. + win.showInactive(); + } else { + win.show(); + } win.moveTop(); if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { win.setIgnoreMouseEvents(false); diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index de81b01fc..52e9fa43c 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -2948,7 +2948,7 @@ const VideoPlayback = forwardRef(