Cross-origin lifecycle for play, seek, capture — one library, one canonical data shape, OGraf v1 compatible.
flow-bridge e a unica biblioteca em runtime de um Edge Template. Ela roda dentro do iframe do template, conversa com o host (editor da Insyde, recorder, flow-playout) via postMessage, e fala com o engine do template via uma interface uniforme. O template em si fica magrissimo: declara metodos semanticos (load, playAction, captureFrame, etc.) e o resto e responsabilidade do bridge.
flow-bridge is the only runtime library inside an Edge Template. It runs in the iframe, talks to the host (Insyde editor, recorder, flow-playout) over postMessage, and talks to the template engine through a uniform interface. The template itself stays minimal: declare semantic methods (load, playAction, captureFrame, etc.) and the bridge handles the rest.
┌─────────────────────────────────────────────────────────────┐
│ Host (editor / recorder / playout / OGraf renderer) │
│ │
│ values (canonical) ── flow:update ──────→ │ │
│ │ postMessage │
│ ←── flow:loaded / flow:played / ──────────┤ │
│ flow:error / flow:captureDone │ │
└─────────────────────────────────────────────┼───────────────┘
│
┌──────────▼──────────┐
│ flow-bridge.js v2 │
│ │
│ • bootstrap │
│ • config.json fetch│
│ • autoParseQS │
│ • OGraf adapter │
│ • postMessage I/O │
│ • error wrapping │
│ • lifecycle FSM │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ template engine │
│ Web Component OR │
│ window.flowEngine │
└─────────────────────┘
A same-origin policy do browser bloqueia iframe.contentWindow.x quando o iframe esta em outro origin. CORS nao desbloqueia isso. document.domain foi descontinuado. A unica saida limpa e postMessage — permite mensagens estruturadas e ate transferir ImageBitmap sem copia (zero-copy) atraves do limite de origem.
Browser same-origin policy blocks iframe.contentWindow.x across origins. CORS does not unlock that. document.domain is deprecated. The only clean path is postMessage — structured messages and zero-copy ImageBitmap transfers across the origin boundary.
O template inclui o bridge depois do app.js e da lib de captura. O app.js declara dois facades sobre o mesmo engine: um Web Component (pra OGraf strict) e window.flowEngine (pro bridge encontrar predizivelmente).
The template includes the bridge after app.js and the capture lib. app.js declares two facades over the same engine: a Web Component (for strict OGraf) and window.flowEngine (for the bridge to find predictably).
index.html
<script defer src="vendor/gsap.min.js"></script>
<script defer src="vendor/dom-to-image-more.min.js"></script>
<script defer src="app.js"></script>
<script defer src="vendor/flow-bridge.min.js"></script>
app.js (esqueleto / skeleton)
class Engine {
async load({ data, isUpdate = false } = {}) {
// Read canonical (long-name) data.
if (data.headline != null) headlineEl.textContent = data.headline;
const bars = Array.isArray(data.bars) ? data.bars : DEFAULTS;
// ... render DOM, build timeline (paused at 0)
return { statusCode: 200 };
}
async playAction({ skipAnimation } = {}) {
skipAnimation ? tl.progress(1).pause() : tl.play(0);
return { statusCode: 200, currentStep: 1 };
}
async stopAction({ skipAnimation } = {}) {
skipAnimation ? tl.pause(0) : tl.reverse();
return { statusCode: 200 };
}
async dispose() { return { statusCode: 200 }; }
getDuration() { return tl.duration() * 1000; }
seek(timeMs) { tl.pause(); tl.time(timeMs / 1000); }
async captureFrame(w, h) { const c = await domtoimage.toCanvas(document.body, { width: w, height: h }); return await createImageBitmap(c); }
}
const engine = new Engine();
// (a) Web Component — OGraf v1 strict
class EleicoesXxx extends HTMLElement {
static get capabilities() { return ['realtime', 'nonRealtime']; }
async load(args) { return engine.load(args); }
async playAction(args) { return engine.playAction(args); }
async stopAction(args) { return engine.stopAction(args); }
async dispose() { return engine.dispose(); }
getDuration() { return engine.getDuration(); }
seek(t) { return engine.seek(t); }
captureFrame(w, h) { return engine.captureFrame(w, h); }
}
customElements.define('eleicoes-xxx', EleicoesXxx);
// (b) flow-bridge global facade — same engine instance
window.flowEngine = {
definition: { name: 'eleicoes-xxx', capabilities: ['realtime', 'nonRealtime'] },
load: (args) => engine.load(args),
playAction: (args) => engine.playAction(args),
stopAction: (args) => engine.stopAction(args),
dispose: () => engine.dispose(),
getDuration: () => engine.getDuration(),
seek: (t) => engine.seek(t),
captureFrame: (w, h) => engine.captureFrame(w, h),
};
Sem bootstrap inline. Sem parseDataFromQS. Sem flag ?record=1. O bridge cuida de tudo. O template fica focado em renderizar.
No inline bootstrap. No parseDataFromQS. No ?record=1 branch. The bridge owns those concerns. The template focuses on rendering.
window.flowEngine; se nao tiver, escaneia o DOM por um custom element com load + playAction. Faz polling ate 5s pra cobrir variacoes de ordem de carregamento../config.json. Carrega o schema do template (irmao de index.html). Se faltar, cai num parser generico.data[field.name] = coerce(params.get(field.key), field.type)${col.key}${i} pra cada coluna em cada linha → data[field.name] = [{ <col.key>: value }, ...]engine.load({ data }) — template aplica dados, monta DOM, monta timeline pausado em 0.flow:ready { definition }. Host sabe que template ta carregado. Drena qualquer mensagem que chegou antes (queue interno).window.parent === window (aba isolada de fato): auto-play.?autoplay=1 (override explicito, util pra embeds): auto-play mesmo em iframe.flow:play do host.
Uma unica shape e usada ponta-a-ponta: estado do form na flow-front, payload do flow:update, e o data que o engine recebe.
One shape used end-to-end: flow-front form state, flow:update payload, and the data the engine receives.
{
// scalars: long names from config.field.name
headline: 'PESQUISA DE INTENCAO DE VOTOS',
subheadline: 'SAO PAULO',
source: 'CNN ATLAS | 1o TURNO',
showPartyColor: true,
gfxMode: true,
barCount: 8,
// tables: rows array, each row keyed by config.column.key
bars: [
{ n: 'MARCIO FRANCA', p: 'PL', va: 41, vn: 3, vc: 56 },
{ n: '..', p: 'PSB', va: 36, vn: 9, vc: 55 },
],
}
A URL mantem chaves curtas (?hl=...&n1=...&va1=...) por compactness — isso e uma preocupacao de serializacao tratada exclusivamente pelo bridge.
The URL keeps short keys for compactness — that's a serialization concern handled exclusively by the bridge.
| Method | When called | Returns | Notes |
|---|---|---|---|
load({ data, isUpdate? }) | on flow:update (host) ou na bootstrap | { statusCode } | Aplica dados. isUpdate=true → patch in place; bridge depois chama playAction({skipAnimation:true}) pra snap pro estado final. |
playAction({ skipAnimation? }) | on flow:play | { statusCode, currentStep } | Inicia animacao. skipAnimation=true pula direto pro fim sem animar. |
stopAction({ skipAnimation? }) | on flow:stop | { statusCode } | Reverse / out animation. |
dispose() | on flow:dispose | { statusCode } | Cleanup — mata timelines, listeners, etc. |
next() | on flow:next | any | Multi-step templates apenas. Opcional. |
getDuration() | on flow:getDuration | number (ms) | Necessario pra nonRealtime. |
seek(timeMs) | on flow:seek | void | Necessario pra nonRealtime. Pausa antes de mover. |
captureFrame(w, h) | on flow:capture | ImageBitmap | Necessario pra nonRealtime. Roda DENTRO do iframe (resolve @font-face certo). |
{ statusCode: 200 }: e o shape esperado pelo OGraf v1, mas o bridge nao valida nem usa o valor — ele so awaita a Promise. Retornar e recomendado pra compliance OGraf, mas nao obrigatorio.
{ statusCode: 200 }: this is the OGraf v1 expected shape, but the bridge does not validate nor consume the value — it only awaits the Promise. Returning it is recommended for OGraf compliance, but not enforced.
Apos resolveEngine, o bridge sobrescreve tres propriedades do engine pra servir como hooks de notificacao. O engine chama esses metodos quando atinge marcos internos — o bridge converte em mensagens postMessage:
After resolveEngine, the bridge overwrites three properties on the engine to serve as notification hooks. The engine calls these methods when it reaches internal milestones — the bridge converts each call into a postMessage:
| Engine call | Bridge posts | Quando / When |
|---|---|---|
this.onAnimationDone(phase) |
flow:played { phase } |
Animacao chegou ao fim natural. phase: 'in' ou 'out'. Default 'in' se omitido. |
this.onStopped() |
flow:stopped |
Out-animation completou (alternativa a esperar o promise de stopAction resolver). |
this.onError(err) |
flow:error { code: 'INVALID_DATA', message } |
Erro de runtime que o engine quer surfacear pro host. |
Esses callbacks sao opcionais — o engine nao precisa cha-los. Se a sua animacao tem fim natural e o host quer saber, dispare onAnimationDone. Se nao, omita.
These callbacks are optional — the engine doesn't need to call them. If your in-animation has a natural end and the host wants to know, fire onAnimationDone. Otherwise omit.
| Type | Payload | Effect |
|---|---|---|
flow:update | { fields, isUpdate?: bool } | Aplica dados; bridge devolve flow:loaded. Se isUpdate, snap-to-end automatico. |
flow:play | { skipAnimation?: bool } | Mapeado pra playAction. |
flow:stop | { skipAnimation?: bool } | Mapeado pra stopAction. |
flow:next | — | Multi-step. |
flow:seek | { t: ms } | Resposta: flow:seekDone. |
flow:capture | { width, height } | Resposta: flow:captureDone { bitmap } (transferable). |
flow:getDuration | — | Resposta: flow:duration { durationMs }. |
flow:dispose | — | Cleanup. Bridge para de aceitar mensagens. |
flow:hello | — | Re-emite flow:ready + flow:loaded. Util quando um host conecta DEPOIS do bootstrap (ex: recorder herdando o iframe do editor sem reload). |
| Type | Payload | Quando / When |
|---|---|---|
flow:ready | { definition, bridge } | Bootstrap completo + initial load resolveu. Inclui versao do bridge. |
flow:loaded | — | Qualquer load() resolveu (incluindo isUpdate). |
flow:played | { phase: 'in' \| 'out' } | Animacao chegou no fim natural (engine dispara via onAnimationDone). |
flow:stopped | — | stopAction terminou. |
flow:duration | { durationMs } | Resposta de flow:getDuration. |
flow:seekDone | { t } | Resposta de flow:seek. |
flow:captureDone | { bitmap } (ImageBitmap transferable) | Resposta de flow:capture. |
flow:error | { code, message, recoverable? } | Qualquer falha, com codigo padronizado. |
Emitidos pelo bridge / Emitted by the bridge:
LOAD_FAILED — load() rejeitou ou engine nao foi resolvido em <5s.PLAY_FAILED — playAction() rejeitou.STOP_FAILED — stopAction() rejeitou.SEEK_FAILED — seek() rejeitou ou engine nao implementa.CAPTURE_FAILED — captureFrame() rejeitou ou engine nao implementa.INVALID_DATA — autoParseQS falhou, ou fallback de erro generico.Reservados para uso futuro (nao emitidos pelo bridge atual): TIMEOUT, UNSUPPORTED_CAPABILITY. Hosts podem emitir esses codigos em sua propria sinalizacao.
Reserved for future use (not emitted by the current bridge): TIMEOUT, UNSUPPORTED_CAPABILITY. Hosts may emit these codes in their own signalling.
bootstrap load(d) play()
INIT ──────────────► LOADING ──────► READY ──────────► PLAYING
│ │ │
│ error │ stop() │ ends naturally
▼ ▼ ▼
ERROR STOPPING ───────────► PLAYED
│
▼
STOPPED
Eventos emitidos: flow:ready (READY), flow:loaded (apos cada load), flow:played (PLAYING→PLAYED), flow:stopped (STOPPING→STOPPED), flow:error (qualquer →ERROR).
O array definition.capabilities declara o que o template suporta. Hosts (editor, recorder, renderers) usam pra habilitar/desabilitar UI.
realtime — playAction/stopAction funcionam; standard renderer playout.nonRealtime — getDuration/seek/captureFrame tambem funcionam. Recorders podem dirigir frame-a-frame.transparent — corpo do template tem fundo transparente; ideal pra compositing sobre video.
config.json → meta.capabilities e metadata editorial (filtragem na galeria do editor). A fonte de verdade em runtime e flow:ready.definition.capabilities.
Templates flow-bridge-compliant rodam sem modificacao em qualquer renderer OGraf v1. O contrato bate na semantica:
| OGraf v1 | flow-bridge |
|---|---|
Web Component class registrada via customElements.define | Mesmo |
load({ data }) => { statusCode } | Mesmo (extends com optional isUpdate) |
playAction({ skipAnimation }) | Mesmo |
stopAction({ skipAnimation }) | Mesmo |
dispose() | Mesmo |
Capabilities realtime / nonRealtime / transparent | Mesmo |
getDuration() / seek(timeMs) pra nonRealtime | Mesmo |
callbacks onAnimationDone(phase) / onStopped() / onError() | Mesmo (bridge encaminha) |
| schema attribute | definition.schema (proximo passo) ou config.json sibling |
| — | captureFrame(w, h) — extensao nossa pra recording, opcional |
Um renderer OGraf instancia o WC e chama os metodos diretamente — bypass do bridge. flow-bridge usa window.flowEngine (que delega ao mesmo engine instance interno). Os dois caminhos coexistem sem conflito.
captureFrame(w, h) roda dentro do iframe. Essencial — e a unica forma das URLs de @font-face resolverem. Captura externa cai pra fonte do sistema e o video sai com metricas erradas.
async captureFrame(width, height) {
// 1. settle layout (em caso de seek recente)
await new Promise((r) => requestAnimationFrame(() => r()));
// 2. rasterize document.body em canvas
const canvas = await window.domtoimage.toCanvas(document.body, { width, height });
// 3. canvas → ImageBitmap (transferable, zero-copy cross-origin)
return await createImageBitmap(canvas);
}
O bridge transfere o ImageBitmap com postMessage(payload, '*', [bitmap]). Host pode usar direto em new VideoFrame(bitmap) (WebCodecs) ou desenhar num canvas pra toBlob.
(a) Aba isolada / Standalone tab
https://cdn.insyde.one/data-elements/<uuid>/index.html?hl=PESQUISA&shl=SP&n1=A&p1=PL&va1=41&vn1=3&vc1=56&...
Bridge bootstrap → fetch config.json → autoParseQS gera shape canonico → engine.load → (window.parent === window) → engine.playAction. Animacao roda. ✓
(b) Editor preview (flow-front, sem record mode)
Iframe carrega com mesma URL canonica. Bridge bootstrap igual ao (a), mas window.parent !== window e nao tem ?autoplay → nao auto-play. Posta flow:ready. Host (flow-front) recebe e:
flow:update { fields, isUpdate: false } com os values atuais do form.flow:loaded.flow:play.Animacao roda. Quando user edita um campo, host manda flow:update { fields, isUpdate: true } — bridge faz patch + snap-to-end automatico. Sem reload do iframe, sem flash de fonte. ✓
(c) Editor record mode
Mesmo iframe, mesmo bridge. Host nao chama flow:play; em vez disso roda o loop de seek/capture:
for (let i = 0; i < totalFrames; i++) {
await bridge.seek(i / fps * 1000);
const bitmap = await bridge.capture(1920, 1080);
await encoder.encodeFrame(bitmap, i);
}
Encoder e WebCodecs (mp4-muxer / webm-muxer) ou ffmpeg.wasm fallback. Output mp4 ou webm. ✓
(d) flow-playout / OGraf renderer (live broadcast)
Renderer instancia o WC <eleicoes-barras-empilhadas-8> ou usa o bridge global. Manda load({data}) + playAction(). Animacao roda no ar. onAnimationDone('in') dispara → renderer sabe que terminou a in-animation. ✓
De um template legacy (Web Component com __template + ?record=1) pra v2:
flow-bridge.min.js + dom-to-image-more.min.js em vendor/; declarar em manifest.json → vendor.index.html: carregar bridge depois do app.js.update(fields) → load({ data, isUpdate }); ler long names (data.headline em vez de data.hl) e column keys (data.bars[i].n em vez de data.bars[i].name).{ statusCode: 200 } em load/playAction/stopAction/dispose.play() → playAction({ skipAnimation }); stop() → stopAction({ skipAnimation }).parseDataFromQS e o bootstrap inline. O bridge faz.window.flowEngine (ambos delegando ao mesmo engine instance).capabilities no definition e em manifest.json → meta.capabilities.index.html?<qs> — auto-play funciona via bootstrap.await window.flowEngine.captureFrame(1920, 1080) retorna ImageBitmap.flow:update + flow:play — verificar respostas.meta block)edge-template.schema.json)
Source canonico do bridge: flow-playout/public/templates/flow-bridge.js. Vendored em cada template como vendor/flow-bridge.min.js.