Skip to content

feat(electron): add --export CLI flag for headless project rendering#628

Closed
agentiknet wants to merge 1 commit into
siddharthvaddem:mainfrom
agentiknet:feat/headless-cli-export
Closed

feat(electron): add --export CLI flag for headless project rendering#628
agentiknet wants to merge 1 commit into
siddharthvaddem:mainfrom
agentiknet:feat/headless-cli-export

Conversation

@agentiknet
Copy link
Copy Markdown

@agentiknet agentiknet commented May 21, 2026

Summary

Adds an --export CLI flag that lets OpenScreen render a .openscreen project to MP4/GIF entirely headlessly — no window, no save dialog, no user interaction. Enables batch exports from shell scripts / CI.

Usage

Openscreen --export <project.openscreen> \
           --output <out.mp4|gif> \
           [--format mp4|gif] \
           [--quality good|medium|source]

What it does

When --export is set:

  1. Forces HEADLESS=true so createEditorWindow uses show: false (the existing offscreen path).
  2. Hides the Dock icon on macOS (app.dock.hide()).
  3. Reads the project file.
  4. Overrides the pick-export-save-path IPC to auto-return the --output path (no file dialog).
  5. Overrides the write-export-to-path IPC to write the rendered blob and quit cleanly.
  6. Boots the editor window (offscreen), sized to 2560×1440 so the React layout fits.
  7. Sends a trigger-headless-export IPC to the renderer.

The renderer-side hook in VideoEditor.tsx:

  1. Scans project.editor.annotationRegions for unique fontFamily references and preloads each via addCustomFont() so ctx.font doesn't silently fall back to sans-serif (Canvas font rendering doesn't error on missing fonts — just substitutes).
  2. Calls applyLoadedProject() to apply trim/zoom/annotation/cursor regions.
  3. Waits for videoPlaybackRef.current.video.readyState >= 2 and duration > 0.
  4. Calls handleExport(settings) directly with the requested format/quality.

Files changed

  • electron/main.ts — CLI parsing + runHeadlessExport() driver
  • electron/preload.ts — exposes onHeadlessExportTrigger() with a typed payload
  • electron/electron-env.d.ts — TS types for the new API
  • src/components/video-editor/VideoEditor.tsx — renderer-side listener (font preload + project apply + handleExport)

262 lines added across 4 files. No upstream code paths touched outside the new branches.

Test plan

  • Single export — ./node_modules/.bin/electron . --export X.openscreen --output Y.mp4 produces a valid MP4 with trim/zoom/annotation regions applied.
  • Batch — 5 projects exported sequentially, all completed without hangs.
  • Annotation font (Nunito) renders correctly after the font-preload step (was previously falling back to sans-serif on a fresh Electron session).
  • No window appears, no Dock bounce, no focus steal.
  • GIF format path — not yet exercised but uses the same code path.
  • Linux / Windows — only tested on macOS so far.

Why

The existing GIF e2e test (tests/e2e/gif-export.spec.ts) shows the headless export pipeline works, but driving it requires Playwright, knowing the React testIds, and clicking UI elements that can move between versions. A first-class CLI is more useful and is what most "render this for me" automation actually wants.

Opens a hidden editor window, applies a .openscreen project, calls
handleExport() directly, and writes the rendered MP4/GIF to the path
passed via --output. No UI is shown, no save dialog opens.

Usage:
  Openscreen --export <project.openscreen> --output <out.mp4|gif>
             [--format mp4|gif] [--quality good|medium|source]

Implementation:
- main.ts parses CLI args, forces HEADLESS=true, overrides the
  pick-export-save-path + write-export-to-path IPCs to route the blob
  to --output, then signals the renderer via "trigger-headless-export".
- preload.ts exposes onHeadlessExportTrigger() with a typed payload.
- VideoEditor.tsx listens for the trigger, scans annotations for
  fontFamily references and preloads them via addCustomFont() so
  ctx.font doesn't silently fall back to sans-serif, applies the
  project state, waits for video.readyState>=2, then calls
  handleExport(settings) directly.

