EnfinitOSEnfinitOS
DevelopersVisual render
Production-ready scaffold

DOOH Renderer SDK

Governance-aware media-player runtime — playback, panel health, dimming, and proof-of-play reporting.

@enfinitos/sdk-dooh-rendererSubstrate DOOHTypeScript
Install

Get the SDK

npm install @enfinitos/sdk-dooh-renderer

About this status badge

Typed, tested, documented, and wired to the EnfinitOS platform endpoints that exist today. Vendor-side SDK integrations (Broadsign / VIOOH / DJI / Tizen / Alexa / Twilio / Stripe / etc.) land per-customer at pilot integration time — those bring the renderer/transport/exchange-specific code; the EnfinitOS half is ready.

README

The developer-facing documentation in full

The same README the SDK package ships with — rendered here at build time so what you read matches exactly what you install.

@enfinitos/sdk-dooh-renderer

EnfinitOS reference SDK for the DOOH substrate — a governance- aware media-player runtime that runs on a DOOH screen hardware (billboard, transit shelter, mall panel, taxi-top display, elevator screen) and reports playback, surface health, and panel-dimming events to the EnfinitOS rights/policy/audit plane.

This is the SDK an operator integrates in place of a Broadsign player (or alongside one in a mixed estate). Operators happy with Broadsign keep their existing player and EnfinitOS connects through the Broadsign adapter in packages/integrations/broadsign-adapter. Operators wanting tighter EnfinitOS integration — pre-render consent gating, governance proofs, rights-aware fallback content, regulatory dimming audit trails — deploy this SDK.

The SDK builds on @enfinitos/sdk-renderer-core, the substrate-agnostic foundation that every EnfinitOS renderer wraps. The DOOH renderer composes the core's primitives (resolve, grant, event reporter, health reporter, consent client, transport) and adds three DOOH-specific concerns:

  1. Asset preloading — pre-fetch the panel's next-N-minutes of scheduled content during periods of cheap bandwidth so the panel keeps rendering through cellular blips.
  2. Surface-health reporting — a DOOH-flavoured health envelope covering decoder frame stats, ambient-light readings, panel temperature, content-cache occupancy, network bearer, and currently-applied dimming.
  3. Panel-dimming audit — record the moments when the panel dimmed (and why) so regulators in jurisdictions that mandate ambient-light-driven dimming (UK, FR, NL, NSW, several US states) can audit conformance.
Platform-side counterpart. This SDK consumes the platform's existing runtime endpoints (/runtime/resolve, /runtime/grant, /runtime/event-ingest, /runtime/health-ingest) — no new server-side work is required for the SDK to function. The DOOH-flavoured health subsystem map rides under the existing subsystems field on DeviceHealth.

Architecture

                       ┌──────────────────────────┐
                       │   EnfinitOS Platform     │
                       │  (runtime, rights,       │
                       │   audit, fleet health)   │
                       └────────────┬─────────────┘
                                    │  HTTPS REST
                                    │  (device JWT)
                                    │
   ┌────────────────────────────────▼─────────────────────────┐
   │                                                          │
   │   ┌──────────────────────────────────────────────────┐   │
   │   │             EnfinitOSDoohRenderer                │   │
   │   │                                                  │   │
   │   │   ┌─────────────────┐   ┌─────────────────────┐  │   │
   │   │   │ renderer-core   │   │  SurfaceHealth      │  │   │
   │   │   │ Client          │   │  Collector +        │  │   │
   │   │   │  (resolve,      │   │  Reporter           │  │   │
   │   │   │   grant,        │   │   (decoder,         │  │   │
   │   │   │   events,       │   │    ambient,         │  │   │
   │   │   │   health,       │   │    temp,            │  │   │
   │   │   │   consent)      │   │    cache,           │  │   │
   │   │   │                 │   │    network,         │  │   │
   │   │   │                 │   │    dimming)         │  │   │
   │   │   └─────────────────┘   └─────────────────────┘  │   │
   │   │                                                  │   │
   │   │   ┌──────────────────────────────────────────┐   │   │
   │   │   │  AssetPreloader  (concurrency-bounded    │   │   │
   │   │   │                   pre-fetch + retry +    │   │   │
   │   │   │                   pluggable cache sink)  │   │   │
   │   │   └──────────────────────────────────────────┘   │   │
   │   └──────────────────────────────────────────────────┘   │
   │                                                          │
   └──────────────────┬───────────────────────────────────────┘
                      │
                      ▼
           Panel firmware
           (video decoder, ambient-light sensor,
            temperature probe, dimming controller,
            on-disk cache, display element)

