main

Source: main.js:10

Electron main process. Creates the window, owns the renderer↔main IPC bridge, spawns and supervises the Python audio worker (newline-delimited JSON over stdin/stdout), serves restored media over the ezl-file:// protocol, and manages the native menu, single-instance lock, navigation guards, and shutdown sequence.


Instance Methods

failPendingRequests(err: Error)

Reject every in-flight request and drop their queued payloads, so callers get a real error immediately instead of running out their timeouts.

Parameters

  • err (Error) — Rejection reason handed to every pending caller.

markWorkerDown()

Mark the worker permanently unavailable and tell the user once. Analysis, preview transcode, and export all run through it, so the session is effectively review-only from here — every later request is rejected up-front in pyWrite.

pyWrite(id: string, payload: string)

Route a request to the worker: write immediately, queue until the ready signal, or — when the worker is down — reject the pending entry right away so the caller sees a clear failure instead of a timeout.

Parameters

  • id (string) — Request id already registered in pendingRequests.
  • payload (string) — Newline-terminated JSON message for the worker.

spawnPython()

Spawn (or respawn) the Python audio worker and wire up its lifecycle.

Command resolution — packaged: run the frozen PyInstaller worker binary directly (no interpreter, no source .py shipped). Dev: prefer the worker's own venv (worker/.venv, where requirements.txt is installed) so its deps never need to pollute the system python3; fall back to PATH python3 when no venv exists. worker/ lives at the repo root, a sibling of desktop/ — not inside the app dir.

Environment — the worker builds its export "Created By" tag from EZL_APP_NAME / EZL_APP_VERSION instead of reading package.json off disk (which doesn't exist inside the frozen onefile binary). EZL_FFMPEG hands the worker the bundled ffmpeg location only when packaged; in dev it stays unset so the worker uses ffmpeg from PATH (the bundled binaries may not match the dev machine's arch). config.py reads these vars.

lineBuffer is reset before each spawn: a crash mid-line must not corrupt the next worker's first message.

The 'error' event fires when the binary itself can't start (e.g. python3 not on PATH, frozen worker missing); without a listener that's an uncaught exception that crashes the app. A write racing the worker's death (before its 'exit' lands) errors on the stdin stream; the stdin 'error' listener only logs, because unhandled it's an uncaught exception and the exit handler owns recovery.

stdout is parsed as newline-delimited JSON. The worker's ready message flushes the deferred write queue. Each response resolves its pending request; a transcode or video-proxy response with no waiter (its request already timed out) references a temp file nobody will read or delete, so its wav_path / proxy_path is unlinked here.

On unexpected exit, in-flight requests are failed and the worker respawns with linear backoff, keeping the single-persistent-worker design. A worker that keeps dying gets a bounded number of attempts (PY_MAX_RESTARTS), then the session stays down.

releaseVideoProxy()

Delete the current preview proxy (a temp file — best-effort cleanup) and revoke its ezl-file:// streaming permission.

approvedProjectPath(p: string): string | null

Resolve a renderer-supplied project path against the approved set.

Parameters

  • p (string) — Path received over IPC.

Returns

  • string | null — The normalized path, only for a .ezlproj location that came from one of our dialogs; null otherwise.

allowProjectMedia(project: object): Promise.<void>

Register a project's media files (video + tracks) as allowed ezl-file:// targets, realpath'd on entry. A file that's missing is skipped — restore skips it too, so there's nothing to allow.

Parameters

  • project (object) — Parsed .ezlproj object ({ video?.path, tracks[]?.path }).

Returns

  • Promise.<void>

registerEzlFileProtocol()

Register the ezl-file:// protocol handler that streams allowlisted media off disk.

Why it exists: on project restore there is no browser File object — only a saved absolute path — and the renderer can't build a File (or a blob: URL) from a path. This protocol bridges the gap: a restored track/video stores ezl-file://localhost/abs/path wherever a fresh drag/drop import stores a blob: URL, and the rest of the pipeline (, fetch→decodeAudioData, waveform draw) consumes it transparently.

It serves like a real file server — Content-Length on the full body, and HTTP Range (206) for partial requests. That matters because a / element only reports a finite, seekable .duration when the source advertises a byte length and honors Range; without it the element's duration comes back Infinity, which on restore left the trim system with duration 0 (handles stuck at 0, undraggable). Range also keeps multi-GB video off the heap — only the requested slice is streamed. Range requests are "bytes=START-END" (END optional); the element uses the 206 + Content-Range to learn the total size and to seek — e.g. an initial "bytes=0-" is how it discovers the duration. An unsatisfiable range gets a 416. Requests without Range (e.g. fetch()→decodeAudioData for the waveform) get the full body + length.

