Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions electron/hudOverlayWindowOptions.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
12 changes: 12 additions & 0 deletions electron/hudOverlayWindowOptions.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
45 changes: 40 additions & 5 deletions electron/ipc/nativeVideoExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
buildNativePrecompositedStaticLayoutArgs,
buildNativeStaticBackgroundRenderArgs,
buildNativeStaticLayoutChunks,
buildNativeVideoExportArgs,
buildTrimmedSourceAudioFilter,
createNativeSquircleMaskPgmBuffer,
FFMPEG_BT709_VIDEO_COLOR_ARGS,
isNativeCudaOutOfMemory,
} from "./nativeVideoExport";

Expand Down Expand Up @@ -150,14 +152,44 @@ 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([...FFMPEG_BT709_VIDEO_COLOR_ARGS]));
});

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", () => {
Expand All @@ -166,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", () => {
Expand All @@ -181,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",
);
});

Expand Down Expand Up @@ -229,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", () => {
Expand Down
37 changes: 31 additions & 6 deletions electron/ipc/nativeVideoExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ 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;

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";
Expand Down Expand Up @@ -296,7 +311,7 @@ export function buildNativeVideoExportArgs(
"-i",
"pipe:0",
"-vf",
"vflip",
"vflip,scale=in_range=full:out_range=tv",
"-an",
"-c:v",
encoder,
Expand All @@ -309,7 +324,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;
}

Expand All @@ -328,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",
Expand All @@ -338,6 +360,7 @@ export function buildNativeCudaOverlayStaticLayoutArgs(
"h264_nvenc",
...getNvencStaticLayoutModeArgs(config.encodingMode),
...getBitrateArgs(config.bitrate),
...FFMPEG_BT709_VIDEO_COLOR_ARGS,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"-movflags",
"+faststart",
config.outputPath,
Expand All @@ -359,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",
Expand All @@ -371,6 +394,7 @@ export function buildNativeCudaScaleCpuPadStaticLayoutArgs(
...getBitrateArgs(config.bitrate),
"-pix_fmt",
"yuv420p",
...FFMPEG_BT709_VIDEO_COLOR_ARGS,
"-movflags",
"+faststart",
config.outputPath,
Expand Down Expand Up @@ -514,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",
Expand All @@ -533,6 +557,7 @@ export function buildNativePrecompositedStaticLayoutArgs(
...getBitrateArgs(config.bitrate),
"-pix_fmt",
"yuv420p",
...FFMPEG_BT709_VIDEO_COLOR_ARGS,
"-movflags",
"+faststart",
config.outputPath,
Expand Down
21 changes: 17 additions & 4 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,12 @@ function createTray() {
tray.on("double-click", () => focusOrCreateMainWindow());
}

function shouldUseTray() {
// 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) {
return path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename);
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down
28 changes: 23 additions & 5 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -464,10 +465,11 @@ 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.
...getHudOverlayTaskbarOptions(process.platform),
hasShadow: false,
show: false,
focusable: false,
webPreferences: {
preload: path.join(electronWindowsDir, "preload.mjs"),
nodeIntegration: false,
Expand All @@ -482,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);
Expand Down Expand Up @@ -693,7 +701,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", () => {
Expand Down Expand Up @@ -1001,7 +1014,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()) {
Expand Down
Loading
Loading