Why this SDK and not just renderer-core?

@enfinitos/sdk-renderer-core ships everything substrate-agnostic. It does NOT prescribe:

  • Pre-fetch policy. DOOH panels run on bandwidth-constrained private 4G; pre-fetch is a real cost driver and a real reliability driver. The core couldn't sensibly pick a default.
  • Substrate-specific health vocabulary. A mobile SDK's health has different subsystems (ATT consent, view-port visibility) from a DOOH player's (decoder frame stats, ambient light). The core leaves the subsystems map free-form; the DOOH SDK fills it with the right typed fields.
  • Panel-dimming audit. This is a regulatory concern that simply doesn't apply to most substrates. Encoding it as a first-class method here makes the audit trail explicit and easy to grep for.

Getting started

Install

pnpm add @enfinitos/sdk-dooh-renderer

(@enfinitos/sdk-renderer-core is a peer dependency.)

Five-minute hello-world

import { EnfinitOSDoohRenderer } from "@enfinitos/sdk-dooh-renderer";

const renderer = new EnfinitOSDoohRenderer({
  apiBaseUrl: "https://api.enfinitos.com",
  surfaceId: "panel_londonbridge_west",
  deviceId: "panel_londonbridge_west",       // same as surface here
  deviceToken: process.env.ENFINITOS_JWT!,
  onAssetReady: (asset) => video.src = asset.assetUrl,
  onError: (err) => console.error("dooh", err),
});

await renderer.start();

// 1. Resolve the next slot.
const asset = await renderer.resolveNext({
  doohSlot: {
    placementType: "transit_shelter",
    orientation: "portrait",
    aspectRatio: 9 / 16,
    estimatedDwellS: 14,
  },
});

if (asset) {
  await renderer.reportPlayStarted(asset, new Date());
  // ... play it ...
  await renderer.reportPlayEnded(asset, new Date(), 6000);
}

// 2. Pre-fetch the next hour of scheduled content overnight.
await renderer.preloadAssets([
  "ad_42_v3",
  "ad_99_v1",
  "ad_house_002",
]);

// 3. Ambient light dropped → dim the panel and record it.
await renderer.reportPanelDimming({
  level: 0.4,
  appliedAt: new Date().toISOString(),
  reason: "ambient",
  ambientReading: { reading: 8200, unit: "lux", sensorState: "ok" },
});

// 4. Heartbeat health every 30s.
renderer.surfaceHealth.setTemperature({ reading: 42, unit: "C" });
renderer.surfaceHealth.setDecoderCounters({ framesDecoded: 1800, framesDropped: 1 });
const stopHeartbeat = renderer.startHealthHeartbeat(30_000);

API surface

Constructor

new EnfinitOSDoohRenderer({
  apiBaseUrl: string;                    // platform URL
  surfaceId: string;                     // DOOH placement
  deviceId?: string;                     // defaults to surfaceId
  deviceToken: string;                   // platform-issued JWT
  onAssetReady?: (asset) => void;
  onError?: (err) => void;
  cacheSink?: AssetCacheSink;            // optional: disk-backed cache
  fetcher?: AssetFetcher;                // optional: custom byte fetcher
  surfaceHealth?: SurfaceHealthCollectorOptions;
  preloader?: { concurrency?; maxAttempts?; fetchTimeoutMs?; };
  renderer?: { /* renderer-core opts */ };
})

Inherited (from renderer-core)

