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.
constructor(options: CompositorOptions)Options
| Property | Type | Default | Description |
|---|---|---|---|
canvas | HTMLCanvasElement | OffscreenCanvas | Required | Canvas element for rendering |
width | number | Canvas width or 1920 | Output width in pixels |
height | number | Canvas height or 1080 | Output height in pixels |
backgroundColor | string | '#000000' | Background color (CSS color string) |
enableAudio | boolean | true | Enable WebAudio playback for audio layers |
worker | boolean | CompositorWorkerOptions | false | Render in an OffscreenCanvas worker (HTMLCanvasElement only) |
Example
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:
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.
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:
// 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.
async loadImage(source: string | Blob | File): Promise<CompositorSource>Parameters:
source: Image source (URL, File, or Blob)
Returns: The loaded compositor source
Example:
const image = await compositor.loadImage('https://example.com/overlay.png');loadAudio(source, options?)
Loads an audio source into the compositor's source pool.
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:
const audio = await compositor.loadAudio('https://example.com/music.mp3');unloadSource(id)
Unloads a source from the compositor's source pool.
unloadSource(id: string): booleanParameters:
id: The source ID to unload
Returns: True if the source was found and unloaded
Example:
compositor.unloadSource(video.id);getSource(id)
Gets a source by ID from the source pool.
getSource(id: string): CompositorSource | undefinedExample:
const source = compositor.getSource('source-id');
if (source) {
console.log(`Found source: ${source.id}`);
}getAllSources()
Gets all sources currently loaded in the source pool.
getAllSources(): CompositorSource[]Example:
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.
async render(frame: CompositionFrame): Promise<boolean>Parameters:
frame: The composition frame to render
Returns: True if rendering succeeded
Example:
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.
clear(): voidPreview 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().
preview(options: PreviewOptions): voidParameters:
options.duration: Total duration in secondsoptions.loop: Whether to loop playback (optional)options.getComposition: Callback that returns the composition for a given time
Example:
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.
async play(): Promise<void>Throws: Error if preview() has not been called first
Example:
await compositor.play();pause()
Pauses playback of the preview composition.
pause(): voidExample:
compositor.pause();seek(time)
Seeks to a specific time in the preview composition.
async seek(time: number): Promise<void>Parameters:
time: Time in seconds to seek to
Example:
await compositor.seek(15.5); // Seek to 15.5 secondsFrame Export
exportFrame(time, options?)
Exports a single frame at the specified time as an image blob.
async exportFrame(
time: number,
options?: FrameExportOptions
): Promise<Blob | null>Parameters:
time: Time in seconds to exportoptions.format: Image format ('png', 'jpeg', 'webp')options.quality: JPEG/WebP quality (0-1)
Returns: Image blob or null if export failed
Example:
// 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
| Property | Type | Description |
|---|---|---|
currentTime | number | Current playback time in seconds |
duration | number | Total duration of the preview composition in seconds |
playing | boolean | Whether the compositor is currently playing |
paused | boolean | Whether the compositor is currently paused |
seeking | boolean | Whether the compositor is currently seeking |
Example:
console.log(`Time: ${compositor.currentTime}/${compositor.duration}`);
console.log(`Playing: ${compositor.playing}`);Dimension Methods
getWidth()
Gets the current canvas width.
getWidth(): numberReturns: Width in pixels
getHeight()
Gets the current canvas height.
getHeight(): numberReturns: Height in pixels
resize(width, height)
Resizes the compositor canvas without disposing loaded sources.
resize(width: number, height: number): voidParameters:
width: New width in pixelsheight: New height in pixels
Example:
// 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.
on<K extends keyof CompositorEventMap>(
event: K,
listener: CompositorEventListener<K>
): () => voidReturns: Unsubscribe function
once(event, listener)
Subscribes to a compositor event for a single invocation.
once<K extends keyof CompositorEventMap>(
event: K,
listener: CompositorEventListener<K>
): () => voidReturns: Unsubscribe function
off(event, listener?)
Unsubscribes from a compositor event.
off<K extends keyof CompositorEventMap>(
event: K,
listener?: CompositorEventListener<K>
): voidEvent Types
| Event | Data Type | Description |
|---|---|---|
play | void | Playback started |
pause | void | Playback paused |
ended | void | Playback ended |
seeking | { time: number } | Seeking started |
seeked | { time: number } | Seeking completed |
timeupdate | { currentTime: number } | Playback time changed |
compositionchange | void | Composition configuration changed |
sourceloaded | { id: string, source: CompositorSource } | Source loaded |
sourceunloaded | { id: string } | Source unloaded |
Example:
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.
dispose(): voidExample:
compositor.dispose();Type Definitions
CompositorSource
A loaded media source.
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.
interface CompositionFrame {
time: number;
layers: CompositorLayer[];
}CompositorLayer
A layer in the composition.
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.
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.
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.
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.
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:
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();
});