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 grounded in the 2026 platform reality. Awaiting first customer-integration validation.

README

The developer-facing documentation in full

Rendered from packages/sdks/dooh-renderer/README.md at build time — the same source the package ships with.

@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.

Sandbox

Run this SDK against a real tenant

The hosted sandbox is the fastest way to verify DOOH Renderer SDK against a real EnfinitOS tenant before committing to a pilot. Launching Q4 2026.