Skip to content

Compositor API

The Compositor is a canvas-based video compositing engine for layering multiple media sources with transforms, opacity, and rotation. It's designed for building video editors, multi-source players, and compositing applications.

Constructor

new Compositor(options)

Creates a new Compositor instance.

typescript
constructor(options: CompositorOptions)

Options

PropertyTypeDefaultDescription
canvasHTMLCanvasElement | OffscreenCanvasRequiredCanvas element for rendering
widthnumberCanvas width or 1920Output width in pixels
heightnumberCanvas height or 1080Output height in pixels
backgroundColorstring'#000000'Background color (CSS color string)
enableAudiobooleantrueEnable WebAudio playback for audio layers
workerboolean | CompositorWorkerOptionsfalseRender in an OffscreenCanvas worker (HTMLCanvasElement only)

Example

typescript
import { Compositor } from '@mediafox/core';

const canvas = document.querySelector('canvas');
const compositor = new Compositor({
  canvas,
  width: 1920,
  height: 1080,
  backgroundColor: '#000000'
});

Worker Rendering

Use OffscreenCanvas + Web Worker to move compositing work off the main thread:

typescript
import { Compositor } from '@mediafox/core';
import CompositorWorkerUrl from '@mediafox/core/compositor-worker?worker&url';

const compositor = new Compositor({
  canvas,
  width: 1920,
  height: 1080,
  worker: {
    enabled: true,
    url: CompositorWorkerUrl,
    type: 'module'
  }
});

The ?worker&url suffix tells Vite to bundle the worker with all dependencies (including mediabunny from your node_modules) and return the URL.

Worker behavior:

  • Rendering runs in a Web Worker using OffscreenCanvas
  • Audio playback stays on the main thread for stable WebAudio scheduling
  • All sources must be loaded through the compositor instance (it proxies to the worker)
  • CompositorSource.getFrameAt() is not available in worker mode
  • Video sources with audio decode audio separately on the main thread

See the Compositor Guide for bundler-specific configuration.

Source Management

loadSource(source, options?)

Loads a video source into the compositor's source pool.

typescript
async loadSource(
  source: MediaSource,
  options?: CompositorSourceOptions
): Promise<CompositorSource>

Parameters:

  • source: Video source (URL, File, Blob, or MediaStream)
  • options: Optional loading configuration

Returns: The loaded compositor source

Example:

typescript
// Load from URL
const video = await compositor.loadSource('https://example.com/video.mp4');

// Load from file
const fileSource = await compositor.loadSource(fileInput.files[0]);

console.log(`Duration: ${video.duration}s`);
console.log(`Size: ${video.width}x${video.height}`);

loadImage(source)

Loads an image source into the compositor's source pool.

typescript
async loadImage(source: string | Blob | File): Promise<CompositorSource>

Parameters:

  • source: Image source (URL, File, or Blob)

Returns: The loaded compositor source

Example:

typescript
const image = await compositor.loadImage('https://example.com/overlay.png');

loadAudio(source, options?)

Loads an audio source into the compositor's source pool.

typescript
async loadAudio(
  source: MediaSource,
  options?: CompositorSourceOptions
): Promise<CompositorSource>

Parameters:

  • source: Audio source (URL, File, Blob, or MediaStream)
  • options: Optional loading configuration

Returns: The loaded compositor source

Example:

typescript
const audio = await compositor.loadAudio('https://example.com/music.mp3');

unloadSource(id)

Unloads a source from the compositor's source pool.

typescript
unloadSource(id: string): boolean

Parameters:

  • id: The source ID to unload

Returns: True if the source was found and unloaded

Example:

typescript
compositor.unloadSource(video.id);

getSource(id)

Gets a source by ID from the source pool.

typescript
getSource(id: string): CompositorSource | undefined

Example:

typescript
const source = compositor.getSource('source-id');
if (source) {
  console.log(`Found source: ${source.id}`);
}

getAllSources()

Gets all sources currently loaded in the source pool.

typescript
getAllSources(): CompositorSource[]

Example:

typescript
const sources = compositor.getAllSources();
console.log(`Loaded ${sources.length} sources`);

Rendering

render(frame)

Renders a composition frame to the canvas. Fetches all layer frames in parallel before drawing to prevent flicker.

typescript
async render(frame: CompositionFrame): Promise<boolean>

Parameters:

  • frame: The composition frame to render

Returns: True if rendering succeeded

Example:

typescript
await compositor.render({
  time: 5.0,
  layers: [
    {
      source: backgroundVideo,
      transform: { opacity: 1 }
    },
    {
      source: overlayImage,
      transform: {
        x: 100,
        y: 50,
        scaleX: 0.5,
        scaleY: 0.5,
        opacity: 0.8,
        rotation: 15
      },
      zIndex: 1
    }
  ]
});

