SPDN SDK Documentation
The SPDN browser SDK delivers HLS segments over a peer-to-peer WebRTC mesh without changing your origin, your player, or your auth. This document covers integration with the players we support today, the full JS API, the REST surface, and the operational quirks worth knowing before you go live.
For a 60-second taste, see the hls.js demo or
the Clappr demo. Both pages run the same
SDK we publish at https://spdn.tv/sdk/spdn-p2p.js.
Quickstart
Three steps, one HTML file:
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js"></script> <script src="https://spdn.tv/sdk/spdn-p2p.js"></script> <video id="player" controls></video> <script> const spdn = new SPDN({ token: "spdn_app_YOUR_TOKEN_HERE", streamId: "live-channel-1" }); spdn.ready.then(() => { const hls = new Hls(); hls.attachMedia(document.getElementById("player")); spdn.attachToHls(hls); hls.loadSource("https://your-cdn.example.com/live.m3u8"); }); </script>
.ts chunks come
in from other viewers instead of your CDN.
Authentication
Every SDK session is gated by an app token — an opaque
spdn_app_… string minted in the dashboard. Tokens are
domain-locked: a token created for example.com won't work
if pasted into a page on other-site.com.
Minting a token
- Go to Dashboard → Developer.
- Open the SDK Tokens tab.
- Enter a name (e.g.
customer-prod) and at least one allowed domain. - Click Create SDK token. The raw token is revealed once — copy it.
Domain whitelist
Each token carries a list of allowed origins. The SDK sends the page's
Origin header on the /sdk/sessions POST; the
server checks it against the whitelist before issuing a session.
| Entry | Matches |
|---|---|
example.com | exact host match only |
*.example.com | any subdomain (a.example.com, b.example.com); NOT example.com itself |
| (empty list) | any origin — NOT recommended; logged as a warning |
Need to rotate domains under takedown pressure? Click Edit domains on the token row — the same token stays valid; only the whitelist changes.
Player integrations
Anything built on hls.js works today. The integration is always
create the player, then call spdn.attachToHls(hls) — only
the part about where to find the hls.js instance differs.
hls.js (raw)
You own the Hls instance, so just pass it in:
const spdn = new SPDN({ token, streamId }); spdn.ready.then(() => { const hls = new Hls(); hls.attachMedia(video); spdn.attachToHls(hls); hls.loadSource(url); });
Clappr
Clappr lazily instantiates an internal Hls inside its playback
object. We poll for it (every 100 ms, capped at 15 s):
const spdn = new SPDN({ token, streamId }); spdn.ready.then(() => { const player = new Clappr.Player({ source: url, parentId: "#player", autoPlay: true, mute: true // browser autoplay policy }); function attachWhenReady() { const pb = player.core && player.core.activeContainer && player.core.activeContainer.playback; const hls = pb && (pb._hls || pb.hls); if (hls) { spdn.attachToHls(hls); return true; } return false; } if (!attachWhenReady()) { const t = setInterval(() => { if (attachWhenReady()) clearInterval(t); }, 100); setTimeout(() => clearInterval(t), 15000); } });
Video.js
Vanilla Video.js uses VHS (its built-in HLS engine) which SPDN cannot hook into. Register a custom Video.js source handler at priority 0 — that swaps in hls.js above VHS, and SPDN attaches to the exposed instance:
// 1. Register handler BEFORE any new videojs(...) call videojs.getTech("Html5").registerSourceHandler({ name: "spdn-hlsjs", canHandleSource: src => /\.m3u8(\?|$)/i.test(src.src) ? "probably" : "", handleSource: function (source, tech) { const hls = new Hls(); hls.attachMedia(tech.el()); hls.loadSource(source.src); tech.hlsjs = hls; return { dispose: () => hls.destroy() }; } }, 0); // index 0 = above VHS // 2. Attach SPDN once tech.hlsjs is populated const player = videojs("player"); player.src({ src: url, type: "application/x-mpegURL" }); const wait = setInterval(() => { const hls = player.tech({IWillNotUseThisInPlugins:true}).hlsjs; if (hls) { spdn.attachToHls(hls); clearInterval(wait); } }, 100);
Live wiring at /sdk/videojs-demo. Pin Video.js to the 7.x line — Video.js 8 deprecated the source-handler API.
JW Player
JW Player's default HLS engine is proprietary. Use the SPDN JW bridge to register
hls.js as a JW provider, then auto-wire the P2P engine via
hlsjsConfig.p2pConfig.
JW Player has no free dev tier — a license key is required.
attachToHls(), you pass spdn.getEngineConfig() via
hlsjsConfig.p2pConfig and the SPDN hls.js engine bundle auto-wires
the P2P engine. Use the spdn-hls-engine.min.js bundle, NOT vanilla hls.js.
// + spdn-jw-bridge.min.js script tag BEFORE jwplayer.js // + spdn-hls-engine.min.js bundle (NOT vanilla hls.js) jwplayer.key = "YOUR_JW_LICENSE_KEY"; jwplayer_hls_provider.attach(); jwplayer("player").setup({ file: url, hlsjsdefault: true, hlsjsConfig: { liveSyncDurationCount: 3, p2pConfig: spdn.getEngineConfig() // P2P auto-wire } });
Live wiring at /sdk/jwplayer-demo — a demo JW key is pre-filled for testing, replace with your own for production.
JS API reference
new SPDN(options)
| Option | Type | Required | Notes |
|---|---|---|---|
token | string | yes | App token from the dashboard (spdn_app_…). |
streamId | string | yes | Stable identifier per stream. All viewers of the same stream MUST share this. |
apiBase | string | no | Override the SPDN backend host. Defaults to https://spdn.tv. |
debug | boolean | no | Verbose console output. false by default. |
onReady | function | no | Called after /sessions succeeds and the engine has loaded. |
onError | function | no | Called if init fails. spdn.ready also rejects. |
spdn.attachToHls(hlsInstance)
Primary entry point. Pass any Hls instance — raw or extracted from a wrapper
player — and the SDK hijacks its segment loader from the inside. Throws if called before
spdn.ready resolves.
spdn.createHls(hlsConfig?)
Sugar for the raw-hls.js path. Equivalent to:
const hls = new Hls(cfg); spdn.attachToHls(hls); return hls;
spdn.destroy()
Tear down the engine, the stats timer, and flush a final heartbeat. Call on page unload or when the player is removed from the DOM. Idempotent.
spdn.ready
A Promise that resolves to the SPDN instance after /sessions succeeds and the
underlying P2P engine has loaded. Never call attachToHls() before this
resolves — the engine won't exist yet.
Bandwidth analytics
The SDK sends a small JSON heartbeat to /sdk/stats every 30 seconds.
Each beat carries delta byte counters (P2P + origin since the last beat) and a
snapshot of unique peer IDs seen during the session.
Per-token usage is aggregated into hourly buckets in the database; the dashboard's Developer panel surfaces them on the SDK Tokens tab (per-token view coming in a follow-up release). Until then, query the raw window:
GET /api/v1/sdk/tokens/{id}/usage?hours=24 // returns { buckets: [{ hourBucket, p2pBytes, originBytes, uniqueViewers }, ...] }
REST surface (under the hood)
Three endpoints back the SDK. You don't normally call them directly — the browser SDK does — but they're useful for debugging and for server-side integrations (e.g. dashboards).
POST /api/v1/sdk/sessions
Opens a viewer session. Browser SDK calls this on new SPDN().
| Field | Required | Notes |
|---|---|---|
appToken | yes | Long-lived spdn_app_… token |
streamId | yes | String |
viewerId | no | Stable per-viewer ID (cookie or device id) |
region | no | Tracker-routing hint (auto-resolved server-side if omitted) |
Returns { sessionToken, peerId, signalingUrl, trackerUrl, expiresAt, heartbeatSec }.
POST /api/v1/sdk/stats
Heartbeat. Browser SDK calls this every heartbeatSec seconds.
| Field | Required | Notes |
|---|---|---|
sessionToken | yes | From /sessions response |
p2pBytes | yes | Delta since last beat |
originBytes | yes | Delta since last beat |
uniqueViewers | no | Snapshot of distinct peer IDs seen |
Returns 204 on success, 410 if the session expired (SDK re-inits automatically).
Dashboard /api/v1/sdk/tokens
Cookie-authenticated CRUD for managing tokens from your own automation. Same surface the dashboard uses.
| Verb + Path | Action |
|---|---|
POST /api/v1/sdk/tokens | Create — returns raw token once |
GET /api/v1/sdk/tokens | List your tokens (prefix-only) |
PUT /api/v1/sdk/tokens/{id}/domains | Update allowed_domains list |
DELETE /api/v1/sdk/tokens/{id} | Revoke |
GET /api/v1/sdk/tokens/{id}/usage?hours=N | Hourly bandwidth buckets |
Requirements
Same constraints as every browser P2P CDN. The browser sandbox is unmovable:
- HTTPS everywhere. WebRTC +
fetch()only run in secure contexts. Your manifest, segments, and the page hosting the SDK all needhttps://. - CORS on your CDN. Add
Access-Control-Allow-Origin: *(or your exact domain) to.m3u8/.ts/.m4sresponses, plusAccess-Control-Allow-Headers: RangeandAccess-Control-Expose-Headers: Content-Length, Content-Range. - Modern hls.js (≥ 1.4). The integration is loader-shimmed via the SPDN P2P engine, which requires the current hls.js loader interface.
- Not Safari iOS for now. iOS Safari uses native HLS (no MSE), so the JS-loader hijack can't happen. Roadmap: a Service-Worker fallback.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
SDK throws auth failed (HTTP 403) origin_not_allowed |
Your page's host isn't in the token's allowed_domains | Dashboard → Edit domains → add it |
SDK throws auth failed (HTTP 401) invalid_token |
Token typo or already revoked | Verify the raw token; mint a new one if needed |
| Mixed Content error in console, video doesn't load | HTTPS page + HTTP manifest URL | Serve manifest over HTTPS (Cloudflare proxy works) |
| CORS error on segment fetch | Your CDN doesn't send Access-Control-Allow-Origin |
Add the CORS headers listed above to your origin/CDN config |
Player plays but ↓P2P stays 0 |
You're the only viewer (no peer pool yet), or NAT/firewall blocks UDP | Open a second tab with the same stream to seed; check WebRTC stats in chrome://webrtc-internals |
Cannot read properties of undefined (reading 'split') inside SDK init |
Stale CDN cache of the SDK — known race during a deploy | Hard refresh (Ctrl/Cmd-Shift-R) |
SDK loads, but spdn.ready never resolves |
spdn.tv blocked by network filter; SPDN engine can't load | Allow spdn.tv in your CSP / corporate firewall |
Still stuck? Mail info@spdn.tv with your dashboard email, the stream URL you're trying, and a copy of the DevTools console + Network tab output.