Skip to main content

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

  1. Boot inside an <iframe> served from the Percus Player origin.
  2. Listen for inbound postMessage commands from the host page.
  3. Load the animation template (Lottie JSON), binding manifest, and personalization data.
  4. Apply data bindings through the BindingEngine.
  5. Drive the Renderer (powered by lottie-web) in response to play/pause/seek commands.
  6. 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

PropertyTypeRequiredDescription
stageElHTMLElementNoDOM element used as the render container. Defaults to document.body.
allowedOriginsstring[]NoWhitelist of origins allowed to send commands. Empty array = allow all (development only).
onDebug(snapshot: PlayerRuntimeDebugSnapshot) => voidNoCalled on every state change with a full snapshot for debugging.
templateLoaderTemplateLoaderNoOverride the default FetchTemplateLoader.
manifestLoaderManifestLoaderNoOverride the default FetchManifestLoader.
dataProviderDataProviderNoOverride the default DefaultDataProvider.
bindingEngineBindingEngineNoOverride the default NoopBindingEngine.
rendererRendererNoOverride the default lottie-web renderer.

State machine

The runtime transitions through four states:

idle → loading → ready
↘ error
StateMeaning
idleWaiting for an INIT command.
loadingFetching template, manifest, and data concurrently.
readyAnimation loaded; responds to play / pause / seek.
errorA 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:

  1. Validates that exactly one of data / dataUrl is provided (or neither for a static template).
  2. Transitions state to loading.
  3. Fetches template, manifest, and data in parallel.
  4. Runs BindingEngine.applyBindings().
  5. Calls Renderer.load() with the resolved template JSON.
  6. Emits PERCUS/READY on success, PERCUS/ERROR on 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

ConcernBehaviour
Origin validationisAllowedOrigin(origin) checks against allowedOrigins. Empty list allows all origins (dev only).
PII protectionPersonalization data is never written to logs, localStorage, or error messages.
Error sanitizationPERCUS/ERROR payloads must not contain raw data values.
targetOriginShould be pinned to the known host origin in production (currently "*").
Iframe sandboxEnforced by the host page, not the player.