@molecule/api-video-render

Utility · video-render · API (Node) · v2.0.1 · Apache-2.0

FFmpeg-driven server-side video rendering, queue-driven (MP4 export). Wraps ffmpeg with a Renderer interface and a worker entry-point compatible with a queue bond (e.g. @molecule/api-queue-redis).

npm install @molecule/api-video-render

npm · Source on GitHub

How it works

@molecule/api-video-render is a utility package for the API (Node) side (video-render).

import { renderVideo, getRenderStatus, cancelRender } from '@molecule/api-video-render'

const job = await renderVideo(
  {
    duration: 10,
    resolution: { width: 1920, height: 1080 },
    fps: 30,
    tracks: [
      {
        id: 'v0',
        kind: 'video',
        clips: [
          { id: 'c0', source: '/uploads/intro.mp4', start: 0, duration: 5 },
          { id: 'c1', source: '/uploads/main.mp4', start: 5, duration: 5 },
        ],
        effects: [{ id: 'fx0', kind: 'fade-in', start: 0, duration: 1 }],
      },
    ],
  },
  { format: 'mp4', outputPath: '/tmp/out.mp4' },
)

// Poll for completion.
let status = await getRenderStatus(job.jobId)
while (status.status === 'queued' || status.status === 'rendering') {
  await new Promise((r) => setTimeout(r, 250))
  status = await getRenderStatus(job.jobId)
}

Works with: @molecule/api-queue

Reference

Server-side video rendering for molecule.dev. Takes a VideoTimeline (clips + tracks + effects) and queues an ffmpeg-driven render that produces an MP4 (or WebM) file at a caller-supplied outputPath.

Renders run asynchronously: renderVideo enqueues a job onto the bonded @molecule/api-queue provider and returns a { jobId, status: 'queued' } handle. Workers process jobs by calling processRenderJob, which spawns ffmpeg with a sanitized, argv-only command line — never a shell string. Status is observed via getRenderStatus; jobs are cancelled via cancelRender.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { renderVideo, getRenderStatus, cancelRender } from '@molecule/api-video-render'

const job = await renderVideo(
  {
    duration: 10,
    resolution: { width: 1920, height: 1080 },
    fps: 30,
    tracks: [
      {
        id: 'v0',
        kind: 'video',
        clips: [
          { id: 'c0', source: '/uploads/intro.mp4', start: 0, duration: 5 },
          { id: 'c1', source: '/uploads/main.mp4', start: 5, duration: 5 },
        ],
        effects: [{ id: 'fx0', kind: 'fade-in', start: 0, duration: 1 }],
      },
    ],
  },
  { format: 'mp4', outputPath: '/tmp/out.mp4' },
)

// Poll for completion.
let status = await getRenderStatus(job.jobId)
while (status.status === 'queued' || status.status === 'rendering') {
  await new Promise((r) => setTimeout(r, 250))
  status = await getRenderStatus(job.jobId)
}
// Express adapter (POST /render/video, GET/DELETE /render/jobs/:id).
// Mount it behind your authentication middleware.
import express from 'express'
import {
  createEnqueueRenderHandler,
  createGetRenderStatusHandler,
  createCancelRenderHandler,
} from '@molecule/api-video-render'

const router = express.Router()
const enqueue = createEnqueueRenderHandler({
  mediaRoot: '/srv/app/uploads', // clip sources must resolve inside this dir
  outputDir: '/srv/app/renders', // output is written here as <uuid>.<format>
})
const status = createGetRenderStatusHandler()
const cancel = createCancelRenderHandler()

router.post('/render/video', (req, res, next) => {
  enqueue(
    { body: req.body },
    {
      setStatus: (s) => {
        res.status(s)
      },
      sendJson: (j) => {
        res.json(j)
      },
    },
  ).catch(next)
})

router.get('/render/jobs/:id', (req, res, next) => {
  status(
    { params: req.params },
    {
      setStatus: (s) => {
        res.status(s)
      },
      sendJson: (j) => {
        res.json(j)
      },
    },
  ).catch(next)
})

router.delete('/render/jobs/:id', (req, res, next) => {
  cancel(
    { params: req.params },
    {
      setStatus: (s) => {
        res.status(s)
      },
      sendJson: (j) => {
        res.json(j)
      },
    },
  ).catch(next)
})

Type

utility

Installation

npm install @molecule/api-video-render @molecule/api-queue

API

Interfaces

FfmpegStderr