MethodDescription
start() / stop()Open / close the platform session.
resolveNext(context?)Resolve the next asset; accepts DOOH-flavoured context.
grant(assetId, opts?)Standalone grant (rare; mostly for pre-fetch).
reportPlayStarted / Ended / ErrorProof-of-play events.
reportClick / ConversionViewer-interaction events.
reportHealth(state, subsystems?)Substrate-agnostic health heartbeat.
startHealthHeartbeat(ms)Periodic heartbeat (defaults to a DOOH-rich builder).
drainEvents()Force a queue drain before sleep.

DOOH-specific

MethodDescription
`preloadAssets(ids \resolved[])`Pre-fetch bytes to the panel cache.
reportSurfaceHealth(metrics)Submit a DOOH-flavoured health envelope.
reportPanelDimming(level)Record a dimming event (regulator audit).

Direct subsystem access

renderer.surfaceHealth   // SurfaceHealthCollector — feed sensor readings here
renderer.preloaderRef    // AssetPreloader — inspect / drive directly
renderer.rendererCore    // EnfinitOSRendererClient — escape hatch

Surface-health vocabulary

The DOOH renderer's surface-health envelope is the substrate-specific specialisation of the renderer-core's DeviceHealth.subsystems map. Every field is optional — panels report what their firmware exposes.

SubsystemWhat it carries
decodercodec, frames decoded / dropped / error count this window, last error
ambientLightreading (lux / nits), sampledAt, sensorState (ok / drift / offline)
temperaturereading + unit (°C/°F/K), sampledAt, warnAt
contentCachebytes / capacity / count, preload attempts + failures this window
networkbearer (eth/wifi/cellular), RTT, RSRP dBm, up/downlink kbps
dimmingLevel0..1 currently-applied dimming

The derived overall state from the SDK's SurfaceHealthCollector:

ConditionDerived state
Decoder drop > 10%UNHEALTHY
Temperature > 85°C (or unit equivalent)UNHEALTHY
Decoder drop > 2% or decoder errors > 0DEGRADED
Temperature > 70°CDEGRADED
Ambient-light sensor offline or driftDEGRADED
Cache fill < 10%DEGRADED
Cellular RSRP ≤ -110 dBmDEGRADED
otherwiseOK

Application code can override this with reporter.reportNow({ overrideState: "OFFLINE" }) when it knows something the collector can't infer (e.g. the management plane just told it to go to a maintenance window).

Asset preloader pattern

The preloader is a small state-machine over a substrate-pluggable cache backend:

   enqueue() ─► [pending] ──► [fetching] ──► [ready]
                                ├──────────► [failed]   (maxAttempts hit)
                                └──────────► [pending]  (retry w/ backoff)

   pruneExpired() ──► [ready] ──► [expired]

Default cache sink: InMemoryCacheSink (256 MiB). Production deployments bring their own (a disk-backed LRU, a webOS app cache, a Tizen JS cache). Implement AssetCacheSink:

class DiskBackedSink implements AssetCacheSink {
  async store({ assetId, bytes, ... }) { /* write to disk */ }
  async has(assetId) { /* check disk */ }
  async evict(assetId) { /* unlink */ }
  async stats() { return { cacheBytes, cacheCapacityBytes, cachedAssetCount }; }
}

The preloader supports:

  • Concurrency cap (default 4). Bandwidth-friendly.
  • Per-asset exponential backoff with attempt cap (default 3).
  • Side-band cache hit checks: if the sink already has the asset, no fetch happens.
  • Side-band cache stats exposed to the SRE dashboard via the surface-health collector's contentCache subsystem.

Panel-dimming audit

reportPanelDimming(level) records a panel-dimming event for regulatory audit. The reported level is also remembered by the surface-health collector so the next heartbeat reflects the current applied dimming. The level is sent as an interaction event on a synthetic "_panel" asset so the platform-side audit log keeps panel-level events distinct from viewer-driven events on actual content.

The full report shape:

{
  level: 0.4,                                  // 0..1
  appliedAt: "2026-05-13T19:24:00.000Z",
  reason: "ambient" | "schedule" | "operator" | "regulatory",
  correlationId?: "rule_uk_billboards_v3",     // optional
  ambientReading?: { reading: 8200, unit: "lux", sensorState: "ok" },
}