Enables batch rendering of OpenScreen projects from CI / shell scripts.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR adds command-line driven video export to Electron, skipping the normal UI. CLI args (--export, --output, --format, --quality) trigger invisible editor boot, font preloading, video readiness polling, and direct-to-file output routing via IPC handler replacement and a 10-minute timeout failsafe.

Changes

Headless Export Feature

Layer / File(s) Summary
HeadlessExportPayload type definition
electron/preload.ts
HeadlessExportPayload interface exports the contract with projectPath, project, format ("mp4" | "gif"), quality, and outputPath.
Electron API bridge contract
electron/electron-env.d.ts
Window.electronAPI.onHeadlessExportTrigger method added to type definitions, accepting a callback and returning an unsubscribe function.
Preload IPC bridge
electron/preload.ts
onHeadlessExportTrigger implementation listens to "trigger-headless-export" IPC channel, forwards typed payload to callback, and returns a listener cleanup function.
CLI argument parsing and startup branching
electron/main.ts
Parses --export, --output, --format, --quality arguments; sets process.env.HEADLESS = "true"; branches app startup to run runHeadlessExport() when enabled.
Headless export execution engine
electron/main.ts
runHeadlessExport() hides Dock (macOS), loads project JSON, replaces IPC handlers to write export blob directly to --output path, boots editor invisibly, resizes for layout, sends trigger event after load, and enforces 10-minute timeout failsafe.
Renderer headless export handler
src/components/video-editor/VideoEditor.tsx
Headless hook subscribes to IPC trigger, preloads custom fonts from annotations, applies project state, polls video decode-readiness, builds ExportSettings from payload, triggers export with GIF defaults (30 fps, loop, medium preset, 1280x720).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Suggested reviewers

  • siddharthvaddem

Poem

📺 export without the window pane,
CLI whispers through the main,
fonts preload, video primes tight,
headless renders through the night
✨ no ui, pure command delight

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a CLI flag for headless project export, which is the primary feature introduced across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description covers all template sections with clear motivation, detailed implementation notes, files changed, and testing status.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59fc8f1e98

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread electron/main.ts

