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>
That's the whole thing. No code changes on your origin, no manifest rewrite, no custom auth. Reload the page, open DevTools → Network, and watch .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

  1. Go to Dashboard → Developer.
  2. Open the SDK Tokens tab.
  3. Enter a name (e.g. customer-prod) and at least one allowed domain.
  4. Click Create SDK token. The raw token is revealed once — copy it.
One-time reveal. The dashboard stores only a bcrypt hash of the token. If you lose it, mint a new one — there's no recovery path.

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.

EntryMatches
example.comexact host match only
*.example.comany 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.

JW Player uses a different integration shape — instead of 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)

OptionTypeRequiredNotes
tokenstringyesApp token from the dashboard (spdn_app_…).
streamIdstringyesStable identifier per stream. All viewers of the same stream MUST share this.
apiBasestringnoOverride the SPDN backend host. Defaults to https://spdn.tv.
debugbooleannoVerbose console output. false by default.
onReadyfunctionnoCalled after /sessions succeeds and the engine has loaded.
onErrorfunctionnoCalled 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().

FieldRequiredNotes
appTokenyesLong-lived spdn_app_… token
streamIdyesString
viewerIdnoStable per-viewer ID (cookie or device id)
regionnoTracker-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.

FieldRequiredNotes
sessionTokenyesFrom /sessions response
p2pBytesyesDelta since last beat
originBytesyesDelta since last beat
uniqueViewersnoSnapshot 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 + PathAction
POST /api/v1/sdk/tokensCreate — returns raw token once
GET /api/v1/sdk/tokensList your tokens (prefix-only)
PUT /api/v1/sdk/tokens/{id}/domainsUpdate allowed_domains list
DELETE /api/v1/sdk/tokens/{id}Revoke
GET /api/v1/sdk/tokens/{id}/usage?hours=NHourly 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 need https://.
  • CORS on your CDN. Add Access-Control-Allow-Origin: * (or your exact domain) to .m3u8 / .ts / .m4s responses, plus Access-Control-Allow-Headers: Range and Access-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.
HTTP-only IPTV source? The browser will block mixed content. Two paths: (a) put a CDN in front of your origin (Cloudflare's free tier does HTTPS termination + CORS in one step), or (b) use SPDN Pro embed instead — we ingest HTTP sources server-side and serve HTTPS at the edge.

Troubleshooting

SymptomLikely causeFix
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.