Go to https://www.marketplace.recordly.dev/extensions for full, regularly updated documentation
Recordly extensions run in the editor renderer and use a permission-gated host API. They can draw into the render pipeline, react to playback and export events, register cursor effects, add settings panels, and contribute packaged assets such as frames, wallpapers, and cursor styles.
For local user-installed extensions, use Extensions -> Open Directory in the app. Recordly stores them in the app userData/extensions directory. This repo also includes installable example bundles under extension-examples/.
my-extension/
recordly-extension.json
index.js
{
"id": "com.example.my-extension",
"name": "My Extension",
"version": "1.0.0",
"description": "A short description",
"author": "Your Name",
"license": "MIT",
"main": "index.js",
"permissions": ["render"]
}export function activate(api) {
api.log("Hello from my extension");
}
export function deactivate() {}Extensions can be authored in TypeScript. Import the RecordlyExtensionAPI type from types.ts in this repo for full IDE auto-complete and compile-time checks:
import type { RecordlyExtensionAPI } from "./types";
export function activate(api: RecordlyExtensionAPI) {
api.registerRenderHook("final", (ctx) => {
ctx.ctx.fillStyle = "rgba(255,255,255,0.1)";
ctx.ctx.fillRect(0, 0, ctx.width, 30);
});
}
export function deactivate() {}Recordly loads the main entry from the manifest, which must be a .js file. If you author in TypeScript, compile or bundle to JavaScript before packaging. A tsconfig.json with "module": "ESNext" and "target": "ESNext" works well since extensions run in a Chromium renderer.
Extensions can include screenshots in the manifest to show preview images in the marketplace:
{
"id": "com.example.my-extension",
"screenshots": [
"screenshots/preview-1.png",
"screenshots/preview-2.png"
]
}Paths are relative to the extension root. The marketplace displays screenshots in a carousel on both the in-app detail modal and the web detail page.
recordly-extension.json is validated both when the extension loads and when a zip is uploaded to the marketplace.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique identifier, for example yourname.cool-effect |
name |
string |
Yes | Human-readable name shown in the UI |
version |
string |
Yes | Strict semver version, for example 1.0.0 |
description |
string |
Yes | One-line summary |
author |
string |
No | Author or organisation |
homepage |
string |
No | HTTPS homepage or repository URL |
license |
string |
No | SPDX license identifier |
engine |
string |
No | Minimum supported Recordly version |
icon |
string |
No | Relative path to a PNG icon |
screenshots |
string[] |
No | Relative paths to preview images shown in the marketplace |
main |
string |
Yes | Relative entry point JS file |
permissions |
string[] |
Yes | Required capabilities |
contributes |
object |
No | Metadata for packaged frames, cursor styles, sounds, wallpapers, and webcam frames |
contributes is metadata only today. Recordly does not auto-register runtime behavior from the manifest. Use activate() to call APIs such as registerFrame(), registerWallpaper(), registerCursorStyle(), registerSettingsPanel(), and playSound().
| Permission | Grants access to |
|---|---|
render |
Render hook registration |
cursor |
Cursor telemetry and cursor effect registration |
audio |
Bundled sound playback |
timeline |
Playback and timeline events |
ui |
Settings panels and device frame registration |
assets |
Bundled asset resolution plus wallpaper and cursor style registration |
export |
Export lifecycle events |
Render hooks draw into hookCtx.ctx, a CanvasRenderingContext2D, at specific phases.
| Phase | Preview | Export | Notes |
|---|---|---|---|
background |
Reserved | Reserved | Exists in the type surface but is not dispatched yet |
post-video |
Yes | Yes | Runs inside the scene transform |
post-zoom |
Yes | Yes | Runs inside the scene transform |
post-cursor |
Yes | Yes | Runs inside the scene transform |
post-webcam |
Yes | Yes | Runs after the built-in transform |
post-annotations |
Yes | Yes | Runs after the built-in transform |
final |
Yes | Yes | Last pass for HUD-style overlays |
post-video,post-zoom, andpost-cursoralready follow zoom and motion in preview and export.post-webcam,post-annotations, andfinalrun after Recordly restores the canvas transform. UsesceneTransformmanually if you want those overlays to move with the scene.
{
width: number;
height: number;
timeMs: number;
durationMs: number;
cursor: { cx: number; cy: number; interactionType?: string } | null;
smoothedCursor?: {
cx: number;
cy: number;
trail: Array<{ cx: number; cy: number }>;
} | null;
ctx: CanvasRenderingContext2D;
videoLayout?: {
maskRect: { x: number; y: number; width: number; height: number };
borderRadius: number;
padding: number;
};
zoom?: { scale: number; focusX: number; focusY: number; progress: number };
sceneTransform?: { scale: number; x: number; y: number };
shadow?: { enabled: boolean; intensity: number };
getPixelColor(x: number, y: number): { r: number; g: number; b: number; a: number };
getAverageSceneColor(): { r: number; g: number; b: number; a: number };
getEdgeAverageColor(edgeWidth?: number): { r: number; g: number; b: number; a: number };
getDominantColors(count?: number): Array<{ r: number; g: number; b: number; frequency: number }>;
}Use videoLayout.maskRect and videoLayout.borderRadius for scene-relative sizing. That gives you true scene borders and correctly shaped rounded corners instead of canvas-wide overlays.
Cursor effect callbacks run each frame after a click until they return false.
api.registerCursorEffect((ctx) => {
const progress = ctx.elapsedMs / 400;
if (progress >= 1) return false;
const sceneWidth = ctx.videoLayout?.maskRect.width ?? ctx.width;
const x = ctx.cx * ctx.width;
const y = ctx.cy * ctx.height;
const radius = sceneWidth * 0.03 * progress;
ctx.ctx.beginPath();
ctx.ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.ctx.stroke();
return true;
});CursorEffectContext now includes videoLayout, zoom, and sceneTransform, so effects can scale relative to the scene instead of the full canvas.
api.registerRenderHook(phase, hook);
api.registerCursorEffect(effect);
api.registerFrame(frame);
api.registerWallpaper(wallpaper);
api.registerCursorStyle(cursorStyle);
api.registerSettingsPanel(panel);
api.on(event, handler);Every registration returns a dispose function.
api.getSetting(settingId);
api.setSetting(settingId, value);
api.onSettingChange((settingId, value) => {});
api.getAllSettings();Supported settings field types are toggle, slider, select, color, and text.
api.resolveAsset("images/overlay.png");
api.playSound("sounds/click.mp3", { volume: 0.8 });
api.log("hello", payload);api.getVideoInfo();
api.getVideoLayout();
api.getCursorAt(timeMs);
api.getSmoothedCursor();
api.getZoomState();
api.getShadowConfig();
api.getKeystrokesInRange(startMs, endMs);
api.getAspectRatio();
api.getActiveFrame();
api.isExtensionActive(extensionId);
api.getPlaybackState();
api.getCanvasDimensions();
api.drawIcon(ctx, "Sparkle", 100, 100, 20, "#2563EB", "regular");Extensions can draw icons from Recordly's bundled Phosphor icon set directly on a canvas context:
api.drawIcon(
ctx,
"ArrowClockwise", // icon name from @phosphor-icons/react
120, // x (center)
80, // y (center)
18, // size in px
"#ffffff", // color
"bold", // optional weight: thin | light | regular | bold | fill
);This is useful for lightweight overlays and avoids bundling your own icon assets.
api.registerSettingsPanel({
id: "my-settings",
label: "My Extension",
icon: "sparkles",
parentSection: "cursor",
fields: [
{ id: "enabled", label: "Enable", type: "toggle", defaultValue: true },
{ id: "size", label: "Size", type: "slider", defaultValue: 1, min: 0.1, max: 3, step: 0.1 },
{
id: "style",
label: "Style",
type: "select",
defaultValue: "ripple",
options: [{ label: "Ripple", value: "ripple" }, { label: "Pulse", value: "pulse" }],
},
{ id: "color", label: "Color", type: "color", defaultValue: "#2563EB" },
],
});Use parentSection to nest your panel inside an existing area such as cursor or scene.
| Event | Permission | Description |
|---|---|---|
playback:timeupdate |
timeline |
Fires each playback tick |
playback:play |
timeline |
Playback started |
playback:pause |
timeline |
Playback paused |
cursor:click |
cursor |
Cursor click detected |
cursor:move |
cursor |
Cursor move detected |
timeline:region-added |
timeline |
Region added |
timeline:region-removed |
timeline |
Region removed |
export:start |
export |
Export started |
export:frame |
export |
A frame was rendered during export |
export:complete |
export |
Export finished |
registerFrame()is the runtime API for device frames. Use adraw(ctx, width, height)function when possible for resolution-independent output.registerWallpaper()contributes scene backgrounds.registerCursorStyle()contributes cursor image packs.- All three rely on packaged files relative to the extension root.
- Discovery: Recordly scans built-in extensions and the user extensions directory.
- Activation:
activate(api)runs and you register hooks, effects, panels, and assets. - Runtime: Registered callbacks execute in preview and export according to their phase.
- Deactivation:
deactivate()runs and all registrations are automatically disposed.
extension-examples/webadderall.more-wallpapersshows a user-installable wallpaper bundle that registers 180 packaged wallpapers throughregisterWallpaper().