Subset of child_process.ChildProcess's stderr we care about — enough to listen for the textual progress lines ffmpeg writes to stderr.

interface FfmpegStderr {
  on(event: 'data', listener: (chunk: Buffer | string) => void): void
}

FfmpegProcess

The minimal child-process surface the worker observes. Real ChildProcesses satisfy this; tests provide a EventEmitter-shaped fake.

interface FfmpegProcess {
  stderr: FfmpegStderr | null
  on(event: 'close', listener: (code: number | null) => void): void
  on(event: 'error', listener: (error: Error) => void): void
  /** Sends SIGTERM (or another signal) to the child. */
  kill(signal?: NodeJS.Signals | number): boolean
}

VideoRenderRequest

Minimal request shape used by the render handlers.

interface VideoRenderRequest {
  /** Parsed JSON body — only needed for the enqueue endpoint. */
  body?: unknown
  /** Path parameters — `{ id: string }` for status/cancel. */
  params?: Record<string, string | undefined>
}

VideoRenderResponse

Minimal response shape used by the render handlers.

interface VideoRenderResponse {
  /** Set the HTTP status code. */
  setStatus(status: number): void
  /** Write a JSON body and end the response. */
  sendJson(body: unknown): void
}

CreateEnqueueRenderHandlerOptions

Options for createEnqueueRenderHandler. mediaRoot and outputDir are required; the optional validate hook can reject requests pre-flight (e.g. tier limits, max duration).

interface CreateEnqueueRenderHandlerOptions {
  /**
   * Directory every clip source must live under. Relative sources resolve
   * against it; URLs, `..` segments and paths outside it are rejected.
   */
  mediaRoot: string
  /** Directory rendered files are written to, as `<uuid>.<format>`. */
  outputDir: string
  /** Pre-flight validator — throw to reject with HTTP 400. */
  validate?: (timeline: VideoTimeline, options: RenderVideoOptions) => void | Promise<void>
}

JobStore

The minimal contract job-status backends must satisfy. All operations are async to allow remote stores (Redis, database, etc.).

interface JobStore {
  /** Read the latest status snapshot for a job. */
  get(jobId: string): Promise<RenderJobStatus | undefined>
  /** Write/replace the status snapshot for a job. */
  set(jobId: string, status: RenderJobStatus): Promise<void>
  /** Merge partial fields into the existing status (creating it if absent). */
  patch(jobId: string, patch: Partial<RenderJobStatus>): Promise<RenderJobStatus>
  /** Remove a job's status entry. Used by `cancelRender` cleanup. */
  delete(jobId: string): Promise<void>
}

VideoResolution

Render output resolution as discrete pixel dimensions. The renderer scales each clip's source to fit. Dimensions must be even (codec requirement).

interface VideoResolution {
  /** Output width in pixels. Must be even. */
  width: number
  /** Output height in pixels. Must be even. */
  height: number
}

VideoClip

A single source clip placed on a timeline track. The renderer trims the source from [sourceStart, sourceStart + duration) and places it at start on the track timeline.

source is a local path. https?: URLs are only opened when the trusted caller sets allowRemoteSources; the HTTP handler accepts only paths inside its configured mediaRoot.

interface VideoClip {
  /** Stable identifier for the clip (used for status / progress correlation). */
  id: string
  /**
   * Source file path or absolute URL. The worker validates that the value
   * is a plain string with no shell metacharacters before passing to ffmpeg.
   */
  source: string
  /** Track-local start time (seconds). */
  start: number
  /** Clip duration on the timeline (seconds). */
  duration: number
  /** Source-local trim-in point (seconds). Defaults to 0. */
  sourceStart?: number
  /** Override clip volume (linear, `1` = unity). Defaults to 1. */
  volume?: number
  /**
   * Whether to mute audio for this clip. When true, no audio stream is
   * pulled from the source.
   */
  muted?: boolean
}

VideoEffect

A pre-baked effect placed on a clip or track. Effects are name-keyed; the worker resolves them to safe ffmpeg filter graphs from a built-in allow-list. Free-form -vf filter strings are NEVER accepted from user input — all parameters are typed and validated.

interface VideoEffect {
  /** Stable identifier. */
  id: string
  /** Allow-listed effect kind. */
  kind: 'fade-in' | 'fade-out' | 'crossfade' | 'crop' | 'scale' | 'volume'
  /** Track-local start time (seconds). Effects apply from `start` to `start + duration`. */
  start: number
  /** Effect duration (seconds). */
  duration: number
  /**
   * Numeric parameters for the effect. Validated against the effect kind by
   * the worker (e.g. `crop` requires `x`, `y`, `width`, `height`).
   */
  params?: Record<string, number>
}

