Percus Player
El Percus Player es un runtime de iframe autocontenido que recibe comandos a través de la API postMessage, carga un template de animación basado en Lottie, aplica bindings de personalización y renderiza el resultado.
Responsabilidades
- Arrancar dentro de un
<iframe>servido desde el origen del Percus Player. - Escuchar comandos
postMessageentrantes desde la página host. - Cargar el template de animación (Lottie JSON), el manifest de bindings y los datos de personalización.
- Aplicar los bindings de datos mediante el
BindingEngine. - Controlar el
Renderer(basado en lottie-web) en respuesta a los comandos de reproducción/pausa/seek. - Emitir eventos de progreso y error de vuelta a la página host.
Versión del runtime
RUNTIME_VERSION = "0.1.0"
Inicialización
PlayerRuntime se instancia una vez al arrancar desde main.ts.
import { PlayerRuntime } from "./PlayerRuntime";
const runtime = new PlayerRuntime(options);
runtime.init(); // comienza a escuchar eventos postMessage
PlayerRuntimeOptions
| Propiedad | Tipo | Requerido | Descripción |
|---|---|---|---|
stageEl | HTMLElement | No | Elemento DOM usado como contenedor de renderizado. Por defecto document.body. |
allowedOrigins | string[] | No | Lista blanca de orígenes autorizados a enviar comandos. Array vacío = permitir todos (solo desarrollo). |
onDebug | (snapshot: PlayerRuntimeDebugSnapshot) => void | No | Se llama en cada cambio de estado con un snapshot completo para depuración. |
templateLoader | TemplateLoader | No | Reemplaza el FetchTemplateLoader por defecto. |
manifestLoader | ManifestLoader | No | Reemplaza el FetchManifestLoader por defecto. |
dataProvider | DataProvider | No | Reemplaza el DefaultDataProvider por defecto. |
bindingEngine | BindingEngine | No | Reemplaza el NoopBindingEngine por defecto. |
renderer | Renderer | No | Reemplaza el renderer de lottie-web por defecto. |
Máquina de estados
El runtime transita por cuatro estados:
idle → loading → ready
↘ error
| Estado | Significado |
|---|---|
idle | Esperando un comando INIT. |
loading | Descargando template, manifest y datos concurrentemente. |
ready | Animación cargada; responde a play / pause / seek. |
error | Ocurrió un error fatal; se envió un mensaje PERCUS/ERROR al host. |
PlayerRuntimeDebugSnapshot
{
state: "idle" | "loading" | "ready" | "error";
lastError?: { code: string; message: string };
connectedOrigin?: string;
playing: boolean;
timeMs: number;
durationMs?: number;
}
Mensajes entrantes (Host → Player)
PERCUS/INIT
Activa el pipeline completo de carga y renderizado.
{
version: 1;
type: "PERCUS/INIT";
payload: {
templateUrl: string; // URL al template Lottie JSON
manifestUrl: string; // URL al manifest de bindings
data?: PersonalizationData; // Datos de personalización inline (mutuamente exclusivo con dataUrl)
dataUrl?: string; // URL para obtener los datos (mutuamente exclusivo con data)
config?: Record<string, JsonValue>; // Configuración opcional del runtime
requestId?: string; // ID de correlación que se devuelve en READY
};
}
Comportamiento:
- Valida que exactamente uno de
data/dataUrlesté presente (o ninguno para templates estáticos). - Transiciona el estado a
loading. - Descarga template, manifest y datos en paralelo.
- Ejecuta
BindingEngine.applyBindings(). - Llama a
Renderer.load()con el JSON del template resuelto. - Emite
PERCUS/READYen caso de éxito,PERCUS/ERRORen caso de fallo.
PERCUS/PLAY
{ version: 1; type: "PERCUS/PLAY"; payload: {} }
Inicia la reproducción y activa el heartbeat de progreso cada 500 ms. Se ignora si el estado no es ready.
PERCUS/PAUSE
{ version: 1; type: "PERCUS/PAUSE"; payload: {} }
Pausa la reproducción y detiene el heartbeat. Se ignora si el estado no es ready.
PERCUS/SEEK
{
version: 1;
type: "PERCUS/SEEK";
payload: {
timeMs: number; // Posición objetivo en milisegundos
};
}
Lleva el renderer a la posición indicada. Se ignora si el estado no es ready.
Nota: El Player Runtime trabaja internamente en milisegundos. El SmartEmbed SDK convierte desde segundos en su frontera.
Eventos salientes (Player → Host)
PERCUS/READY
Emitido una vez que la animación está completamente cargada y vinculada.
{
version: 1;
type: "PERCUS/READY";
payload: {
playerVersion?: string; // ej. "0.1.0"
requestId?: string; // Devuelto desde INIT si se proporcionó
};
}
PERCUS/PROGRESS
Emitido aproximadamente cada 500 ms durante la reproducción activa.
{
version: 1;
type: "PERCUS/PROGRESS";
payload: {
timeMs: number; // Posición actual en milisegundos
durationMs?: number; // Duración total en milisegundos (si se conoce)
playing: boolean; // Si la animación está reproduciéndose actualmente
};
}
PERCUS/ERROR
Emitido cuando ocurre un error fatal (fallo de red, manifest inválido, etc.).
{
version: 1;
type: "PERCUS/ERROR";
payload: {
code: string; // Código de error legible por máquina (ej. "LOAD_FAILED")
message: string; // Descripción legible por humano (sanitizada – sin PII)
details?: unknown; // Contexto estructurado opcional
};
}
Módulos intercambiables
Todos los módulos de procesamiento interno están definidos como interfaces, permitiendo inyectar implementaciones personalizadas mediante PlayerRuntimeOptions.
TemplateLoader
interface TemplateLoader {
loadTemplateJson(templateUrl: string): Promise<unknown>;
}
Por defecto: FetchTemplateLoader – realiza un fetch() simple.
ManifestLoader
interface ManifestLoader {
loadManifestJson(manifestUrl: string): Promise<BindingManifest>;
}
Por defecto: FetchManifestLoader – realiza un fetch() simple y valida el resultado.
Esquema de BindingManifest:
{
version: 1;
bindings: Array<Record<string, unknown>>;
}
DataProvider
interface DataProvider {
getData(input: { data?: PersonalizationData; dataUrl?: string }): Promise<PersonalizationData>;
}
Por defecto: DefaultDataProvider – devuelve data directamente o descarga desde dataUrl.
BindingEngine
interface BindingEngine {
applyBindings(input: {
templateJson: unknown;
manifest: BindingManifest;
data: PersonalizationData;
}): Promise<unknown>;
}
Por defecto: NoopBindingEngine – devuelve el template sin modificar (placeholder para implementación estudiantil).
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;
}
Por defecto: implementación basada en lottie-web mediante LottiePercusPlayer.
Ciclo de vida
new PlayerRuntime(opts)
└── runtime.init()
└── window.addEventListener("message", handleHostMessage)
└── al recibir PERCUS/INIT
├── TemplateLoader.loadTemplateJson() ┐
├── ManifestLoader.loadManifestJson() ├── en paralelo
└── DataProvider.getData() ┘
└── BindingEngine.applyBindings()
└── Renderer.load()
└── postMessage PERCUS/READY
└── al recibir PERCUS/PLAY → Renderer.play() + iniciar heartbeat
└── al recibir PERCUS/PAUSE → Renderer.pause() + detener heartbeat
└── al recibir PERCUS/SEEK → Renderer.seek(timeMs)
runtime.dispose() // elimina el event listener y destruye el renderer
Funcionalidades planificadas
Las siguientes capacidades aún no están implementadas pero son necesarias para alcanzar la visión del producto. Cada sección describe la forma de mensaje esperada para que el diseño y la implementación puedan comenzar.
PERCUS/PLAY_COMPLETE y PERCUS/PLAY_INCOMPLETE
Se emiten cuando la reproducción termina. El player debe distinguir entre un fin natural de la animación (PLAY_COMPLETE) y el caso en que el host llamó a destroy() o navegó antes de que terminara (PLAY_INCOMPLETE). Estos son la base de cualquier historia de analíticas de engagement.
// Finalización natural
{ version: 1; type: "PERCUS/PLAY_COMPLETE"; payload: { durationMs: number } }
// El usuario salió antes del final
{ version: 1; type: "PERCUS/PLAY_INCOMPLETE"; payload: { timeMs: number; durationMs: number } }
PERCUS/CTA
Se emite cuando la animación alcanza un marcador de llamada a la acción definido en el manifest de bindings. La página host utiliza esto para disparar acciones de negocio (abrir un formulario, redirigir a una página de producto, etc.).
{
version: 1;
type: "PERCUS/CTA";
payload: {
ctaId: string; // Identificador definido en el manifest
label?: string; // Etiqueta legible para mostrar
url?: string; // URL de destino opcional
timeMs: number; // Posición en la animación cuando se disparó
data?: unknown; // Metadatos arbitrarios del manifest
};
}
PERCUS/EVENT
Marcador de evento genérico dentro de la animación. Permite a los diseñadores de templates colocar triggers con nombre en cualquier punto de la línea de tiempo sin requerir un nuevo tipo de mensaje.
{
version: 1;
type: "PERCUS/EVENT";
payload: {
eventId: string;
timeMs: number;
data?: unknown;
};
}
PERCUS/CHAPTER_ENTER y PERCUS/CHAPTER_EXIT
Se emiten cuando la reproducción cruza los límites de capítulo declarados en el manifest. Permite a la página host renderizar un menú de navegación por capítulos o sincronizar elementos de UI externos.
{ 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
Se emite cuando la política de autoplay del navegador impide que la reproducción comience automáticamente. La página host debe reaccionar mostrando un botón de play visible o un aviso para el usuario.
{ version: 1; type: "PERCUS/AUTOPLAY_FAILURE"; payload: { reason: string } }
Interfaz de tracker de analíticas
PlayerRuntimeOptions ganará un campo opcional tracker que acepta una implementación de la interfaz PercusTracker. Esto separa las preocupaciones de analíticas del runtime y permite inyectar distintos backends de tracking (servicio de analíticas de Percus, Google Analytics, personalizado).
interface PercusTracker {
onEvent(eventType: string, payload: unknown): void;
}
Soporte de subtítulos
El manifest de bindings se extenderá para referenciar pistas de subtítulos (VTT/SRT). La interfaz Renderer ganará métodos opcionales loadCaptions() y setCaptionsEnabled(), y se emitirá un nuevo evento PERCUS/CAPTIONS_AVAILABLE luego de la carga para que la página host pueda mostrar un botón de CC.
Seguridad
| Aspecto | Comportamiento |
|---|---|
| Validación de origen | isAllowedOrigin(origin) verifica contra allowedOrigins. Lista vacía permite todos los orígenes (solo dev). |
| Protección de PII | Los datos de personalización nunca se escriben en logs, localStorage ni mensajes de error. |
| Sanitización de errores | Los payloads de PERCUS/ERROR no deben contener valores de datos en bruto. |
targetOrigin | Debe apuntar al origen host real en producción (actualmente "*"). |
| Sandbox del iframe | Gestionado por la página host, no por el player. |