clear()

Clears the canvas with the background color.

typescript
clear(): void

Preview Playback

The compositor includes a built-in playback system for previewing compositions.

preview(options)

Configures the preview playback with a composition callback. Must be called before play() or seek().

typescript
preview(options: PreviewOptions): void

Parameters:

  • options.duration: Total duration in seconds
  • options.loop: Whether to loop playback (optional)
  • options.getComposition: Callback that returns the composition for a given time

Example:

typescript
compositor.preview({
  duration: 30,
  loop: true,
  getComposition: (time) => ({
    time,
    layers: [
      {
        source: video1,
        sourceTime: time,
        transform: { opacity: 1 }
      },
      {
        source: overlay,
        transform: {
          x: 100,
          y: 50,
          opacity: time < 10 ? 1 : 0 // Hide after 10 seconds
        },
        zIndex: 1
      }
    ]
  })
});

play()

Starts playback of the preview composition.

typescript
async play(): Promise<void>

Throws: Error if preview() has not been called first

Example:

typescript
await compositor.play();

pause()

Pauses playback of the preview composition.

typescript
pause(): void

Example:

typescript
compositor.pause();

seek(time)

Seeks to a specific time in the preview composition.

typescript
async seek(time: number): Promise<void>

Parameters:

  • time: Time in seconds to seek to

Example:

typescript
await compositor.seek(15.5); // Seek to 15.5 seconds

Frame Export

exportFrame(time, options?)

Exports a single frame at the specified time as an image blob.

typescript
async exportFrame(
  time: number,
  options?: FrameExportOptions
): Promise<Blob | null>

Parameters:

  • time: Time in seconds to export
  • options.format: Image format ('png', 'jpeg', 'webp')
  • options.quality: JPEG/WebP quality (0-1)

Returns: Image blob or null if export failed

Example:

typescript
// Export PNG
const png = await compositor.exportFrame(5.0);

// Export JPEG with quality
const jpeg = await compositor.exportFrame(5.0, {
  format: 'jpeg',
  quality: 0.9
});

// Download the frame
const url = URL.createObjectURL(png);
const a = document.createElement('a');
a.href = url;
a.download = 'frame.png';
a.click();
URL.revokeObjectURL(url);

State Properties

PropertyTypeDescription
currentTimenumberCurrent playback time in seconds
durationnumberTotal duration of the preview composition in seconds
playingbooleanWhether the compositor is currently playing
pausedbooleanWhether the compositor is currently paused
seekingbooleanWhether the compositor is currently seeking

Example:

typescript
console.log(`Time: ${compositor.currentTime}/${compositor.duration}`);
console.log(`Playing: ${compositor.playing}`);

Dimension Methods

getWidth()

Gets the current canvas width.

typescript
getWidth(): number

Returns: Width in pixels

getHeight()

Gets the current canvas height.

typescript
getHeight(): number

Returns: Height in pixels

resize(width, height)

Resizes the compositor canvas without disposing loaded sources.

typescript
resize(width: number, height: number): void

Parameters:

  • width: New width in pixels
  • height: New height in pixels

Example:

typescript
// Change to portrait orientation
compositor.resize(1080, 1920);

// Change aspect ratio presets
const PRESETS = {
  '16:9': [1920, 1080],
  '9:16': [1080, 1920],
  '4:3': [1440, 1080],
  '1:1': [1080, 1080]
};

compositor.resize(...PRESETS['9:16']);

Events

on(event, listener)

Subscribes to a compositor event.

typescript
on<K extends keyof CompositorEventMap>(
  event: K,
  listener: CompositorEventListener<K>
): () => void

Returns: Unsubscribe function

once(event, listener)

Subscribes to a compositor event for a single invocation.

typescript
once<K extends keyof CompositorEventMap>(
  event: K,
  listener: CompositorEventListener<K>
): () => void

Returns: Unsubscribe function

off(event, listener?)

Unsubscribes from a compositor event.

typescript
off<K extends keyof CompositorEventMap>(
  event: K,
  listener?: CompositorEventListener<K>
): void

Event Types

EventData TypeDescription
playvoidPlayback started
pausevoidPlayback paused
endedvoidPlayback ended
seeking{ time: number }Seeking started
seeked{ time: number }Seeking completed
timeupdate{ currentTime: number }Playback time changed
compositionchangevoidComposition configuration changed
sourceloaded{ id: string, source: CompositorSource }Source loaded
sourceunloaded{ id: string }Source unloaded