VideoTrack

A timeline track. Tracks render bottom-to-top (last track on top). Clips within a track render in start-order; overlaps composite via the renderer's mixing rules.

interface VideoTrack {
  /** Stable identifier. */
  id: string
  /** Track kind — `video` tracks contribute pixels, `audio` tracks contribute audio only. */
  kind: 'video' | 'audio'
  /** Clips on this track, in `start`-order. */
  clips: VideoClip[]
  /** Effects scoped to this track. */
  effects?: VideoEffect[]
  /** Track-level volume (linear). Defaults to 1. */
  volume?: number
  /** Whether the track is muted. */
  muted?: boolean
}

VideoTimeline

Top-level timeline definition consumed by renderVideo.

interface VideoTimeline {
  /** Total timeline duration (seconds). */
  duration: number
  /** Output resolution. */
  resolution: VideoResolution
  /** Output frames-per-second. */
  fps: VideoFps
  /** Tracks, rendered bottom-to-top. */
  tracks: VideoTrack[]
  /** Optional default background color (CSS `#rrggbb`). Defaults to black. */
  background?: string
}

RenderVideoOptions

Render-time options accepted by renderVideo.

interface RenderVideoOptions {
  /** Output container format. Defaults to `mp4`. */
  format?: VideoRenderFormat
  /**
   * Override the timeline's output resolution. Useful for proxy renders.
   */
  resolution?: VideoResolution
  /**
   * Override the timeline's output FPS.
   */
  fps?: VideoFps
  /**
   * Encoder codec. Defaults to `libx264` for mp4, `libvpx-vp9` for webm.
   */
  codec?: VideoCodec
  /**
   * Constant Rate Factor (quality knob) — lower = higher quality. Codec-specific
   * range. Defaults are sensible (`23` for x264).
   */
  crf?: number
  /**
   * Output destination path. The worker writes the rendered file here.
   * Trusted server code only — the HTTP handler ignores any client value and
   * mints a path inside its configured `outputDir`.
   */
  outputPath: string
  /**
   * Queue name used for enqueuing render jobs. Defaults to `video-render`.
   */
  queueName?: string
  /**
   * Optional caller-supplied job ID. When omitted, an auto-generated random
   * ID is used. Trusted server code only — the HTTP handler never forwards a
   * client-supplied value.
   */
  jobId?: string
  /**
   * Let ffmpeg open `http:`/`https:` sources. Defaults to `false`, which
   * passes `-protocol_whitelist file` so only local files can be read.
   * Trusted server code only — the HTTP handler never sets it.
   */
  allowRemoteSources?: boolean
}

RenderJob

Result of renderVideo. The job is queued; callers must poll getRenderStatus to observe progress.

interface RenderJob {
  /** Stable job identifier. Use this to poll status and to cancel. */
  jobId: string
  /** Initial job state — always `queued`. */
  status: 'queued'
  /** Queue name the job was enqueued onto. */
  queueName: string
}

RenderJobStatus

Status snapshot for a render job, returned by getRenderStatus.

interface RenderJobStatus {
  /** Current state of the job. */
  status: RenderJobState
  /** Progress fraction 0..1 when in `rendering` state. */
  progress?: number
  /** Output URL/path when in `completed` state. */
  outputUrl?: string
  /** Error message when in `failed` state. */
  error?: string
  /** Wall-clock timestamps for state transitions. */
  startedAt?: Date
  /** Wall-clock timestamps for state transitions. */
  finishedAt?: Date
}

RenderJobMessage

Internal envelope written to the queue for each render request. Workers deserialize this back into the renderer.

interface RenderJobMessage {
  /** Job identifier. */
  jobId: string
  /** Timeline definition. */
  timeline: VideoTimeline
  /** Resolved render options (defaults already applied). */
  options: RenderVideoOptions & { format: VideoRenderFormat; codec: VideoCodec }
}

ProcessRenderJobDeps

Optional dependency overrides for processRenderJob. Defaults to the bonded job store + ffmpeg runner.

interface ProcessRenderJobDeps {
  /** Override the job store. Defaults to `getDefaultJobStore`. */
  jobStore?: JobStore
  /** Override the ffmpeg runner. Defaults to `getDefaultFfmpegRunner`. */
  ffmpegRunner?: FfmpegRunner
}

Types

