Skip to content
Closed
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,22 @@ OpenScreen is 100% free for personal and commercial use. Use it, modify it, dist
- Add annotations (text, arrows, images).
- Trim sections of the clip.
- Customize speed at different segments.
- **Webcam Focus** — draw attention to your face at key moments.
- Export in different aspect ratios and resolutions.

### Webcam Focus

When you record with a webcam, you can mark specific time ranges on the timeline where the webcam should take center stage. During those segments the screen recording blurs and dims while the webcam expands to fill most of the frame. Outside the region everything returns to the normal layout. Both transitions animate smoothly.

**How to use it:**
1. Make sure your recording includes a webcam feed.
2. In the editor, click the **camera icon** (🎥) in the timeline toolbar to place a Webcam Focus region at the current playhead position.
3. Drag the edges of the indigo region to set the start and end times.
4. Press **Play** to preview — the webcam enlarges to portrait near-full-screen and the screen recording fades behind it.
5. Delete a region by selecting it and pressing `Delete` / `Backspace`.

The effect is saved with your project and included in the exported video.

## Installation

Download the latest installer for your platform from the [GitHub Releases](https://github.com/siddharthvaddem/openscreen/releases) page.
Expand Down
8 changes: 8 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ interface Window {
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
setLocale: (locale: string) => Promise<void>;
generateSubtitles: (
videoPath: string,
lang?: string,
) => Promise<{
success: boolean;
subtitles?: Array<{ id: string; startMs: number; endMs: number; text: string }>;
error?: string;
}>;
};
}

Expand Down
62 changes: 62 additions & 0 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
Expand Down Expand Up @@ -781,4 +782,65 @@ export function registerIpcHandlers(
return { success: false, error: String(error) };
}
});

ipcMain.handle("generate-subtitles", async (_, videoPath: string, lang = "pt") => {
const scriptPath = path.join(app.getAppPath(), "scripts", "extract-subtitles.mjs");
try {
await fs.access(scriptPath);
} catch {
return { success: false, error: "extract-subtitles.mjs script not found" };
}

const nodeBin =
process.env.NODE_BINARY ||
(process.platform === "win32" ? "node.exe" : "node");

return new Promise<{
success: boolean;
subtitles?: Array<{ id: string; startMs: number; endMs: number; text: string }>;
error?: string;
}>((resolve) => {
const child = execFile(
nodeBin,
[scriptPath, videoPath, "--json", "--lang", lang],
{
maxBuffer: 10 * 1024 * 1024,
timeout: 300_000,
cwd: app.getAppPath(),
},
(error, stdout, stderr) => {
if (error) {
console.error("Subtitle generation error:", stderr || error.message);
resolve({ success: false, error: error.message });
return;
}
try {
const jsonStart = stdout.indexOf("{");
const jsonEnd = stdout.lastIndexOf("}");
if (jsonStart === -1 || jsonEnd === -1) {
resolve({ success: false, error: "No JSON output from script" });
return;
}
const config = JSON.parse(stdout.slice(jsonStart, jsonEnd + 1));
const items = config?.subtitles?.items ?? [];
const FPS = 30;
const subtitles = items.map(
(item: { text: string; startFrame: number; endFrame: number }, idx: number) => ({
id: `sub-${idx + 1}`,
startMs: Math.round((item.startFrame / FPS) * 1000),
endMs: Math.round((item.endFrame / FPS) * 1000),
text: item.text,
}),
);
resolve({ success: true, subtitles });
} catch (parseError) {
resolve({ success: false, error: `Failed to parse output: ${String(parseError)}` });
}
},
);
child.stderr?.on("data", (data) => {
console.log("[subtitle-gen]", String(data).trim());
});
});
});
}
3 changes: 3 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
setLocale: (locale: string) => {
return ipcRenderer.invoke("set-locale", locale);
},
generateSubtitles: (videoPath: string, lang?: string) => {
return ipcRenderer.invoke("generate-subtitles", videoPath, lang ?? "pt");
},
setMicrophoneExpanded: (expanded: boolean) => {
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
},
Expand Down
Loading
Loading