Example:

typescript
compositor.on('timeupdate', ({ currentTime }) => {
  updateTimeline(currentTime);
});

compositor.on('ended', () => {
  console.log('Playback finished');
});

compositor.on('sourceloaded', ({ id, source }) => {
  console.log(`Loaded source ${id}: ${source.duration}s`);
});

Lifecycle

dispose()

Disposes the compositor and releases all resources. After disposal, the compositor cannot be used.

typescript
dispose(): void

Example:

typescript
compositor.dispose();

Type Definitions

CompositorSource

A loaded media source.

typescript
interface CompositorSource {
  id: string;
  type: 'video' | 'image' | 'audio';
  duration: number;
  width?: number;
  height?: number;
  getFrameAt(time: number): Promise<CanvasImageSource | null>;
}

CompositionFrame

A single frame of the composition.

typescript
interface CompositionFrame {
  time: number;
  layers: CompositorLayer[];
}

CompositorLayer

A layer in the composition.

typescript
interface CompositorLayer {
  source: CompositorSource;
  sourceTime?: number;      // Time in source (defaults to frame time)
  transform?: LayerTransform;
  zIndex?: number;          // Layer order (higher = on top)
  visible?: boolean;        // Whether to render (default: true)
}

LayerTransform

Transform properties for a layer.

typescript
interface LayerTransform {
  x?: number;           // X position (default: 0)
  y?: number;           // Y position (default: 0)
  width?: number;       // Override width
  height?: number;      // Override height
  scaleX?: number;      // Horizontal scale (default: 1)
  scaleY?: number;      // Vertical scale (default: 1)
  rotation?: number;    // Rotation in degrees (default: 0)
  opacity?: number;     // Opacity 0-1 (default: 1)
  anchorX?: number;     // Anchor point X 0-1 (default: 0.5)
  anchorY?: number;     // Anchor point Y 0-1 (default: 0.5)
}

PreviewOptions

Options for preview playback.

typescript
interface PreviewOptions {
  duration: number;
  fps?: number;
  loop?: boolean;
  getComposition: (time: number) => CompositionFrame;
}

fps caps the preview render rate (useful to reduce CPU while keeping time-based playback).

CompositorWorkerOptions

Options for OffscreenCanvas worker rendering.

typescript
interface CompositorWorkerOptions {
  enabled?: boolean;
  url?: string;
  type?: 'classic' | 'module';
}

When worker is an object, it is treated as enabled by default unless enabled: false is provided.

FrameExportOptions

Options for frame export.

typescript
interface FrameExportOptions {
  format?: 'png' | 'jpeg' | 'webp';
  quality?: number;  // 0-1, for JPEG/WebP
}

Complete Example

Here's a complete example of building a simple video editor preview:

typescript
import { Compositor } from '@mediafox/core';

// Setup
const canvas = document.querySelector('canvas');
const compositor = new Compositor({
  canvas,
  width: 1920,
  height: 1080
});

// Load sources
const background = await compositor.loadSource('background.mp4');
const overlay = await compositor.loadImage('logo.png');
const music = await compositor.loadAudio('soundtrack.mp3');

// Define clips on timeline
const clips = [
  { source: background, start: 0, duration: 30 },
  { source: overlay, start: 5, duration: 10, x: 50, y: 50, scale: 0.3 }
];

// Configure preview
compositor.preview({
  duration: 30,
  loop: false,
  getComposition: (time) => {
    const layers = clips
      .filter(clip => time >= clip.start && time < clip.start + clip.duration)
      .map((clip, i) => ({
        source: clip.source,
        sourceTime: time - clip.start,
        transform: {
          x: clip.x ?? 0,
          y: clip.y ?? 0,
          scaleX: clip.scale ?? 1,
          scaleY: clip.scale ?? 1,
          opacity: 1
        },
        zIndex: i
      }));

    return { time, layers };
  }
});

// Playback controls
document.querySelector('#play').onclick = () => compositor.play();
document.querySelector('#pause').onclick = () => compositor.pause();
document.querySelector('#seek').oninput = (e) => {
  compositor.seek(parseFloat(e.target.value));
};

// Update UI
compositor.on('timeupdate', ({ currentTime }) => {
  document.querySelector('#time').textContent = currentTime.toFixed(2);
  document.querySelector('#seek').value = currentTime;
});

// Export frame
document.querySelector('#export').onclick = async () => {
  const blob = await compositor.exportFrame(compositor.currentTime);
  // Download blob...
};

// Cleanup
window.addEventListener('beforeunload', () => {
  compositor.dispose();
});

MIT License