FfmpegRunner

Function that spawns ffmpeg with a fully-formed argv. The first element of args is NOT the ffmpeg binary — implementations supply that themselves.

Implementations MUST NOT pass args through a shell.

type FfmpegRunner = (args: readonly string[]) => FfmpegProcess

VideoRenderFormat

Output container format. Drives the codec defaults and file extension.

type VideoRenderFormat = 'mp4' | 'webm'

VideoFps

Frames per second for the rendered output. Defaults to 30.

type VideoFps = number

VideoCodec

Encoder codec. Sensible defaults are picked per format when omitted — libx264 for mp4, libvpx-vp9 for webm.

type VideoCodec = 'libx264' | 'libx265' | 'libvpx' | 'libvpx-vp9' | 'libaom-av1'

RenderJobState

Render job lifecycle states.

type RenderJobState = 'queued' | 'rendering' | 'completed' | 'failed' | 'cancelled'

Functions

assertSafePath(value, label)

Validate a source path or URL. Throws TypeError with a descriptive message if the value is unsafe.

function assertSafePath(value: unknown, label: string): void
  • value — The candidate path/URL.
  • label — Human-readable name (e.g. 'clip.source') for error messages.

assertAllowedFormat(value)

Validate an output format against the allow-list.

function assertAllowedFormat(value: unknown): string
  • value — The candidate format.

Returns: The format, unchanged.

assertAllowedCodec(value)

Validate a video codec against the allow-list.

function assertAllowedCodec(value: unknown): string
  • value — The candidate codec.

Returns: The codec, unchanged.

assertFiniteNonNegative(value, label, max)

Validate a finite, non-negative number with an upper bound. Throws on bad input.

function assertFiniteNonNegative(value: unknown, label: string, max?: number): number
  • value — The candidate number.
  • label — Field name for error messages.
  • max — Optional maximum (defaults to 24h in seconds).

assertEvenDimension(value, label)

Validate an even, positive integer dimension (codec requirement).

function assertEvenDimension(value: unknown, label: string): number
  • value — The candidate dimension.
  • label — Field name for error messages.

effectToFilter(effect)

Validate an effect kind against the allow-list and return a fixed, parameter-only filter string. Numeric parameters are validated and formatted; nothing user-typed is interpolated as a filter token.

function effectToFilter(effect: VideoEffect): string
  • effect — The effect to translate.

Returns: The ffmpeg filter expression for the effect.

assertValidClip(clip, trackId)

Validate a VideoClip's shape and return a sanitized copy.

function assertValidClip(clip: VideoClip, trackId: string): VideoClip
  • clip — The candidate clip.
  • trackId — Owning track ID, for error messages.

assertValidTrack(track)

Validate a VideoTrack and its clips/effects.

function assertValidTrack(track: VideoTrack): VideoTrack
  • track — The candidate track.

assertValidTimeline(timeline)

Validate a complete VideoTimeline.

function assertValidTimeline(timeline: VideoTimeline): VideoTimeline
  • timeline — The candidate timeline.

buildFfmpegArgs(message)

Build a flat, read-only argv array for ffmpeg from a validated render message. Inputs are passed via -i per clip; effects translate to a filter_complex graph via the allow-listed effectToFilter.

The returned array is the EXACT argv that gets passed to spawn — no additional shell processing happens.

function buildFfmpegArgs(message: RenderJobMessage): readonly string[]
  • message — The validated render job message.

Returns: The ffmpeg argv (excluding the ffmpeg binary itself).

setFfmpegBinaryPath(path)

Set (or clear) an explicit path to the ffmpeg binary used by defaultFfmpegRunner. Takes precedence over the FFMPEG_PATH environment variable. Pass undefined to clear the override and fall back to FFMPEG_PATH / 'ffmpeg' again.

function setFfmpegBinaryPath(path: string | undefined): void
  • path — Absolute path to the ffmpeg binary, or undefined to reset.

getFfmpegBinaryPath()

Resolve the ffmpeg binary path the default runner will spawn. Precedence: explicit override (setFfmpegBinaryPath) → FFMPEG_PATH env var → 'ffmpeg' (resolved on $PATH).

function getFfmpegBinaryPath(): string

Returns: The ffmpeg binary path/name to spawn.

toActionableSpawnError(error, binaryPath)

Translate a spawn failure into an actionable Error. A raw spawn ffmpeg ENOENT says nothing about the fix; this rewrites it to name the resolved binary path and how to point at a real one. Non-ENOENT errors are returned unchanged.

