Skip to main content
The RenderOrchestrator class enables distributed rendering by splitting a composition into multiple chunks that can be rendered in parallel. This dramatically reduces rendering time for long videos on multi-core systems.

How it works

  1. Plan: Divides the composition into frame ranges based on concurrency
  2. Render: Executes chunks in parallel using worker renderers
  3. Concatenate: Stitches chunk videos into a single file
  4. Mix: Applies final audio mixing and encoding

Static methods

plan

static plan(
  compositionUrl: string,
  outputPath: string,
  options: DistributedRenderOptions
): RenderPlan
Creates a render plan without executing it. Useful for previewing the distribution strategy.
compositionUrl
string
required
URL of the HTML composition to render.
outputPath
string
required
Final output file path for the rendered video.
options
DistributedRenderOptions
required
Configuration object extending RendererOptions with distributed rendering settings.

Returns

A RenderPlan object containing:
totalFrames
number
Total number of frames in the composition.
chunks
RenderChunk[]
Array of chunk specifications, each containing:
  • id: Chunk identifier
  • startFrame: First frame in this chunk
  • frameCount: Number of frames to render
  • outputFile: Temporary file path for chunk output
  • options: RendererOptions for this specific chunk
concatManifest
string[]
List of chunk file paths to concatenate.
concatOutputFile
string
Temporary file path for the concatenated video (before final audio mix).
finalOutputFile
string
Final output path (same as the outputPath parameter).
mixOptions
RendererOptions
Options for the final audio mixing pass.
cleanupFiles
string[]
List of temporary files to delete after successful rendering.

Example

import { RenderOrchestrator } from '@helios/renderer';

const plan = RenderOrchestrator.plan(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 4
  }
);

console.log(`Rendering ${plan.totalFrames} frames across ${plan.chunks.length} chunks`);
plan.chunks.forEach(chunk => {
  console.log(`Chunk ${chunk.id}: frames ${chunk.startFrame}-${chunk.startFrame + chunk.frameCount}`);
});

render

static async render(
  compositionUrl: string,
  outputPath: string,
  options: DistributedRenderOptions,
  jobOptions?: RenderJobOptions
): Promise<void>
Executes a distributed render job.
compositionUrl
string
required
URL of the HTML composition to render.
outputPath
string
required
Final output file path for the rendered video.
options
DistributedRenderOptions
required
Configuration object extending RendererOptions with distributed rendering settings.
jobOptions
RenderJobOptions
Optional job configuration including progress callbacks and abort signals.

Example

await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 120,
    concurrency: 8,
    audioFilePath: './music.mp3'
  },
  {
    onProgress: (progress) => {
      console.log(`Overall progress: ${(progress * 100).toFixed(1)}%`);
    }
  }
);

Distributed render options

interface DistributedRenderOptions extends RendererOptions {
  concurrency?: number;
  executor?: RenderExecutor;
}
concurrency
number
Number of parallel render workers. Defaults to os.cpus().length - 1.
executor
RenderExecutor
Custom executor for running render chunks. Defaults to LocalExecutor which runs chunks as parallel processes on the local machine. Implement custom executors for cloud-based or distributed execution.

Concurrency behavior

Automatic concurrency

// Uses CPU count - 1 workers
await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60
    // concurrency not specified
  }
);

Single worker fallback

When concurrency: 1, the orchestrator skips chunking and uses a standard Renderer directly:
await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 1 // No parallelization
  }
);

Custom concurrency

await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 16 // Use 16 workers
  }
);

Audio handling in distributed mode

The orchestrator uses a multi-stage audio pipeline:

Chunk rendering

  1. Each chunk renders with PCM audio (pcm_s16le codec)
  2. Audio tracks are not mixed during chunk rendering
  3. Chunks output to .mov container format to preserve PCM audio

Concatenation

  1. Chunk videos are concatenated without re-encoding
  2. PCM audio streams are preserved in the concatenated file

Final mix

  1. Concatenated video is processed with final audio options
  2. Audio tracks from audioFilePath and audioTracks are mixed
  3. Output is encoded with specified audioCodec (default: aac)
  4. Final video uses the container format from outputPath extension