Operator integration patterns

Mode 1: full EnfinitOS player

Panel firmware vendor builds a JS host (Tizen / WebOS / Android TV / custom Linux + Electron / Chromium kiosk) and includes this SDK verbatim. The SDK drives resolve, render, report, health-report loop. Operator's existing CMS schedules feed the SDK via preloadAssets() or via the platform's day-part scheduling.

Mode 2: hybrid with Broadsign player

Operator keeps Broadsign's player for legacy estate and runs this SDK on the new estate. Both players report against EnfinitOS for audit and rights enforcement; reconciliation happens server-side via the Broadsign adapter in packages/integrations/broadsign-adapter.

Mode 3: adapter mode (server-side)

Operator can't change their panel firmware but wants EnfinitOS governance. The Broadsign / VIOOH / Hivestack / PlaceExchange adapters in packages/integrations use this SDK's contract types to mirror the same wire shape against EnfinitOS — the panel firmware is unchanged.

Sample integration — minimal viable panel

import {
  EnfinitOSDoohRenderer,
  InMemoryCacheSink,
} from "@enfinitos/sdk-dooh-renderer";

async function main() {
  const renderer = new EnfinitOSDoohRenderer({
    apiBaseUrl: "https://api.enfinitos.com",
    surfaceId: "panel_001",
    deviceToken: process.env.ENFINITOS_JWT!,
    onAssetReady: (a) => video.src = a.assetUrl,
    onError: console.error,
  });

  await renderer.start();

  // Slot loop.
  while (true) {
    const asset = await renderer.resolveNext({
      doohSlot: { placementType: "outdoor_billboard", orientation: "landscape" },
    });
    if (asset) {
      const start = new Date();
      await renderer.reportPlayStarted(asset, start);
      await playUntilEnd(video, asset);
      await renderer.reportPlayEnded(asset, new Date(), Date.now() - +start);
    }
    await sleep(asset?.durationMs ?? 6_000);
  }
}

Tests

pnpm test

The vitest suite covers three areas:

ModuleTests
surfaceHealth.tsderivation policy (OK/DEGRADED/UNHEALTHY ladder), counter resets, ambient-light flagging, temperature unit normalisation, reporter integration with the renderer-core.
assetPreloader.tsstate machine transitions, retry policy, in-memory sink eviction, custom sink contract, expiry pruning.
doohRenderer.tsargument validation, lifecycle delegation, panel-dimming validation + audit-event emission, preload bytes-to-cache integration, resolve-context folding.

What's SDK-side vs platform-side

ConcernThis SDKPlatform
DOOH-flavoured health envelope✅ shape + collection✅ accepts existing DeviceHealth.subsystems
Asset preloader— (no platform endpoint, panel-local)
Panel-dimming audit event✅ rides existing interaction-event ingest✅ existing endpoint
Pluggable cache sink✅ contract(panel-firmware-specific impl)
Substrate-agnostic primitives(inherits from renderer-core)✅ existing runtime plane

See also

  • packages/sdks/renderer-core/README.md — substrate-agnostic foundation.
  • packages/integrations/broadsign-adapter/ — mode-3 adapter for Broadsign-controlled fleets.
  • docs/launch/substrate-readiness-matrix.md — DOOH substrate readiness.
  • packages/sdks/streaming-player/ — sibling SDK for streaming substrates (HLS / DASH / WebRTC).
  • packages/sdks/ctv-app/ — sibling SDK for CTV (Roku / Tizen / WebOS / Apple TV).
API reference

Hit the HTTP surface directly

The DOOH Renderer SDK is a thin client over the same governed HTTP API every other SDK calls. The full OpenAPI 3.1 reference lives on the docs site, published alongside the April 2027 platform launch.

Sandbox

Run this SDK against a real tenant

The browser demo at enfinitos.com/sandbox runs today against a shared synthetic tenant. The dedicated developer sandbox — your own persistent tenant, API keys, full HTTP-contract coverage — opens ahead of the April 2027 platform launch.