The URL carries an explicit localhost host (see descriptorFromPath): a standard scheme with an empty host (triple-slash) doesn't canonicalize an absolute path cleanly, so the host makes pathname reliable. The host is ignored and the file resolves from the pathname only — ezl-file://localhost/Users/foo/My%20Clip.mov → /Users/foo/My Clip.mov. The path is realpath'd (canonicalized) before the allowlist check so a symlink or ../ can't alias a request onto a file outside the opened project's media; a path that fails realpath (file moved/deleted mid-session) gets a 404, one not on the allowlist a 403.

Every response carries Access-Control-Allow-Origin: ''. The renderer's media elements load with crossorigin="anonymous" so Web Audio can process them: ezl-file:// is a different origin from the file:// page, and without CORS approval createMediaElementSource TAINTS the element and outputs pure silence (OA / restored tracks play nothing). The scheme is app-internal and allowlisted per-file, so '' grants nothing beyond our own renderer.

buildMenu(win: BrowserWindow)

Build and install the native application menu. Menus are owned by the main process: a menu click fires HERE, but session state lives in the renderer — so Save/Open/New don't act directly, they message the renderer over menu:new-project / menu:save-project / menu:open-project (the first main→renderer direction in the app; see preload onMenu + renderer listeners). macOS expects the first menu to be the app-name menu (About / Quit); Windows has none, and Quit belongs under File there. The editMenu / viewMenu roles give correct per-platform labels and shortcuts for free.

Parameters

  • win (BrowserWindow) — Window whose webContents receives the menu messages.

createWindow(): BrowserWindow

Create the main application window (context-isolated, no node integration, preload bridge only) and load the renderer. The icon affects window chrome on Windows/Linux and is ignored on macOS.

Returns

  • BrowserWindow — The new window — returned so the menu can target this window's webContents.

Instance Fields

pyWorker

Handle to the single persistent Python worker subprocess (null until spawned).

pyReady

True once the worker has sent its ready signal and accepts writes.

pyDown

Worker unavailable for the rest of the session (spawn failed / restart budget spent).

pyQuitting

App shutdown in progress — worker exit is expected, don't restart.

pyRestartCount

Respawn attempts made this session.

lineBuffer

Partial stdout line carried between data chunks.

requestCounter

Monotonic counter used to build request ids.

pyWriteQueue

Payloads deferred until the worker sends its ready signal.

currentVideoProxyPath

Absolute realpath of the one live video preview proxy on disk, or null. Chromium has no decoder for pro acquisition codecs (ProRes, DNxHD, …), so those videos preview as a black frame. The renderer detects it and asks for a preview-only H.264/AAC proxy, which the worker writes to a temp .mp4. Unlike the audio path the bytes are NOT slurped over IPC — a proxy can be hundreds of MB, so it's served off disk over ezl-file:// (Range streaming, same as project restore) and deleted when replaced/released/quit. Export never sees the proxy; it reads the original source path.

Other

binPath

Root directory for the bundled external binaries. Packaged: electron-builder copies resources/ → Contents/Resources/bin (see build.extraResources). Dev: resources/ lives in desktop/, one level above src/.

platform

Platform id ('darwin' | 'win32') — selects the per-platform binary subfolder.

ffmpegExt

Bundled ffmpeg executable path; Windows binaries carry .exe, macOS none.

pythonExt

Frozen Python worker binary path (used by packaged builds only).

PY_MAX_RESTARTS

Respawn attempts allowed per session; beyond this the worker stays down.

PY_DOWN_MESSAGE

User-facing message for requests refused while the worker is down.

pendingRequests

In-flight worker requests: id → { resolve, reject, timer }.

PROJECT_FILTERS

File-dialog filter for .ezlproj session files.

approvedProjectPaths

Project paths a main-process dialog has handed out this session — the only paths the project read/write handlers will touch. The renderer owns all session state, so those handlers are pure plumbing (pick a path, then read/write JSON the renderer builds); the renderer never gets to name a location the user didn't pick, and a crafted IPC call with an arbitrary path is refused.

EZL_MIME

Content-Type by file extension for ezl-file:// responses.

allowedMediaPaths

Media the renderer may stream over ezl-file:// — only the paths named in a project file the user opened via the dialog, plus the live video preview proxy (fresh imports use blob: URLs and never touch this protocol). Entries are stored realpath'd so the request-time comparison can't be dodged with symlinks or ../ segments; anything else gets a 403 from the protocol handler.

devIconPath

Icon for dev runs and Windows/Linux window chrome. Packaged builds get their icon from electron-builder (build/icon.icns baked into the bundle / .exe), and build/ isn't shipped in the app's files — hence the !isPackaged guard where this path is used.