@enfinitos/sdk-streaming-player
EnfinitOS reference SDK for the STREAMING substrate — a JS player extension that plugs into HLS.js, Shaka Player, Video.js, native iOS (AVPlayer), and native Android (ExoPlayer / Media3) players.
Operators integrate this SDK to fold their streaming inventory into EnfinitOS-managed rights / consent / proof-of-play / audit. The SDK is substrate-aware — it knows about SCTE-35 cues, ABR ladder changes, quartile bookmarks, DRM detection, and the two-phase resolve+grant flow streaming players actually need. It's not a video player; it's the governance layer that wraps whichever player you already ship.
The SDK builds on @enfinitos/sdk-renderer-core, the substrate- agnostic foundation every EnfinitOS renderer wraps. The streaming client composes the core's primitives (resolve, grant, event reporter, health reporter, transport, retry, idempotency) and adds three streaming-specific concerns:
- Player-adapter shape (
PlayerAdapter) — four built-in adapters (HLS.js, Shaka, Video.js, native bridge) translate native player event vocabularies into a single canonicalPlaybackEventshape. - Two-phase resolve (
resolveAdSlot+commitSlot) — match the actual rhythm streaming players want: resolve 10+ seconds ahead of the SCTE-35 cue point for pre-staging; grant at the cue point. - DRM detection hooks — probe which DRM systems the player can speak (Widevine / FairPlay / PlayReady / ClearKey) so the platform's ad-decision plane picks an encrypted variant the player can actually play. Key fetching is NOT implemented in this tranche — see "DRM scope" below.
Platform-side counterpart. This SDK consumes the platform's existing runtime plane endpoints (/runtime/resolve,/runtime/grant,/runtime/event-ingest,/runtime/health-ingest). No new server-side work is required for the SDK to function.
Architecture
┌──────────────────────────┐
│ EnfinitOS Platform │
│ (runtime, rights, │
│ audit, fleet health) │
└────────────┬─────────────┘
│ HTTPS REST
│ + WebSocket (push, opt-in)
│ (device JWT)
│
┌────────────────────────────────▼─────────────────────────┐
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ EnfinitOSStreamingClient │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ renderer-core │ │ IngestReporter │ │ │
│ │ │ Client │ │ (PlaybackEvent → │ │ │
│ │ │ (resolve, grant, │ │ PlayEvent) │ │ │
│ │ │ events, health, │ └────────────────────┘ │ │
│ │ │ consent) │ │ │
│ │ └────────────────────┘ ┌────────────────────┐ │ │
│ │ │ drmHooks (probe) │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PlayerAdapter (one of four) │ │
│ │ │ │
│ │ HLS.js │ Shaka │ Video.js │ Native │ │
│ │ │ │
│ │ Translates native event vocabulary into a single │ │
│ │ canonical PlaybackEvent + cue stream. │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────┬───────────────────────────────────┘
│
▼
Player surface
(HLS.js / Shaka / Video.js JS instance OR
AVPlayer (iOS) / ExoPlayer (Android) bridge)
Why this SDK and not just renderer-core?
The renderer-core's PlayEvent vocabulary is intentionally narrow (started / ended / error / interaction). Streaming substrates need a richer vocabulary the audit ledger keeps queryable:
| What | Where it matters |
|---|---|
| Quartile bookmarks (Q1/Q2/Q3/Q4) | IAB viewability & ad-effectiveness reporting. |
| Pause / resume / seeking | Mid-roll abandonment metric. |
| Mute / unmute | Audio-attention audience predicates. |
| Fullscreen enter / exit | Viewability score input. |
| Buffering start / end | QoE dashboard input. |
| Bitrate change | QoE dashboard input. |
| SCTE-35 cue in / cue out | Server-side ad-insertion verification. |
The renderer-core preserves these on the wire (they ride under interaction.kind) but the streaming SDK is what assembles them into the right shape from native player events.
Getting started
Install
pnpm add @enfinitos/sdk-streaming-player
(@enfinitos/sdk-renderer-core is a peer dependency.)
Five-minute hello-world (HLS.js)
import Hls from "hls.js";
import {
EnfinitOSStreamingClient,
HlsJsAdapter,
} from "@enfinitos/sdk-streaming-player";
const hls = new Hls();
hls.attachMedia(document.getElementById("video") as HTMLVideoElement);
hls.loadSource("https://cdn.example.com/live.m3u8");
const client = new EnfinitOSStreamingClient({
apiBaseUrl: "https://api.enfinitos.com",
viewerId: getPseudonymousSessionId(), // CMP-driven
contentId: "content_42",
territory: "GB",
deviceToken: process.env.ENFINITOS_JWT!,
});
await client.start();
// Attach the HLS.js player to the client. The adapter translates
// HLS.js's native events into the canonical PlaybackEvent shape
// and forwards every one to the EnfinitOS audit ledger.
const adapter = new HlsJsAdapter({
player: hls as never,
asset: { /* the asset currently bound to the player */ } as never,
});
await client.attach(adapter);
// On a SCTE-35 cue: resolve, stage, commit.
adapter.onCue(async (cue) => {
if (cue.type !== "scte35") return;
const candidate = await client.resolveAdSlot("mid-roll", { cueId: cue.cueId });
if (!candidate) return; // no-fill
const staged = client.stageSlot("mid-roll", candidate);
// Wait for the cue-out boundary before committing.
await waitForCueOut(cue.cueId);
await client.commitSlot(staged); // grant + insert
});
Same flow with Shaka Player
import shaka from "shaka-player";
import {
EnfinitOSStreamingClient,
ShakaAdapter,
} from "@enfinitos/sdk-streaming-player";
const player = new shaka.Player(document.getElementById("video"));
await player.load("https://cdn.example.com/live.mpd");
const adapter = new ShakaAdapter({
player: player as never,
asset: currentAsset,
});
await client.attach(adapter);
Same flow with Video.js plugin
import videojs from "video.js";
import { enfinitosPlugin } from "@enfinitos/sdk-streaming-player";
enfinitosPlugin(videojs);
const player = videojs("#vjs");
const adapter = player.enfinitos({ asset: currentAsset });
await client.attach(adapter);
Native iOS / Android
For native apps, implement the NativePlayerBridge interface on the native side (Swift for iOS, Java/Kotlin for Android), expose it through your bridge mechanism (React Native module, Capacitor plugin, JSC), and wrap it with NativePlayerAdapter:
import { NativePlayerAdapter } from "@enfinitos/sdk-streaming-player";
const bridge = NativeModules.EnfinitosBridge; // your binding
const adapter = new NativePlayerAdapter({ bridge, asset: currentAsset });
await client.attach(adapter);
The bridge protocol is documented in src/playerAdapters/_native.ts.
API surface
Constructor
new EnfinitOSStreamingClient({
apiBaseUrl: string;
viewerId: string; // pseudonymous; NEVER a logged-in user id
contentId: string;
territory?: string; // ISO-3166
deviceToken: string;
deviceId?: string; // defaults to `viewer:${viewerId}`
onAssetReady?, onError?: callbacks
renderer?: { /* renderer-core opts */ };
})
Lifecycle
| Method | Description |
|---|---|
start() / stop() | Open / close platform session. |
attach(player) | Bind a PlayerAdapter to the client. |
detach() | Tear down the player binding. |
Resolve / commit
| Method | Description |
|---|---|
resolveAdSlot(position, extra?) | Resolve-only (pre-stage candidate). |
commitSlot(staged) | Grant + insert the staged candidate. |
stageSlot(position, candidate) | Convenience: wrap candidate in a StagedSlot. |
Events
| Method | Description |
|---|---|
reportPlaybackEvent(event) | Direct event reporting (rare). |
drainEvents() | Force-drain renderer-core's event queue. |
queueDepth() / droppedEventCount() | Renderer-core introspection. |
DRM
| Method | Description |
|---|---|
drmSupport() | Cached DRM probe result from attach. |
detectDrm() | Force a fresh probe via the bound adapter. |
Health
| Method | Description |
|---|---|
reportHealth(state, subsystems?) | Substrate-agnostic health heartbeat. |
startHealthHeartbeat(ms) | Periodic heartbeat (default renderer-core builder). |
PlaybackEvent vocabulary
The canonical PlaybackEvent shape carries 20 kinds:
| Kind | Notes |
|---|---|
started, completed | Slot bookends (mapped to renderer-core's started/ended). |
error | Mapped to renderer-core's reportPlayError. |
quartile_q1 / q2 / q3 / q4 | IAB attention bookmarks. Use nextQuartile(pos, dur, fired). |
pause, resume | Viewer pause/resume mid-playback. |
mute, unmute | Audio-attention proxy. |
fullscreen_enter, fullscreen_exit | Viewability input. |
seeking | Mid-roll abandonment. |
buffering_start, buffering_end | QoE dashboard input. |
bitrate_change | ABR ladder change. |
click | Viewer interaction. |
cue_in, cue_out | SCTE-35 marker crossing. |
Everything except the four lifecycle kinds (started/completed/error /click) rides as an interaction event onto the platform's existing event-ingest pipeline; the streaming-specific kind is preserved in interaction.kind.
DRM scope
The SDK detects DRM capability. It does NOT fetch DRM keys, negotiate license servers, or proxy CDM responses. The detectDrmSupport() method reports which key systems the player + runtime can speak; the platform's ad-decision plane uses that to pick an encrypted asset variant the player can play.
This is a deliberate scope decision:
- Each DRM system has its own license-server protocol; implementing them generically requires a CDM-mock and customer-specific key rotation policy.
- Most operators already have a DRM workflow (a Castlabs, Verimatrix, IRDETO contract). The SDK's role is to fit alongside that workflow, not replace it.
Future tranches will add platform-side license-server proxies and a key-fetching hook on the SDK side. The current SDK exports probeEme, staticSupport, mergeSupport, and three preset probes (iOS / Android / Chromecast) as the public API.
Player adapters
| Adapter | Native event vocabulary it wraps |
|---|---|
HlsJsAdapter | HLS.js 1.x — MANIFEST_PARSED, LEVEL_SWITCHED, FRAG_PARSING_METADATA, BUFFER_STALLING/APPENDED, ERROR + native <video> events. |
ShakaAdapter | Shaka 4.x — loaded, buffering, adaptation, timelineregionenter, error + native <video> events. |
VideoJsAdapter | Video.js 7.x — loadedmetadata, play, pause, seeking, volumechange, waiting, canplay, ended, error, fullscreenchange. Includes plugin factory. |
NativePlayerAdapter | Native iOS / Android via a NativePlayerBridge your bridge code implements. |
All four implement the same PlayerAdapter interface so the streaming client never branches on player kind.
Tests
pnpm test
The vitest suite covers four areas:
| Area | Tests |
|---|---|
drmHooks.ts | EME probe behaviour, robustness ladder fall-through, mergeSupport conflict resolution, static probe presets. |
ingestReporter.ts | PlaybackEvent → PlayEvent mapping, dwell derivation, validation, batch reporting, quartile helper. |
playerAdapters/* | Each adapter's translation of its native vocabulary onto the canonical PlaybackEvent shape (fake player surfaces). |
streamingClient.ts | construction validation, attach/detach, resolveAdSlot context shape, commitSlot grant + insert, DRM probe caching. |
What's SDK-side vs platform-side
| Concern | This SDK | Platform |
|---|---|---|
| Player-adapter contract | ✅ | — |
| PlaybackEvent vocabulary | ✅ | ✅ accepts onto interaction-event ingest |
| DRM detection | ✅ | — |
| DRM key fetching | ⏳ future tranche | ⏳ future tranche |
| Two-phase resolve | ✅ wraps existing resolve+grant | ✅ existing endpoints |
| Audit ledger commits | (rides interaction events) | ✅ existing |
| 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/ctv-app/README.md— sibling SDK for CTV (Roku / Tizen / WebOS / Apple TV).docs/launch/substrate-readiness-matrix.md— STREAMING substrate readiness.