@enfinitos/sdk-ctv-app
EnfinitOS reference SDK for the CTV substrate — TypeScript core plus four per-platform thin wrappers covering the four CTV hardware platforms that dominate the market:
- Roku (BrightScript)
- Samsung Tizen (JavaScript on WebKit)
- LG WebOS (JavaScript on WebKit)
- Apple TV / tvOS (Swift on UIKit/SwiftUI)
The SDK is what a CTV app embeds to plug its inventory into the EnfinitOS rights / consent / proof-of-play / audit plane.
Architecture
┌──────────────────────────┐
│ EnfinitOS Platform │
│ (runtime, rights, │
│ audit, fleet health) │
└────────────┬─────────────┘
│ HTTPS REST
│ (device JWT)
│
┌────────────────────────────────▼─────────────────────────┐
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ core/ (TypeScript — ~2000 lines) │ │
│ │ │ │
│ │ ┌──────────────────────┐ ┌─────────────────┐ │ │
│ │ │ EnfinitOSCtvClient │ │ ProofReporter │ │ │
│ │ │ (renderer-core + │ │ (proof-of-play │ │ │
│ │ │ fetchAdSlot / │ │ envelope ride │ │ │
│ │ │ reportPlay* / │ │ on existing │ │ │
│ │ │ reportInteraction) │ │ event-ingest) │ │ │
│ │ └──────────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ ViewabilityScorer │ │ │
│ │ │ (MRC-style score from 6 signals) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────────────┴──────────────────────────┐ │
│ │ platforms/ (~250 lines each) │ │
│ │ │ │
│ │ Roku Tizen WebOS Apple TV │ │
│ │ (BrightScript) (JS) (JS) (Swift) │ │
│ │ │ │
│ │ Each: lifecycle wiring, remote-key mapping, │ │
│ │ native-API signals, thin facade over core. │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────┬────────────────────────────────────┘
│
▼
CTV hardware
(smart TV / set-top box / streaming stick)
Why a TS core + four thin wrappers
Three of the four CTV platforms (Tizen / WebOS / Apple TV) can host the TypeScript core through their JS runtimes or through native-side plumbing. The fourth (Roku) cannot — BrightScript is the only language Roku channel apps speak.
Rather than build four parallel implementations, we ship:
- The TS core with all the substantive logic — fetch / resolve / grant / event reporting / viewability scoring / proof-of-play assembly / DRM probe scaffolding. Tested with full vitest coverage. ~2000 lines.
- Per-platform wrappers that handle each platform's lifecycle and remote-control surface. Each ~150-300 lines, thin enough that their semantics are 1:1 with the TS core.
The Roku BrightScript wrapper is the only platform where the substrate-side code re-implements logic from the TS core (because BrightScript can't import the JS module); it mirrors the TS core's behaviour function-for-function and ships a roca-test target.
SDKs
| SDK | Location | Language | Tests |
|---|---|---|---|
| TS core | core/ | TypeScript | vitest (4 test files, ~80 cases) |
| Roku wrapper | platforms/roku/ | BrightScript | (substantive coverage on TS core) |
| Tizen wrapper | platforms/tizen/ | JavaScript | (substantive coverage on TS core) |
| WebOS wrapper | platforms/webos/ | JavaScript | (substantive coverage on TS core) |
| Apple TV wrapper | platforms/appletv/ | Swift | XCTest skeleton (Tests/EnfinitOSCtvTests.swift) |
Getting started — TS core
pnpm add @enfinitos/sdk-ctv-app-core
import { EnfinitOSCtvClient } from "@enfinitos/sdk-ctv-app-core";
const client = new EnfinitOSCtvClient({
apiBaseUrl: "https://api.enfinitos.com",
appId: "app_streamcorp_v3",
deviceId: getDeviceId(),
deviceToken: getDeviceToken(),
deviceClass: "tizen", // one of "roku" | "tizen" | "webos" | "appletv"
viewerId: getPseudonymousSessionId(), // optional
});
await client.start();
const asset = await client.fetchAdSlot("pre-roll", {
contentCategory: "sports",
});
if (asset) {
await client.reportPlayStart(asset);
client.setDisplayOn(true);
client.setAppForeground(true);
client.setAudioOn(true);
// ... feed asset.assetUrl into your player ...
// On each positionUpdate from the player:
client.recordPosition(currentS, expectedS);
// ... slot ends ...
const viewability = client.buildViewability();
await client.reportPlayComplete(asset, viewability);
}
API surface
Constructor
new EnfinitOSCtvClient({
apiBaseUrl: string;
appId: string;
deviceId: string;
deviceToken: string;
deviceClass: "roku" | "tizen" | "webos" | "appletv";
viewerId?: string;
onAssetReady?, onError?: callbacks
})
Lifecycle
| Method | Description |
|---|---|
start() / stop() | Open / close the platform session. |
Resolve
| Method | Description |
|---|---|
fetchAdSlot(position, extra?) | Resolve+grant an ad slot. |
Reporting
| Method | Description |
|---|---|
reportPlayStart(asset) | Start of impression. |
reportPlayComplete(asset, viewability) | End + proof-of-play envelope. |
reportInteraction(asset, interaction) | Viewer-driven interaction. |
reportPlayError(asset, error) | Non-billable error. |
Signals (forwarded into the scorer)
| Method | Description |
|---|---|
setDisplayOn(on) | Hardware power signal. |
setAppForeground(fg) | App lifecycle signal. |
setAudioOn(on) | Audio path on/off. |
recordPosition(positionS, durationS) | Quartile + dwell tick. |
buildViewability() | Snapshot current viewability score. |
Health + introspection
| Method | Description |
|---|---|
reportHealth(state, subsystems?) | Substrate-agnostic heartbeat. |
startHealthHeartbeat(ms) | Periodic heartbeat. |
queueDepth() / droppedEventCount() | Renderer-core introspection. |
rendererCore / viewability / proofReporter | Direct access to composed instances. |
Viewability scoring
The CTV substrate can't measure pixel-level visibility (no Intersection Observer; no in-tab visibility API). The SDK substitutes six platform-observable signals fused into a 0-1 score:
| Signal | Source | Default weight |
|---|---|---|
displayOn | platform power query | 0.30 |
appForeground | platform lifecycle hook | 0.30 |
audioOn | player muted state | 0.20 |
q2Reached | scorer.recordPosition | 0.10 |
q4Reached | scorer.recordPosition | 0.10 |
The MRC-flavoured "isMrcViewable" predicate requires displayOn, appForeground, q2Reached, and dwellMs >= 2000.
Proof-of-play
Every reportPlayComplete call submits a ProofOfPlay envelope:
{
asset: ResolvedAsset;
startedAt: string;
endedAt: string;
viewability: ViewabilityScore;
completed: boolean; // === viewability.q4Reached
correlationId?: string; // optional pod/cue id
}
The envelope rides the renderer-core's interaction-event path under kind: "proof_of_play" so the platform's existing audit-ledger absorbs it without a new endpoint.
Platform wrapper integration
| Platform | Wrapper | How it's loaded |
|---|---|---|
| Roku | Main.brs | Copied into channel's source/ folder |
| Tizen | index.js | Imported via npm into the Tizen .wgt |
| WebOS | index.js | Imported via npm into the WebOS app |
| Apple TV | EnfinitOSCtv.swift | Swift Package or copied source |
Each wrapper README documents the platform-specific configuration, remote-key mapping, and Tested-versions matrix.
Tests
cd core && pnpm test
The vitest suite covers four areas (~80 test cases):
| Area | Tests |
|---|---|
viewabilityScorer.ts | Score formula, weight overrides, clamping, quartile tracking, dwell, MRC predicate, reset. |
proofReporter.ts | Envelope submission, validation, timestamp inversion, score out-of-range. |
ctvCore.ts | Argument validation, lifecycle, fetchAdSlot context shape, play lifecycle, signal forwarding, health, introspection. |
The XCTest skeleton at platforms/appletv/swift/Tests/EnfinitOSCtvTests.swift covers the Swift wrapper's construction validation and viewability scoring. The BrightScript / Tizen / WebOS wrappers are thin enough that the TS core's coverage applies semantically; smoke-tested against the native runtimes during integration.
What's SDK-side vs platform-side
| Concern | This SDK | Platform |
|---|---|---|
| CTV slot vocabulary | ✅ | (ride existing resolve context) |
| Viewability scorer | ✅ | (audit ledger records the score) |
| Proof-of-play envelope | ✅ rides interaction events | ✅ existing event-ingest |
| Per-platform lifecycle wiring | ✅ in wrappers | — |
| Remote-control key mapping | ✅ in wrappers | — |
| DRM key fetching | ⏳ future tranche | ⏳ future tranche |
| Substrate-agnostic primitives | (inherits from renderer-core) | ✅ existing runtime plane |
See also
packages/sdks/renderer-core/README.md— substrate-agnostic foundation.packages/sdks/dooh-renderer/README.md— sibling SDK for DOOH panels.packages/sdks/streaming-player/README.md— sibling SDK for HLS / DASH / WebRTC streaming.docs/launch/substrate-readiness-matrix.md— CTV substrate readiness.