function toActionableSpawnError(error: unknown, binaryPath: string): Error
  • error — The error thrown/emitted by child_process.spawn.
  • binaryPath — The binary path that failed to spawn.

Returns: An Error with an actionable message (ENOENT) or the original.

defaultFfmpegRunner(args)

Default runner — spawns the configured ffmpeg binary directly (getFfmpegBinaryPath), passing each argv element as a discrete argument (no shell interpretation). A missing binary surfaces as an actionable error via toActionableSpawnError — both when spawn throws synchronously and when the child emits error asynchronously.

function defaultFfmpegRunner(args: readonly string[]): FfmpegProcess
  • args — The argv to pass after the binary name.

Returns: The spawned FfmpegProcess.

getFfmpegRunner()

Returns the active ffmpeg runner — the default child_process.spawn-based runner unless overridden by setFfmpegRunner.

function getFfmpegRunner(): FfmpegRunner

Returns: The active FfmpegRunner.

setFfmpegRunner(runner)

Replace the active ffmpeg runner. Tests pass a stub; production code generally leaves this as the default. Pass undefined to reset.

function setFfmpegRunner(runner: FfmpegRunner | undefined): void
  • runner — The runner to use, or undefined to reset.

parseFfmpegProgressSeconds(chunk)

ffmpeg reports progress as time=HH:MM:SS.SS lines on stderr. Parse and convert to seconds. Returns undefined when no progress line is present in the chunk.

function parseFfmpegProgressSeconds(chunk: string): number | undefined
  • chunk — A chunk of stderr text.

Returns: The current decoded position in seconds, if found.

createEnqueueRenderHandler(handlerOptions)

Build the POST /render/video handler. The request body must be { timeline, options? } (or { video, options? }). Only format, resolution, fps, codec and crf are read from options.

function createEnqueueRenderHandler(
  handlerOptions: CreateEnqueueRenderHandlerOptions,
): (req: VideoRenderRequest, res: VideoRenderResponse) => Promise<void>
  • handlerOptions — Media root, output directory and optional validator.

Returns: An async handler.

createGetRenderStatusHandler()

Build the GET /render/jobs/:id handler.

function createGetRenderStatusHandler(): (
  req: VideoRenderRequest,
  res: VideoRenderResponse,
) => Promise<void>

Returns: An async handler.

createCancelRenderHandler()

Build the DELETE /render/jobs/:id handler.

function createCancelRenderHandler(): (
  req: VideoRenderRequest,
  res: VideoRenderResponse,
) => Promise<void>

Returns: An async handler.

createMemoryJobStore()

In-memory job store backed by a Map. Process-local — restart loses state.

function createMemoryJobStore(): JobStore

getJobStore()

Returns the currently active job store. Defaults to the in-memory store.

function getJobStore(): JobStore

Returns: The active JobStore.

setJobStore(store)

Replace the active job store. Bond packages (e.g. a Redis-backed store) call this during application startup. Pass undefined to reset to the default in-memory store — primarily useful in tests.

function setJobStore(store: JobStore | undefined): void
  • store — The store implementation to use, or undefined to reset.

hasTraversalSegment(value)

Whether a path string contains a .. segment (with / or \ separators).

function hasTraversalSegment(value: string): boolean
  • value — The path to inspect.

Returns: true when any segment is exactly ...

resolveMediaSource(source, mediaRoot, label)

Resolve a client-supplied media source to an absolute path inside mediaRoot. Relative paths are resolved against the root; absolute paths are accepted only when they already sit inside it.

function resolveMediaSource(source: unknown, mediaRoot: string, label: string): string
  • source — The untrusted source string from the request.
  • mediaRoot — The directory every source must live under.
  • label — Field name used in error messages.

Returns: The absolute, confined path.

createOutputPath(outputDir, extension)

Mint a fresh output path inside outputDir from a random UUID.

function createOutputPath(outputDir: string, extension: string): string
  • outputDir — The directory rendered files are written to.
  • extension — File extension without the dot (e.g. mp4).

Returns: An absolute output path no client input contributed to.

generateJobId()

Generate a stable random job ID. Format: vrj_<16 hex chars>. Uses crypto.randomUUID() when available (Node 19+), otherwise a Math.random fallback (sufficient for non-cryptographic uniqueness).

function generateJobId(): string

Returns: A new job ID.

renderVideo(timeline, options)

Enqueue a video render job.

