Troubleshooting
The short list, ordered by how often it's the answer.
Nothing plays, every call fails
Your origin isn't allow-listed. Devtools → Network shows the CORS preflight failing. The key's AllowedOrigins (and AllowedIframeParents for embeds) and the DRM CORS allow-list must all include your origin — onboard it and allow a few minutes for propagation.
Error decoder
All API errors are a typed PlaybackError with { code, httpStatus, retryAfterMs?, requestId? }:
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
INVALID_URN | 400 | malformed URN | no |
UNAUTHORIZED / FORBIDDEN | 401 / 403 | bad key / origin not allowed / DRM rejected | no |
NOT_FOUND | 404 | unknown URN or not entitled (indistinguishable) | no |
GONE | 410 | ended past the catchup window | no |
TOO_EARLY | 425 | upcoming, not live yet | yes (waitForLive) |
RATE_LIMITED | 429 | per-IP limiter | back off |
UNAVAILABLE | 503 | should-be-live, origin warming up | yes |
The ones that surprise people:
NOT_FOUNDfor a match you can see — the key isn't entitled to that tournament. 404 is deliberately indistinguishable from "unknown URN".UNAUTHORIZEDwith a valid key — wrong environment: apk_test_…key against productionbaseUrlor vice versa.FORBIDDENon the license request only — the license URL was modified, or a different api-key signed the license call than the playback call. POST the URL verbatim, same key everywhere.
Autoplay doesn't start
Browser sound policy. Set muted: true for autoplay, or start playback from a user gesture. The managed player fires autoplayblocked (the embed forwards it as oddin:autoplayblocked) — show tap-to-play and call play() from the tap.
Player sits in waiting forever
The match hasn't gone live; the SDK is armed (SSE or polling). That's working as designed — bound it with waitForLive.timeoutMs if your UI needs to give up, and show countdown UX from the waiting event's retryInMs.
Black video
- With DRM, any browser — almost always CORS on the license POST: see DRM → CORS. Console will show an EME/CDM error.
- Chrome/Firefox/Edge on iOS — no Widevine CDM exists there. Call
detectDrmSupport()and route viewers to Safari onno-cdm. - Old Safari — the native fallback plays only OS-trusted passthrough; check the console.
Iframe embed loads but won't play
- The parent page's origin is missing from
AllowedIframeParents. - The iframe lacks
allow="autoplay; encrypted-media"— withoutencrypted-mediaEME can't start in a cross-origin frame (mountEmbedsets this for you).
Latency is higher than expected
The default target is ~2 s behind the live edge (~4–5 s glass-to-glass). If you're seeing tens of seconds: check for a liveLatencyTarget/hlsConfig override, and confirm with player.on('stats', s => s.latencySeconds). After a stall the player catches up at 1.5× automatically — sustained high latency usually means sustained insufficient bandwidth (watch bandwidthKbps vs the current rendition).
Still stuck
Grab PlaybackError.requestId (present on API-originated errors), the match URN, and a timestamp, and write to havik-support@oddin.gg. Current platform health: status-dev.oddin-video.gg.