feat: integrate Electron for desktop app
- Add electron-vite + Electron Forge tooling - Create Electron main process with embedded Express server - Create preload script with native dialog IPC bridge - Refactor server.ts to export createExpressApp() (dual web/electron) - Adapt renderQueue.ts for packaged binaries + pre-built bundle - Add ensureBrowser() for Chrome Headless Shell pre-download - Add scripts/bundle-remotion.js for packaging - Data persists in ~/Library/Application Support/Bradly/ - Web mode preserved via npm run dev:web
This commit is contained in:
@@ -53,7 +53,7 @@ export interface RenderJobCreateParams {
|
||||
// ═══ Constants ═══
|
||||
|
||||
const MAX_CONCURRENT = 1; // Remotion renders are CPU-intensive
|
||||
const RENDERS_DIR = path.join(process.cwd(), 'renders');
|
||||
const RENDERS_DIR = process.env.BRADLY_RENDERS_DIR || path.join(process.cwd(), 'renders');
|
||||
|
||||
// Ensure renders directory exists
|
||||
if (!fs.existsSync(RENDERS_DIR)) {
|
||||
@@ -109,6 +109,14 @@ export function addSSEClient(clientId: string, send: (data: string) => void): ()
|
||||
// ═══ Bundle Management ═══
|
||||
|
||||
async function ensureBundle(): Promise<string> {
|
||||
// In packaged Electron, use pre-built bundle from app resources
|
||||
if (process.env.BRADLY_REMOTION_BUNDLE) {
|
||||
const prebuilt = process.env.BRADLY_REMOTION_BUNDLE;
|
||||
if (fs.existsSync(prebuilt)) return prebuilt;
|
||||
throw new Error(`Pre-built Remotion bundle not found at: ${prebuilt}`);
|
||||
}
|
||||
|
||||
// Development: bundle on demand
|
||||
if (bundlePath) return bundlePath;
|
||||
if (isBundling) {
|
||||
// Wait for existing bundle
|
||||
@@ -127,7 +135,8 @@ async function ensureBundle(): Promise<string> {
|
||||
|
||||
try {
|
||||
const { bundle } = await import('@remotion/bundler');
|
||||
const entryPoint = path.join(process.cwd(), 'src', 'Root.tsx');
|
||||
const entryPoint = process.env.BRADLY_REMOTION_ENTRY
|
||||
|| path.join(process.cwd(), 'src', 'Root.tsx');
|
||||
|
||||
bundlePath = await bundle({
|
||||
entryPoint,
|
||||
@@ -225,6 +234,9 @@ async function renderJob(job: RenderJob): Promise<void> {
|
||||
const ext = job.format;
|
||||
const outputPath = path.join(RENDERS_DIR, `${job.id}.${ext}`);
|
||||
|
||||
// In packaged Electron, point to unpacked compositor binaries
|
||||
const binariesDirectory = process.env.BRADLY_BINARIES_DIR || undefined;
|
||||
|
||||
console.log(`🎬 Rendering [${job.id}] → ${job.format} (${job.width}×${job.height})`);
|
||||
|
||||
// Resolve the full composition config from the bundle
|
||||
@@ -253,6 +265,7 @@ async function renderJob(job: RenderJob): Promise<void> {
|
||||
output: outputPath,
|
||||
imageFormat: job.format as 'png' | 'jpeg',
|
||||
inputProps: job.inputProps,
|
||||
binariesDirectory,
|
||||
});
|
||||
|
||||
job.progress = 100;
|
||||
@@ -267,6 +280,7 @@ async function renderJob(job: RenderJob): Promise<void> {
|
||||
codec: job.format === 'webm' ? 'vp8' : 'h264',
|
||||
outputLocation: outputPath,
|
||||
inputProps: job.inputProps,
|
||||
binariesDirectory,
|
||||
onProgress: ({ renderedFrames, encodedFrames }) => {
|
||||
const progress = Math.round(
|
||||
((renderedFrames ?? encodedFrames ?? 0) / job.durationInFrames) * 100
|
||||
|
||||
Reference in New Issue
Block a user