Troubleshooting
The short list, ordered roughly by how often each one turns out to be 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 entitlement rejected (server) | no |
DRM_CLIENT | 0 or lic. | client-side DRM failure — device/CDM can't play it, or license delivery broke without a server denial | see below |
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 |
Four that regularly catch people out:
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 a productionbaseUrl, or apk_live_…key against integration.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.DRM_CLIENT. The device or browser can't play this DRM: no usable key system, or a CDM/EME error. License delivery can also fail without a server denial, in which casehttpStatuscarries the license status if there was one, and0otherwise. A genuine license401/403still surfaces asUNAUTHORIZED/FORBIDDEN, so branch oncoderather than onhttpStatus. Pre-flight withdetectDrmSupport()and message the viewer; Retry only helps when the failure was transient transport.
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), so 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 is 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, in any browser. Almost always CORS on the license POST: see DRM → CORS. The 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-media, EME 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, so sustained high latency usually means sustained insufficient bandwidth (watch bandwidthKbps against 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.