Percus Player
The Percus Player is a self-contained iframe runtime that receives commands via the postMessage API, loads a Lottie-based animation template, applies personalization bindings, and renders the result.
Responsibilities
- Boot inside an
<iframe>served from the Percus Player origin. - Listen for inbound
postMessagecommands from the host page. - Load the animation template (Lottie JSON), binding manifest, and personalization data.
- Apply data bindings through the
BindingEngine. - Drive the
Renderer(powered by lottie-web) in response to play/pause/seek commands. - Emit progress and error events back to the host page.
Runtime version
RUNTIME_VERSION = "0.1.0"
Initialization
PlayerRuntime is instantiated once at boot time from main.ts.
import { PlayerRuntime } from "./PlayerRuntime";
const runtime = new PlayerRuntime(options);
runtime.init(); // starts listening for postMessage events
PlayerRuntimeOptions
| Property | Type | Required | Description |
|---|---|---|---|
stageEl | HTMLElement | No | DOM element used as the render container. Defaults to document.body. |
allowedOrigins | string[] | No | Whitelist of origins allowed to send commands. Empty array = allow all (development only). |
onDebug | (snapshot: PlayerRuntimeDebugSnapshot) => void | No | Called on every state change with a full snapshot for debugging. |
templateLoader | TemplateLoader | No | Override the default FetchTemplateLoader. |
manifestLoader | ManifestLoader | No | Override the default FetchManifestLoader. |
dataProvider | DataProvider | No | Override the default DefaultDataProvider. |
bindingEngine | BindingEngine | No | Override the default NoopBindingEngine. |
renderer | Renderer | No | Override the default lottie-web renderer. |
State machine
The runtime transitions through four states:
idle → loading → ready
↘ error
| State | Meaning |
|---|---|
idle | Waiting for an INIT command. |
loading | Fetching template, manifest, and data concurrently. |
ready | Animation loaded; responds to play / pause / seek. |
error | A fatal error occurred; a PERCUS/ERROR message was sent to the host. |
PlayerRuntimeDebugSnapshot
{
state: "idle" | "loading" | "ready" | "error";
lastError?: { code: string; message: string };
connectedOrigin?: string;
playing: boolean;
timeMs: number;
durationMs?: number;
}
Inbound messages (Host → Player)
PERCUS/INIT
Triggers the full load-and-render pipeline.
{
version: 1;
type: "PERCUS/INIT";
payload: {
templateUrl: string; // URL to the Lottie JSON template
manifestUrl: string; // URL to the binding manifest
data?: PersonalizationData; // Inline personalization data (mutually exclusive with dataUrl)
dataUrl?: string; // URL to personalization data (mutually exclusive with data)
config?: Record<string, JsonValue>; // Optional runtime config
requestId?: string; // Correlation ID echoed back in READY
};
}
Behaviour:
- Validates that exactly one of
data/dataUrlis provided (or neither for a static template). - Transitions state to
loading. - Fetches template, manifest, and data in parallel.
- Runs
BindingEngine.applyBindings(). - Calls
Renderer.load()with the resolved template JSON. - Emits
PERCUS/READYon success,PERCUS/ERRORon failure.
PERCUS/PLAY
{ version: 1; type: "PERCUS/PLAY"; payload: {} }
Starts playback and activates the 500 ms progress heartbeat. Ignored if state is not ready.
PERCUS/PAUSE
{ version: 1; type: "PERCUS/PAUSE"; payload: {} }
Pauses playback and stops the heartbeat. Ignored if state is not ready.
PERCUS/SEEK
{
version: 1;
type: "PERCUS/SEEK";
payload: {
timeMs: number; // Target position in milliseconds
};
}
Seeks the renderer to the specified time. Ignored if state is not ready.
Note: The Player Runtime uses milliseconds internally. The SmartEmbed SDK converts from seconds at its boundary.
Outbound events (Player → Host)
PERCUS/READY
Emitted once the animation is fully loaded and bound.
{
version: 1;
type: "PERCUS/READY";
payload: {
playerVersion?: string; // e.g. "0.1.0"
requestId?: string; // Echoed from INIT if provided
};
}
PERCUS/PROGRESS
Emitted every ~500 ms during active playback.
{
version: 1;
type: "PERCUS/PROGRESS";
payload: {
timeMs: number; // Current playback position in milliseconds
durationMs?: number; // Total animation duration in milliseconds (if known)
playing: boolean; // Whether the animation is currently playing
};
}
PERCUS/ERROR
Emitted when a fatal error occurs (network failure, invalid manifest, etc.).
{
version: 1;
type: "PERCUS/ERROR";
payload: {
code: string; // Machine-readable error code (e.g. "LOAD_FAILED")
message: string; // Human-readable description (sanitized – no PII)
details?: unknown; // Optional structured context
};
}
Pluggable modules
All internal processing modules are defined as interfaces, allowing custom implementations to be injected via PlayerRuntimeOptions.
TemplateLoader
interface TemplateLoader {
loadTemplateJson(templateUrl: string): Promise<unknown>;
}
Default: FetchTemplateLoader – performs a plain fetch().
ManifestLoader
interface ManifestLoader {
loadManifestJson(manifestUrl: string): Promise<BindingManifest>;
}
Default: FetchManifestLoader – performs a plain fetch() and validates the result.
BindingManifest schema:
{
version: 1;
bindings: Array<Record<string, unknown>>;
}
DataProvider
interface DataProvider {
getData(input: { data?: PersonalizationData; dataUrl?: string }): Promise<PersonalizationData>;
}
Default: DefaultDataProvider – returns data directly or fetches from dataUrl.
BindingEngine
interface BindingEngine {
applyBindings(input: {
templateJson: unknown;
manifest: BindingManifest;
data: PersonalizationData;
}): Promise<unknown>;
}
Default: NoopBindingEngine – returns the template unmodified (placeholder for student implementation).
Renderer
interface Renderer {
load(templateJson: unknown): Promise<void>;
play(): void;
pause(): void;
seek(timeMs: number): void;
destroy(): void;
getCurrentTimeMs?(): number;
getDurationMs?(): number | undefined;
isPlaying?(): boolean;
}
Default: lottie-web based implementation via LottiePercusPlayer.
Lifecycle
new PlayerRuntime(opts)
└── runtime.init()
└── window.addEventListener("message", handleHostMessage)
└── on PERCUS/INIT
├── TemplateLoader.loadTemplateJson() ┐
├── ManifestLoader.loadManifestJson() ├── parallel
└── DataProvider.getData() ┘
└── BindingEngine.applyBindings()
└── Renderer.load()
└── postMessage PERCUS/READY
└── on PERCUS/PLAY → Renderer.play() + start heartbeat
└── on PERCUS/PAUSE → Renderer.pause() + stop heartbeat
└── on PERCUS/SEEK → Renderer.seek(timeMs)
runtime.dispose() // removes event listener, destroys renderer
Planned features
The following capabilities are not yet implemented but are required to reach feature parity with the product vision. Each section describes the expected message shape so design and implementation can begin.
PERCUS/PLAY_COMPLETE and PERCUS/PLAY_INCOMPLETE
Emitted when playback ends. The player must distinguish between a natural end-of-animation (PLAY_COMPLETE) and a case where the host called destroy() or navigated away before the end (PLAY_INCOMPLETE). These are the foundation of any engagement analytics story.
// Natural completion
{ version: 1; type: "PERCUS/PLAY_COMPLETE"; payload: { durationMs: number } }
// Viewer left before the end
{ version: 1; type: "PERCUS/PLAY_INCOMPLETE"; payload: { timeMs: number; durationMs: number } }
PERCUS/CTA
Emitted when the animation reaches a call-to-action marker defined in the binding manifest. The host page uses this to trigger business actions (open a form, redirect to a product page, etc.).
{
version: 1;
type: "PERCUS/CTA";
payload: {
ctaId: string; // Identifier defined in the manifest
label?: string; // Human-readable label for display
url?: string; // Optional target URL
timeMs: number; // Position in the animation when triggered
data?: unknown; // Arbitrary metadata from the manifest
};
}
PERCUS/EVENT
Generic in-animation event marker. Allows template designers to place named triggers at any point in the timeline without requiring a new message type.
{
version: 1;
type: "PERCUS/EVENT";
payload: {
eventId: string;
timeMs: number;
data?: unknown;
};
}
PERCUS/CHAPTER_ENTER and PERCUS/CHAPTER_EXIT
Emitted as playback crosses chapter boundaries declared in the manifest. Enables the host page to render a chapter navigation menu or sync external UI elements.
{ version: 1; type: "PERCUS/CHAPTER_ENTER"; payload: { chapterId: string; label?: string; timeMs: number } }
{ version: 1; type: "PERCUS/CHAPTER_EXIT"; payload: { chapterId: string; timeMs: number } }
PERCUS/AUTOPLAY_FAILURE
Emitted when the browser's autoplay policy prevents playback from starting automatically. The host page should react by showing a visible play button or unmuted prompt.
{ version: 1; type: "PERCUS/AUTOPLAY_FAILURE"; payload: { reason: string } }
Analytics tracker interface
The PlayerRuntimeOptions will gain an optional tracker field accepting an implementation of a PercusTracker interface. This separates analytics concerns from the runtime and allows different tracking backends (Percus analytics service, Google Analytics, custom) to be injected.
interface PercusTracker {
onEvent(eventType: string, payload: unknown): void;
}
Closed captions support
The binding manifest will be extended to reference subtitle tracks (VTT/SRT). The Renderer interface will gain optional loadCaptions() and setCaptionsEnabled() methods, and a new PERCUS/CAPTIONS_AVAILABLE event will be emitted after loading so the host page can show a CC toggle.
Security
| Concern | Behaviour |
|---|---|
| Origin validation | isAllowedOrigin(origin) checks against allowedOrigins. Empty list allows all origins (dev only). |
| PII protection | Personalization data is never written to logs, localStorage, or error messages. |
| Error sanitization | PERCUS/ERROR payloads must not contain raw data values. |
targetOrigin | Should be pinned to the known host origin in production (currently "*"). |
| Iframe sandbox | Enforced by the host page, not the player. |