The compose() function combines clean video with timeline-based cursor and HUD overlays to produce the final output with visual effects.
Function Signature
await compose(
cleanVideoPath: string,
timelineData: TimelineData,
outputPath: string,
options?: ComposeOptions
)
Parameters
Path to the input video file without overlays
Timeline data containing cursor positions, HUD states, and theme configurationGet this from timeline.toJSON() or recorder.getTimelineData()
Path where the composed video will be saved (.mp4, .webm, or .gif)
Composition optionsinterface ComposeOptions {
sfx?: SfxConfig;
crf?: number;
}
ComposeOptions
Sound effects configurationinterface SfxConfig {
click?: 1 | 2 | 3 | 4 | string; // Built-in or custom sound
key?: 1 | 2 | 3 | 4 | string;
}
Built-in sounds:
1 - Default click/key sound
2 - Softer variant
3 - Mechanical variant
4 - Subtle variant
Or provide a custom file path:sfx: {
click: './sounds/custom-click.mp3',
key: './sounds/custom-key.mp3'
}
Constant Rate Factor for video encoding quality
0 - Lossless (very large files)
18 - Visually lossless (recommended)
23 - Default quality
28 - Lower quality
51 - Worst quality
How It Works
The composition process:
- Reads the clean input video
- Renders cursor overlay for each frame based on timeline position
- Renders HUD overlay when keyboard shortcuts are shown
- Composites overlays onto video frames
- Applies sound effects at event timestamps
- Encodes final output in requested format
Usage Example
Basic Composition
import { compose, InteractionTimeline } from '@webreel/core';
// Load timeline from file
const timelineData = JSON.parse(
await fs.readFile('./timeline.json', 'utf-8')
);
// Compose video with overlays
await compose(
'./clean-video.mp4',
timelineData,
'./final-output.mp4'
);
With Sound Effects
import { compose } from '@webreel/core';
await compose(
'./clean-video.mp4',
timelineData,
'./output-with-audio.mp4',
{
sfx: {
click: 1, // Use built-in click sound variant 1
key: 2 // Use built-in key sound variant 2
}
}
);
Custom Quality
await compose(
'./clean-video.mp4',
timelineData,
'./high-quality.mp4',
{
crf: 15 // Higher quality than default
}
);
Complete Workflow
import {
launchChrome,
connectCDP,
Recorder,
RecordingContext,
InteractionTimeline,
compose,
clickAt
} from '@webreel/core';
const chrome = await launchChrome();
const client = await connectCDP(chrome.wsUrl);
// Step 1: Record clean video with timeline
const timeline = new InteractionTimeline(1920, 1080, {
fps: 60,
hud: {
background: 'rgba(0,0,0,0.8)',
fontSize: 56
}
});
const ctx = new RecordingContext();
ctx.setMode('record');
ctx.setTimeline(timeline);
const recorder = new Recorder(1920, 1080, { fps: 60 });
recorder.setTimeline(timeline);
await recorder.start(client, './clean.mp4', ctx);
// Perform interactions
await navigate(client, 'https://example.com');
await clickAt(ctx, client, 500, 300);
await recorder.stop();
// Step 2: Get timeline data
const timelineData = timeline.toJSON();
// Step 3: Compose final video
await compose(
recorder.getTempVideoPath(), // or './clean.mp4'
timelineData,
'./final-demo.mp4',
{
sfx: { click: 1, key: 2 },
crf: 18
}
);
await client.close();
await chrome.process.kill();
Supported output formats based on file extension:
MP4 (recommended)
await compose(cleanPath, timeline, './output.mp4', {
sfx: { click: 1, key: 1 }
});
- H.264 video codec
- AAC audio codec (when sfx provided)
- Best compatibility
- Optimized for web streaming
WebM
await compose(cleanPath, timeline, './output.webm', {
sfx: { click: 1, key: 1 }
});
- VP9 video codec
- Opus audio codec (when sfx provided)
- Good for web
- Open format
GIF
await compose(cleanPath, timeline, './output.gif');
- Animated GIF
- No audio support
- Larger file sizes
- Universal compatibility
Caching
The compositor caches:
- Scaled cursor images for different scales
- Rendered HUD overlays for each unique label combination
- Overlay frames with matching cursor/HUD state
This significantly speeds up processing for recordings with repetitive states.
Memory Usage
For long recordings:
- Frames are streamed to ffmpeg incrementally
- Only active overlays are kept in memory
- Clean video is read by ffmpeg, not loaded into memory
Error Handling
import { compose } from '@webreel/core';
try {
await compose(
'./clean.mp4',
timelineData,
'./output.mp4',
{ sfx: { click: 1, key: 1 } }
);
console.log('Composition complete!');
} catch (error) {
console.error('Composition failed:', error);
// Error includes ffmpeg stderr output for debugging
}
Timeline Data Structure
The TimelineData structure passed to compose:
interface TimelineData {
fps: number;
width: number;
height: number;
zoom: number;
theme: {
cursorSvg: string;
cursorSize: number;
cursorHotspot: 'top-left' | 'center';
hud: {
background: string;
color: string;
fontSize: number;
fontFamily: string;
borderRadius: number;
position: 'top' | 'bottom';
};
};
frames: Array<{
cursor: { x: number; y: number; scale: number };
hud: { labels: string[] } | null;
}>;
events: Array<{ type: 'click' | 'key'; timeMs: number }>;
}
You can modify this data before compositing to:
- Adjust cursor positions
- Change HUD labels
- Modify theme colors
- Add or remove events
- Change zoom level
const data = timeline.toJSON();
// Adjust zoom for larger cursor
data.zoom = 1.5;
// Change HUD position
data.theme.hud.position = 'top';
// Compose with modifications
await compose('./clean.mp4', data, './output.mp4');