Saltar al contenido principal

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

  1. Arrancar dentro de un <iframe> servido desde el origen del Percus Player.
  2. Escuchar comandos postMessage entrantes desde la página host.
  3. Cargar el template de animación (Lottie JSON), el manifest de bindings y los datos de personalización.
  4. Aplicar los bindings de datos mediante el BindingEngine.
  5. Controlar el Renderer (basado en lottie-web) en respuesta a los comandos de reproducción/pausa/seek.
  6. 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

PropiedadTipoRequeridoDescripción
stageElHTMLElementNoElemento DOM usado como contenedor de renderizado. Por defecto document.body.
allowedOriginsstring[]NoLista blanca de orígenes autorizados a enviar comandos. Array vacío = permitir todos (solo desarrollo).
onDebug(snapshot: PlayerRuntimeDebugSnapshot) => voidNoSe llama en cada cambio de estado con un snapshot completo para depuración.
templateLoaderTemplateLoaderNoReemplaza el FetchTemplateLoader por defecto.
manifestLoaderManifestLoaderNoReemplaza el FetchManifestLoader por defecto.
dataProviderDataProviderNoReemplaza el DefaultDataProvider por defecto.
bindingEngineBindingEngineNoReemplaza el NoopBindingEngine por defecto.
rendererRendererNoReemplaza el renderer de lottie-web por defecto.

Máquina de estados

El runtime transita por cuatro estados:

idle → loading → ready
↘ error
EstadoSignificado
idleEsperando un comando INIT.
loadingDescargando template, manifest y datos concurrentemente.
readyAnimación cargada; responde a play / pause / seek.
errorOcurrió 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:

  1. Valida que exactamente uno de data / dataUrl esté presente (o ninguno para templates estáticos).
  2. Transiciona el estado a loading.
  3. Descarga template, manifest y datos en paralelo.
  4. Ejecuta BindingEngine.applyBindings().
  5. Llama a Renderer.load() con el JSON del template resuelto.
  6. Emite PERCUS/READY en caso de éxito, PERCUS/ERROR en 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

AspectoComportamiento
Validación de origenisAllowedOrigin(origin) verifica contra allowedOrigins. Lista vacía permite todos los orígenes (solo dev).
Protección de PIILos datos de personalización nunca se escriben en logs, localStorage ni mensajes de error.
Sanitización de erroresLos payloads de PERCUS/ERROR no deben contener valores de datos en bruto.
targetOriginDebe apuntar al origen host real en producción (actualmente "*").
Sandbox del iframeGestionado por la página host, no por el player.