Example with audio

await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 4,
    audioTracks: [
      {
        path: './music.mp3',
        volume: 0.7,
        fadeInDuration: 2,
        fadeOutDuration: 3
      },
      {
        path: './sfx.wav',
        offset: 10,
        volume: 1.0
      }
    ],
    audioCodec: 'aac',
    audioBitrate: '192k'
  }
);

Progress tracking

The orchestrator aggregates progress from all workers:
const progressBar = new Map<number, number>();

await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 4
  },
  {
    onProgress: (globalProgress) => {
      // globalProgress is weighted by chunk size
      console.log(`Overall: ${(globalProgress * 100).toFixed(1)}%`);
    }
  }
);
Each worker’s progress is weighted by its frame count, ensuring accurate overall progress reporting.

Error handling and cancellation

Abort signal

const controller = new AbortController();

const renderPromise = RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 4
  },
  {
    signal: controller.signal
  }
);

// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10000);

try {
  await renderPromise;
} catch (error) {
  if (error.message === 'Aborted') {
    console.log('Render was cancelled');
  }
}

Worker failure propagation

If any worker fails, all other workers are immediately aborted:
try {
  await RenderOrchestrator.render(
    'file:///composition.html',
    './output.mp4',
    {
      width: 1920,
      height: 1080,
      fps: 30,
      durationInSeconds: 60,
      concurrency: 4
    }
  );
} catch (error) {
  // If worker 2 fails, workers 0, 1, and 3 are aborted
  console.error('Distributed render failed:', error);
}

Temporary file cleanup

The orchestrator automatically cleans up temporary files:
  • Chunk video files (e.g., temp_123_part_0.mov)
  • Concatenated intermediate file (e.g., temp_concat_123.mov)
Cleanup occurs in the finally block, ensuring files are removed even if rendering fails.

Custom executors

Implement the RenderExecutor interface to run chunks on cloud infrastructure:
import { RenderExecutor } from '@helios/renderer';

class CloudExecutor implements RenderExecutor {
  async render(
    compositionUrl: string,
    outputPath: string,
    options: RendererOptions,
    jobOptions?: RenderJobOptions
  ): Promise<void> {
    // Upload composition to cloud storage
    // Trigger cloud render job
    // Download rendered chunk
  }
}

await RenderOrchestrator.render(
  'file:///composition.html',
  './output.mp4',
  {
    width: 1920,
    height: 1080,
    fps: 30,
    durationInSeconds: 60,
    concurrency: 100, // Scale to cloud capacity
    executor: new CloudExecutor()
  }
);

Performance considerations

Optimal concurrency

  • CPU-bound: Set concurrency to CPU core count minus 1
  • Memory-bound: Reduce concurrency if renders are memory-intensive
  • Cloud: Scale concurrency based on available cloud workers

Chunk size

Chunk size is automatically calculated as totalFrames / concurrency. For very short videos, some workers may receive no frames:
// 90 frames, 4 workers = 22-23 frames per chunk
// 90 frames, 100 workers = 0-1 frames per chunk (inefficient)

When to use distributed rendering

Use distributed rendering when:
  • Videos are longer than 30 seconds
  • You have multiple CPU cores available
  • Rendering time is critical
Use standard Renderer when:
  • Videos are very short (< 10 seconds)
  • Single-core environment
  • Simplicity is preferred over speed

Console output

Distributed rendering produces detailed logs:
Starting distributed render with concurrency: 4
[Worker 0] Rendering frames 0 to 450 (450 frames) to /tmp/temp_123_part_0.mov
[Worker 1] Rendering frames 450 to 900 (450 frames) to /tmp/temp_123_part_1.mov
[Worker 2] Rendering frames 900 to 1350 (450 frames) to /tmp/temp_123_part_2.mov
[Worker 3] Rendering frames 1350 to 1800 (450 frames) to /tmp/temp_123_part_3.mov
All chunks rendered. Concatenating...
Mixing audio into concatenated video...
Spawning FFmpeg for audio mixing: /usr/local/bin/ffmpeg -i /tmp/concat.mov ...
Cleaning up temporary files...