11 KiB
Recordly Extension API
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.
Quick Start
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/.
Minimum Extension
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() {}
TypeScript
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.
Manifest Screenshots
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.
Manifest
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().
Permissions
| 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 Pipeline
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 |
Inside vs Outside the Scene Transform
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.
RenderHookContext
{
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 Effects
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 Surface
Registration
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.
Settings
api.getSetting(settingId);
api.setSetting(settingId, value);
api.onSettingChange((settingId, value) => {});
api.getAllSettings();
Supported settings field types are toggle, slider, select, color, and text.
Assets and Audio
api.resolveAsset("images/overlay.png");
api.playSound("sounds/click.mp3", { volume: 0.8 });
api.log("hello", payload);
Read-only Queries
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");
Drawing Icons
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.
Settings Panels
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.
Events
| 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 |
Frames, Wallpapers, and Cursor Styles
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.
Lifecycle
- 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.
Examples
extension-examples/webadderall.more-wallpapersshows a user-installable wallpaper bundle that registers 180 packaged wallpapers throughregisterWallpaper().