The function validates the timeline + options, records an initial queued status in the job store, and pushes a RenderJobMessage onto the bonded queue. Workers consuming the queue invoke processRenderJob to do the actual ffmpeg work.

function renderVideo(timeline: VideoTimeline, options: RenderVideoOptions): Promise<RenderJob>
  • timeline — The timeline definition.
  • options — Render options (must include outputPath).

Returns: A handle containing the new jobId and initial status.

getRenderStatus(jobId)

Read the current status of a render job. When the job is unknown to the job store, returns a failed-shaped status with a clear error message.

function getRenderStatus(jobId: string): Promise<RenderJobStatus>
  • jobId — The job identifier returned by renderVideo.

Returns: The current RenderJobStatus.

cancelRender(jobId)

Mark a render job as cancelled. If the worker has already started processing, it is responsible for observing the cancelled state at the next progress checkpoint and tearing down its ffmpeg child.

Returns the resulting status snapshot.

function cancelRender(jobId: string): Promise<RenderJobStatus>
  • jobId — The job identifier.

Returns: The post-cancel status.

processRenderJob(message, deps)

Process a single render job. Returns the terminal RenderJobStatus after the worker observes ffmpeg's exit (or the job's cancellation).

Implementations of @molecule/api-queue consumers wire this up by subscribing to the queue and calling processRenderJob(message) for each received message.

function processRenderJob(
  message: RenderJobMessage,
  deps?: ProcessRenderJobDeps,
): Promise<RenderJobStatus>
  • message — The render job message to process.
  • deps — Optional dependency overrides (primarily for tests).

Returns: The final job status.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-queue ^1.0.1

Runtime Dependencies

  • @molecule/api-queue

Security model. The argv passed to ffmpeg is built by buildFfmpegArgs — a pure function that validates every field on the timeline against a strict allow-list before emitting tokens. Filenames must match [A-Za-z0-9_./:\-+@%]+ and may not start with - (which ffmpeg would otherwise read as an option). Effects compile to fixed filter strings keyed by effect.kind; arbitrary -vf filter strings from user input are NEVER accepted. The default ffmpeg runner uses child_process.spawn(args, { shell: false }) so argv elements cannot be reinterpreted by a shell. ffmpeg runs with -nostdin, and every input is opened with -protocol_whitelist file — a source can only make ffmpeg fetch a URL when trusted server code passes allowRemoteSources: true. Paths containing .. segments are rejected.

HTTP handlers: untrusted input, authentication required. Mount createEnqueueRenderHandler, createGetRenderStatusHandler and createCancelRenderHandler behind authentication — they render, read and cancel any job by id. createEnqueueRenderHandler({ mediaRoot, outputDir }) requires both directories: the client's outputPath, jobId, queueName and allowRemoteSources are ignored, the output path is minted inside outputDir from a random UUID, and every clip source must be a path that resolves inside mediaRoot (URLs, .. segments and absolute paths outside it are rejected with HTTP 400). outputPath, jobId and allowRemoteSources on renderVideo are for trusted server code only — never copy them from a request.

Resource intensity. Rendering even a short timeline can take minutes and saturate a CPU core; the package is queue-driven by design. Bond a real queue provider (e.g. @molecule/api-queue-redis or @molecule/api-queue-rabbitmq; @molecule/api-queue-memory keeps everything in one process for dev/tests) before calling renderVideo — without one, the call throws because no queue provider is bonded.

Runtime prerequisite: the ffmpeg binary. The worker spawns ffmpeg, which must be installed in the process that runs processRenderJob. The binary path is configurable — setFfmpegBinaryPath (or the FFMPEG_PATH env var) points at a bundled/custom binary, defaulting to 'ffmpeg' on $PATH. ffmpeg is NOT bundled with this package and is NOT present in minimal containers (including the molecule.dev sandbox base image) — install it there. When it's missing, the job fails with a clear, actionable error naming the path and the fix (install ffmpeg or set FFMPEG_PATH), surfaced as job status failed — not a raw spawn ffmpeg ENOENT.

Job status is process-local by default. The default JobStore is an in-memory Map. If the queue worker runs in a separate process from the API (the normal topology for redis/rabbitmq/sqs queues), the API's getRenderStatus polls ITS OWN store and reports queued forever while the worker renders happily elsewhere. Either run the worker in-process (with @molecule/api-queue-memory), or wire a shared store via setJobStore (Redis/database-backed) in BOTH the API and worker processes before enqueueing.

Locale. This package is purely programmatic. There is no companion locale bond; all error messages are English-only by design.