if (IS_HEADLESS_EXPORT) {
// Force HEADLESS=true so createEditorWindow uses `show: false`.
process.env.HEADLESS = "true";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize headless mode before window module reads env

Setting process.env.HEADLESS here is too late for the --export path because main.ts has already imported createEditorWindow from electron/windows.ts, and that module snapshots const HEADLESS = process.env["HEADLESS"] === "true" at import time. In runs where the env var was not pre-set, the editor window is still constructed with show: true, so the supposedly headless CLI export can still open/focus a UI window.

Useful? React with 👍 / 👎.

Comment on lines +1973 to +1976
await applyLoadedProjectRef.current(
payload.project as Parameters<typeof applyLoadedProject>[0],
payload.projectPath,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail headless export when project restore is invalid

applyLoadedProject returns false for invalid project payloads, but this headless path ignores that result and proceeds to export anyway. That means a malformed .openscreen file can lead to exporting stale/default loaded media (or waiting until timeout) instead of failing fast for bad input, which is risky for batch/CI automation correctness.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
electron/main.ts (1)

480-486: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Headless mode still boots UI chrome before branching.

app.dock?.show(), createTray(), updateTrayMenu(), and setupApplicationMenu() all run before runHeadlessExport(). So --export is not actually UI-free on startup; Windows/Linux can still get a tray icon, and macOS can briefly promote the app before you hide it again.

🪟 Keep the UI bootstrap out of the headless path
-	if (process.platform === "darwin") {
+	if (process.platform === "darwin" && !IS_HEADLESS_EXPORT) {
 		app.dock?.show();
 	}
 	...
-	createTray();
-	updateTrayMenu();
-	setupApplicationMenu();
+	if (!IS_HEADLESS_EXPORT) {
+		createTray();
+		updateTrayMenu();
+		setupApplicationMenu();
+	}

Also applies to: 551-553, 585-589

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/main.ts` around lines 480 - 486, The startup currently runs UI
bootstrap (app.dock?.show(), createTray(), updateTrayMenu(),
setupApplicationMenu()) before checking for headless/export mode so the UI
briefly appears; change the app.whenReady() flow to first call
runHeadlessExport() and, only if it returns/indicates non-headless, perform the
UI setup calls (app.dock?.show(), createTray(), updateTrayMenu(),
setupApplicationMenu()). Locate these symbols inside the
app.whenReady().then(async () => { ... }) block and guard or reorder them so the
headless path exits/returns before any dock/tray/menu code runs; apply the same
conditional guard to the other occurrences noted (around the blocks at the later
sites).
🧹 Nitpick comments (1)
electron/electron-env.d.ts (1)

237-245: ⚡ Quick win

Nit: reuse HeadlessExportPayload here instead of inlining it.

This payload shape now exists in two places, which is lowkey risky for a main↔preload↔renderer contract. Cleaner to point this signature at one shared type so it can't drift.

♻️ Cleaner type wiring
+type HeadlessExportPayload = import("./preload").HeadlessExportPayload;
+
 interface Window {
 	electronAPI: {
 		...
 		onHeadlessExportTrigger: (
-			callback: (payload: {
-				projectPath: string;
-				project: unknown;
-				format: "mp4" | "gif";
-				quality: "good" | "medium" | "source";
-				outputPath: string;
-			}) => void,
+			callback: (payload: HeadlessExportPayload) => void,
 		) => () => void;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/electron-env.d.ts` around lines 237 - 245, The
onHeadlessExportTrigger callback currently inlines the payload shape; update its
signature to reuse the existing HeadlessExportPayload type instead of
duplicating the structure. Locate the onHeadlessExportTrigger declaration and
replace the inline payload object with (payload: HeadlessExportPayload) => void
so the main↔preload↔renderer contract points to the single shared
HeadlessExportPayload type (ensure the type is imported or available in the same
scope).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/main.ts`:
- Around line 45-49: Ensure headless export args are validated and fail fast:
when HEADLESS_EXPORT_PROJECT (from getCliArg("export")) is present require
HEADLESS_EXPORT_OUTPUT (from getCliArg("output")) and validate
HEADLESS_EXPORT_FORMAT and HEADLESS_EXPORT_QUALITY values before setting
IS_HEADLESS_EXPORT; if format is not exactly "mp4" or "gif" or quality is not
"good" | "medium" | "source", log a clear error (including the bad value and
accepted values) and exit process with non‑zero status so the headless path is
not enabled accidentally.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1972-1977: The callback that handles payload.project must not
swallow headless setup failures: check the boolean result of applyLoadedProject
(used in VideoEditor.applyLoadedProject via applyLoadedProjectRef.current) and
if it returns false or throws, immediately report the failure to the main
process via a small IPC (e.g., reportHeadlessExportFailure) instead of merely
logging and returning; update the same pattern in the other callback range (the
block spanning the nearby lines 1981–2012) to call the preload IPC on failure
and rethrow or abort the export flow so the app exits with a non-zero code, and
add corresponding IPC handlers in preload/main to receive
reportHeadlessExportFailure and exit/return failure to CI.
- Around line 1992-2003: The headless GIF export hardcodes gifConfig
width/height and sizePreset (1280x720, "medium") in ExportSettings, causing
incorrect aspect ratios for portrait/square projects; update the headless branch
that sets payload.format === "gif" to compute gifConfig using the same sizing
logic used by the interactive path (reuse the dimension calculation used in
applyLoadedProject / handleExport), pulling the current cropRegion, aspectRatio,
and gifSizePreset values instead of hardcoded values, and populate
gifConfig.width, gifConfig.height and sizePreset accordingly so CLI exports
match editor state.

---

Outside diff comments:
In `@electron/main.ts`:
- Around line 480-486: The startup currently runs UI bootstrap
(app.dock?.show(), createTray(), updateTrayMenu(), setupApplicationMenu())
before checking for headless/export mode so the UI briefly appears; change the
app.whenReady() flow to first call runHeadlessExport() and, only if it
returns/indicates non-headless, perform the UI setup calls (app.dock?.show(),
createTray(), updateTrayMenu(), setupApplicationMenu()). Locate these symbols
inside the app.whenReady().then(async () => { ... }) block and guard or reorder
them so the headless path exits/returns before any dock/tray/menu code runs;
apply the same conditional guard to the other occurrences noted (around the
blocks at the later sites).

---

Nitpick comments:
In `@electron/electron-env.d.ts`:
- Around line 237-245: The onHeadlessExportTrigger callback currently inlines
the payload shape; update its signature to reuse the existing
HeadlessExportPayload type instead of duplicating the structure. Locate the
onHeadlessExportTrigger declaration and replace the inline payload object with
(payload: HeadlessExportPayload) => void so the main↔preload↔renderer contract
points to the single shared HeadlessExportPayload type (ensure the type is
imported or available in the same scope).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8429088a-a7f9-4e41-9ad5-df298c1c7cb2

📥 Commits

Reviewing files that changed from the base of the PR and between 1235052 and 59fc8f1.

📒 Files selected for processing (4)
  • electron/electron-env.d.ts
  • electron/main.ts
  • electron/preload.ts
  • src/components/video-editor/VideoEditor.tsx

Comment thread electron/main.ts
Comment on lines +45 to +49
const HEADLESS_EXPORT_PROJECT = getCliArg("export");
const HEADLESS_EXPORT_OUTPUT = getCliArg("output");
const HEADLESS_EXPORT_FORMAT = (getCliArg("format") ?? "mp4") as "mp4" | "gif";
const HEADLESS_EXPORT_QUALITY = (getCliArg("quality") ?? "good") as "good" | "medium" | "source";
const IS_HEADLESS_EXPORT = Boolean(HEADLESS_EXPORT_PROJECT && HEADLESS_EXPORT_OUTPUT);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the headless CLI args before enabling this path.

Right now --export without --output quietly falls back to the normal app, and unknown --format / --quality values are just cast through. In automation that's kinda cursed: a typo can open the UI or send MP4 bytes to a .gif path. Fail fast here instead of treating bad args as valid.

🛠️ Possible guardrail
 const HEADLESS_EXPORT_PROJECT = getCliArg("export");
 const HEADLESS_EXPORT_OUTPUT = getCliArg("output");
-const HEADLESS_EXPORT_FORMAT = (getCliArg("format") ?? "mp4") as "mp4" | "gif";
-const HEADLESS_EXPORT_QUALITY = (getCliArg("quality") ?? "good") as "good" | "medium" | "source";
-const IS_HEADLESS_EXPORT = Boolean(HEADLESS_EXPORT_PROJECT && HEADLESS_EXPORT_OUTPUT);
+const rawHeadlessExportFormat = getCliArg("format") ?? "mp4";
+const rawHeadlessExportQuality = getCliArg("quality") ?? "good";
+const IS_HEADLESS_EXPORT =
+	HEADLESS_EXPORT_PROJECT !== undefined || HEADLESS_EXPORT_OUTPUT !== undefined;
+
+if (IS_HEADLESS_EXPORT && (!HEADLESS_EXPORT_PROJECT || !HEADLESS_EXPORT_OUTPUT)) {
+	throw new Error("`--export` and `--output` must be provided together");
+}
+
+if (rawHeadlessExportFormat !== "mp4" && rawHeadlessExportFormat !== "gif") {
+	throw new Error(`Unsupported export format: ${rawHeadlessExportFormat}`);
+}
+
+if (
+	rawHeadlessExportQuality !== "good" &&
+	rawHeadlessExportQuality !== "medium" &&
+	rawHeadlessExportQuality !== "source"
+) {
+	throw new Error(`Unsupported export quality: ${rawHeadlessExportQuality}`);
+}
+
+const HEADLESS_EXPORT_FORMAT = rawHeadlessExportFormat;
+const HEADLESS_EXPORT_QUALITY = rawHeadlessExportQuality;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const HEADLESS_EXPORT_PROJECT = getCliArg("export");
const HEADLESS_EXPORT_OUTPUT = getCliArg("output");
const HEADLESS_EXPORT_FORMAT = (getCliArg("format") ?? "mp4") as "mp4" | "gif";
const HEADLESS_EXPORT_QUALITY = (getCliArg("quality") ?? "good") as "good" | "medium" | "source";
const IS_HEADLESS_EXPORT = Boolean(HEADLESS_EXPORT_PROJECT && HEADLESS_EXPORT_OUTPUT);
const HEADLESS_EXPORT_PROJECT = getCliArg("export");
const HEADLESS_EXPORT_OUTPUT = getCliArg("output");
const rawHeadlessExportFormat = getCliArg("format") ?? "mp4";
const rawHeadlessExportQuality = getCliArg("quality") ?? "good";
const IS_HEADLESS_EXPORT =
HEADLESS_EXPORT_PROJECT !== undefined || HEADLESS_EXPORT_OUTPUT !== undefined;
if (IS_HEADLESS_EXPORT && (!HEADLESS_EXPORT_PROJECT || !HEADLESS_EXPORT_OUTPUT)) {
throw new Error("`--export` and `--output` must be provided together");
}
if (rawHeadlessExportFormat !== "mp4" && rawHeadlessExportFormat !== "gif") {
throw new Error(`Unsupported export format: ${rawHeadlessExportFormat}`);
}
if (
rawHeadlessExportQuality !== "good" &&
rawHeadlessExportQuality !== "medium" &&
rawHeadlessExportQuality !== "source"
) {
throw new Error(`Unsupported export quality: ${rawHeadlessExportQuality}`);
}
const HEADLESS_EXPORT_FORMAT = rawHeadlessExportFormat;
const HEADLESS_EXPORT_QUALITY = rawHeadlessExportQuality;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/main.ts` around lines 45 - 49, Ensure headless export args are
validated and fail fast: when HEADLESS_EXPORT_PROJECT (from getCliArg("export"))
is present require HEADLESS_EXPORT_OUTPUT (from getCliArg("output")) and
validate HEADLESS_EXPORT_FORMAT and HEADLESS_EXPORT_QUALITY values before
setting IS_HEADLESS_EXPORT; if format is not exactly "mp4" or "gif" or quality
is not "good" | "medium" | "source", log a clear error (including the bad value
and accepted values) and exit process with non‑zero status so the headless path
is not enabled accidentally.

Comment on lines +1972 to +1977
if (payload.project) {
await applyLoadedProjectRef.current(
payload.project as Parameters<typeof applyLoadedProject>[0],
payload.projectPath,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't swallow headless setup failures here.

applyLoadedProject() can return false, the 60s readiness loop can expire, and handleExport() can still no-op on “video not ready”. This callback just logs and returns, so the main process only learns about the failure via its 10-minute watchdog. For CI, that's lowkey brutal — this needs an explicit failure path back to main.

🚨 Fail fast instead of timing out
-				if (payload.project) {
-					await applyLoadedProjectRef.current(
+				if (payload.project) {
+					const applied = await applyLoadedProjectRef.current(
 						payload.project as Parameters<typeof applyLoadedProject>[0],
 						payload.projectPath,
 					);
+					if (!applied) {
+						throw new Error("Project could not be loaded for headless export");
+					}
 				}
 
-				const deadline = Date.now() + 60_000;
+				const deadline = Date.now() + 60_000;
+				let videoReady = false;
 				while (Date.now() < deadline) {
 					const v = videoPlaybackRefRef.current?.video;
-					if (v && v.readyState >= 2 && v.duration > 0) break;
+					if (v && v.readyState >= 2 && v.duration > 0) {
+						videoReady = true;
+						break;
+					}
 					await new Promise((r) => setTimeout(r, 200));
 				}
+				if (!videoReady) {
+					throw new Error("Video never became ready for headless export");
+				}
 				...
 			} catch (err) {
 				console.error("[headless-export] failed:", err);
+				window.electronAPI.reportHeadlessExportFailure?.(
+					err instanceof Error ? err.message : String(err),
+				);
 			}

You'll need a tiny preload/main IPC for reportHeadlessExportFailure so the app can exit immediately with a non-zero code.

Also applies to: 1981-2012

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1972 - 1977, The
callback that handles payload.project must not swallow headless setup failures:
check the boolean result of applyLoadedProject (used in
VideoEditor.applyLoadedProject via applyLoadedProjectRef.current) and if it
returns false or throws, immediately report the failure to the main process via
a small IPC (e.g., reportHeadlessExportFailure) instead of merely logging and
returning; update the same pattern in the other callback range (the block
spanning the nearby lines 1981–2012) to call the preload IPC on failure and
rethrow or abort the export flow so the app exits with a non-zero code, and add
corresponding IPC handlers in preload/main to receive
reportHeadlessExportFailure and exit/return failure to CI.

Comment on lines +1992 to +2003
const settings: ExportSettings = {
format: payload.format,
quality: payload.format === "mp4" ? payload.quality : undefined,
gifConfig:
payload.format === "gif"
? {
frameRate: 30 as GifFrameRate,
loop: true,
sizePreset: "medium" as GifSizePreset,
width: 1280,
height: 720,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Headless GIF export ignores the project's aspect ratio.

This always forces 1280x720 + "medium", so portrait and square projects will export as 16:9 in CLI mode even after you just applied the saved editor state. Reuse the same GIF dimension calculation as the interactive path once the video is ready.

🎞️ Match the normal GIF sizing path
-				const settings: ExportSettings = {
+				const video = videoPlaybackRefRef.current?.video;
+				const sourceWidth = video?.videoWidth || 1920;
+				const sourceHeight = video?.videoHeight || 1080;
+				const effectiveSourceDimensions = calculateEffectiveSourceDimensions(
+					sourceWidth,
+					sourceHeight,
+					cropRegion,
+				);
+				const aspectRatioValue =
+					aspectRatio === "native"
+						? getNativeAspectRatioValue(sourceWidth, sourceHeight, cropRegion)
+						: getAspectRatioValue(aspectRatio);
+				const gifDimensions = calculateOutputDimensions(
+					effectiveSourceDimensions.width,
+					effectiveSourceDimensions.height,
+					gifSizePreset,
+					GIF_SIZE_PRESETS,
+					aspectRatioValue,
+				);
+
+				const settings: ExportSettings = {
 					format: payload.format,
 					quality: payload.format === "mp4" ? payload.quality : undefined,
 					gifConfig:
 						payload.format === "gif"
 							? {
 									frameRate: 30 as GifFrameRate,
 									loop: true,
-									sizePreset: "medium" as GifSizePreset,
-									width: 1280,
-									height: 720,
+									sizePreset: gifSizePreset,
+									width: gifDimensions.width,
+									height: gifDimensions.height,
 								}
 							: undefined,
 				};

That probably wants refs for cropRegion, aspectRatio, and gifSizePreset the same way you're already stabilizing applyLoadedProject and handleExport.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1992 - 2003, The
headless GIF export hardcodes gifConfig width/height and sizePreset (1280x720,
"medium") in ExportSettings, causing incorrect aspect ratios for portrait/square
projects; update the headless branch that sets payload.format === "gif" to
compute gifConfig using the same sizing logic used by the interactive path
(reuse the dimension calculation used in applyLoadedProject / handleExport),
pulling the current cropRegion, aspectRatio, and gifSizePreset values instead of
hardcoded values, and populate gifConfig.width, gifConfig.height and sizePreset
accordingly so CLI exports match editor state.

@siddharthvaddem
Copy link
Copy Markdown
Owner

what is even the motivation behind this?

@agentiknet
Copy link
Copy Markdown
Author

what is even the motivation behind this?

Hey !
On my side, I'm using Gemini to generate programmatically timestamp, annotation, cut etc. I think you have a great potential here for demo Automation.

So I was just missing that CLI based output, as otherwise everything is already ready in your codebase.

Happy to discuss more about it.

@siddharthvaddem
Copy link
Copy Markdown
Owner

Sorry but will have to close this - not in scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants