Files
RUWIO/dev_docs/electron-rendering-integration-plan.md
T
thandClaude Opus 4.8 518d43855b Add desktop/server MP4 export pipeline, hardening, and render tests (v1.3.0)
Desktop MP4 export renders frames from a hidden BrowserWindow via
capturePage, pipes PNGs to a bundled ffmpeg (libx264/yuv420p/+faststart),
and writes the file through an atomic temp-file swap. It shares scene
validation (shared/renderValidation.cjs) and the render.html renderer with
the standalone Puppeteer server, which is hardened in parallel.

Render worker & main (electron/):
- abortable render loop with an inactivity watchdog and cancel plumbing
- atomic temp-file + rename so a cancel/failure never truncates a
  pre-existing target (#7); stdin EPIPE / early-exit / backpressure-hang
  handled (#8)
- HiDPI frames normalized to canvas size (#10); odd dimensions padded to
  even (#13, also server); backgroundThrottling disabled (#12)
- reject canvases larger than the display (#11); single-render guard;
  isolated render session that blocks network requests (#5)

Tests:
- Electron harnesses (scripts/verify-*.cjs) for capture, export, cancel,
  timeout, assets, background, capture-fidelity, resolution, odd-dims,
  aggregated by verify-desktop-render.cjs
- server smoke + e2e and shared validation unit tests
- headless hook (src/devtools/headless.ts) to run the in-app suite under
  browser/electron hosts; pixel-diff util + capture fixtures (test/)
- dev_docs plans: integration, fixes (#7-#13), and the e2e/capture strategy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:41:25 +02:00

11 KiB

Electron Rendering Integration Plan

Date: 2026-06-05

Verdict

Yes, the MP4 rendering part can be included in the Electron app.

The current render server is already made from desktop-compatible pieces:

  • Node orchestration in server/server.js
  • a browser render surface in server/render.html
  • frame capture through Chromium
  • MP4 encoding through ffmpeg
  • progress, cancellation, temp files, validation, and cleanup around the job

The main caveat is that the server cannot be bundled as-is and expected to work in a packaged desktop app. It currently assumes:

  • puppeteer-core is installed in the server package
  • Chromium exists at PUPPETEER_EXECUTABLE_PATH or /usr/bin/chromium
  • ffmpeg exists on PATH
  • the Vue app talks to http://localhost:6000 with VITE_RENDER_SERVER
  • Electron Builder packages only dist/, dist-electron/, and root package.json

So the feature is feasible, but the right implementation is to move the render job into the Electron main process behind IPC, not to expose an HTTP render server from the desktop app.

Use Electron as the local render host:

  • Vue renderer keeps owning project state and export UI.
  • Preload exposes a narrow desktop render API on window.ruwioDesktop.
  • Electron main validates render requests, starts/cancels jobs, captures frames, runs ffmpeg, and returns a saved MP4 path or file bytes.
  • The standalone server/ package remains available for web or self-hosted server deployments.

Preferred render surface:

  • Use a hidden/offscreen Electron BrowserWindow to load a packaged render page.
  • Call webContents.executeJavaScript or webContents.send to initialize the scene and seek each frame.
  • Use webContents.capturePage() for PNG frame capture.
  • Pipe PNG frames into ffmpeg the same way server/server.js does today.

This avoids bundling a second Chromium through Puppeteer. Electron already ships Chromium, so the desktop build only needs to solve ffmpeg packaging.

Fallback/spike option:

  • Start the existing Express render server as a child process bound to 127.0.0.1 with a random token.
  • Point the renderer at that local URL.
  • This is useful for a quick feasibility spike, but it keeps the localhost server, token plumbing, Puppeteer, and external Chromium dependency in the desktop app. It should not be the final architecture.

Dependency Strategy

Chromium

Do not add a separate Chromium binary for the first native implementation. Use Electron's bundled Chromium through a hidden render window.

Only keep Puppeteer for the standalone server unless a spike proves Electron capture cannot match render quality or performance.

ffmpeg

ffmpeg is the real packaged-app dependency.

Implementation options:

  1. Bundle per-platform ffmpeg binaries as extraResources.
  2. Use a maintained npm package that supplies platform-specific binaries.
  3. Support a user-configured/system ffmpeg path as a fallback.

Recommended first production path:

  • Add an internal resolver such as electron/ffmpegPath.cts.
  • In development, prefer process.env.FFMPEG_PATH, then ffmpeg on PATH.
  • In packaged builds, prefer process.resourcesPath/bin/<platform>/ffmpeg.
  • Show a friendly export error if no usable binary is found.

Implementation Plan

Phase 1: Extract Shared Render Validation

Goal: keep server and desktop limits identical.

Tasks:

  1. Move request validation and playback range helpers out of server/server.js into a shared module.
  2. Keep the module Node-compatible and browser-safe where practical.
  3. Export:
    • validateRenderRequest(scene, options)
    • playbackRange(scene)
    • render limits and allowed ffmpeg presets
  4. Update server/server.js to use the shared validator.
  5. Add focused tests for invalid dimensions, fps, duration, frame count, layers, keyframes, crf, and preset.

Acceptance:

  • Existing server behavior is unchanged.
  • Desktop rendering can reuse the same validation before launching any render surface or child process.

Phase 2: Add Desktop Render IPC Contract

Goal: let the Vue app request MP4 rendering without knowing whether the backend is HTTP or Electron.

Tasks:

  1. Extend src/types/desktop.d.ts with a render API:
renderMp4(options: {
  scene: Scene
  fps?: number
  crf?: number
  preset?: string
  suggestedName?: string
}): Promise<{ path: string } | null>
onRenderProgress(callback: (event: {
  jobId: string
  progress: number
  done: boolean
  error?: string
}) => void): () => void
cancelRender(jobId: string): Promise<void>
  1. Add preload wrappers that call ipcRenderer.invoke and subscribe to progress events.
  2. Add main-process handlers:
    • ruwio:render-mp4
    • ruwio:render-cancel
  3. Validate every IPC payload in main before starting a job.
  4. Keep job IDs and abort controllers in main, not in the Vue renderer.

Acceptance:

  • A renderer call can start a fake/stub render and receive progress.
  • Cancel reaches the main process.
  • Invalid payloads fail before any child process or hidden window is created.

Phase 3: Build The Electron Render Worker

Goal: replace Puppeteer with Electron's own Chromium for desktop exports.

Tasks:

  1. Create an Electron-only render worker module, for example electron/renderWorker.cts.
  2. Package a render page:
    • first pass: reuse server/render.html as an extra resource
    • later pass: consider generating a render page from the shared browser renderer to reduce drift
  3. Create a hidden BrowserWindow per job or a serialized reusable worker window:
    • show: false
    • webPreferences.sandbox: true
    • no Node integration
    • restricted navigation
  4. Load the render page from a local file URL.
  5. Initialize the scene.
  6. For each frame:
    • seek to the target time
    • wait for the render function to settle
    • capture the page or canvas bounds
    • write PNG bytes to ffmpeg.stdin
    • emit progress over IPC
  7. On cancel or timeout:
    • abort the frame loop
    • kill ffmpeg
    • close the worker window
    • remove temp files

Acceptance:

  • A tiny scene renders to MP4 in npm run desktop:dev.
  • Progress updates reach ExportDialog.
  • Cancel stops capture and ffmpeg.
  • Temp directories are removed on success, failure, and cancel.

Phase 4: Package ffmpeg

Goal: make MP4 export work in packaged desktop builds.

Tasks:

  1. Choose the binary source:
    • committed/release-managed binaries under a vendor/ or bin/ folder, or
    • npm package binaries copied into Electron Builder resources
  2. Add Electron Builder extraResources entries for:
    • Linux ffmpeg
    • Windows ffmpeg.exe
    • macOS ffmpeg
    • render worker HTML/assets
  3. Add ffmpeg path resolution for dev and packaged builds.
  4. Verify executable permissions on macOS/Linux packaged output.
  5. Document licensing obligations for the selected ffmpeg build.

Acceptance:

  • npm run desktop:pack includes the render page and ffmpeg.
  • The unpacked app exports MP4 without a separately installed render server.
  • Missing ffmpeg produces a clear user-facing error.

Phase 5: Switch ExportDialog To Desktop Rendering

Goal: use native rendering in Electron and keep the current server path for the web app.

Tasks:

  1. Add src/lib/desktopRenderExport.ts.
  2. In ExportDialog.vue, route MP4 export as:
    • if window.ruwioDesktop?.renderMp4 exists: use desktop IPC render
    • else: use current downloadFromServer
  3. Reuse current progress UI and cancellation behavior.
  4. Save the finished MP4 through a native save dialog from main.
  5. Keep VITE_RENDER_SERVER support for browser builds and remote/self-hosted workflows.

Acceptance:

  • Desktop users do not need cd server && npm run start.
  • Browser users can still use the external render server.
  • The MP4 button reports a helpful error when no server is configured in browser mode.

Phase 6: Keep Standalone Server Working

Goal: avoid breaking existing server deployments.

Tasks:

  1. Keep server/server.js as an Express wrapper around shared validation/render helpers where useful.
  2. Keep server/render.html or replace it only after desktop and server render outputs are compared.
  3. Preserve /health, /render, /events, /file, and DELETE behavior.
  4. Add a smoke test or manual check for server MP4 export after shared refactors.

Acceptance:

  • cd server && npm run start still starts the local render server.
  • The Vue app can still render through VITE_RENDER_SERVER and VITE_RENDER_TOKEN.

Phase 7: Verification Matrix

Run these before calling the integration complete:

  • npm run build
  • npm run desktop:pack
  • npm run test:render-server
  • npm run test:render-server:e2e
  • desktop dev MP4 export of a small scene
  • desktop packaged MP4 export of a small scene
  • cancel an MP4 export after progress starts
  • export a scene with image assets
  • export a scene with text/fonts
  • export a scene at non-default fps
  • export a scene with hidden layers skipped
  • server-mode MP4 export still works

Manual platform checks:

  • Linux AppImage
  • Windows NSIS build
  • macOS DMG build

Risks And Mitigations

  • ffmpeg packaging varies by platform: solve with explicit binary resolution and an unpacked-app smoke check before installer work.
  • render drift between server/render.html and src/lib/render.ts: keep parity tests, then consider generating the desktop/server render page from shared code.
  • large scenes can freeze the desktop app: keep render work in main-controlled worker windows, serialize jobs initially, enforce the same render limits as the server.
  • IPC can move large payloads: scenes are JSON-sized today, but avoid sending frame buffers over IPC; frame capture and ffmpeg piping should stay in main.
  • untrusted project assets reach Chromium: keep the hidden render window sandboxed, restrict navigation, block external network requests unless explicitly allowed, and keep existing project validation work moving.

Suggested First Pull Request

Keep the first PR small and proof-oriented:

  1. Extract shared render validation from server/server.js.
  2. Add a stubbed desktop render IPC contract.
  3. Add desktopRenderExport.ts and route the MP4 button to desktop IPC when available.
  4. Return a fake progress/done result in development or behind a temporary flag.

That proves the app integration and cancellation shape before dealing with frame capture and ffmpeg packaging.

Suggested Second Pull Request

Implement the real Electron render worker:

  1. Hidden render window.
  2. Frame capture loop.
  3. ffmpeg stdin piping.
  4. Temp directory cleanup.
  5. Dev-only ffmpeg path resolution.

Suggested Third Pull Request

Make it shippable:

  1. Package ffmpeg and render worker assets.
  2. Verify desktop:pack.
  3. Add packaged-app manual test notes.
  4. Document ffmpeg licensing and update README desktop export notes.