Skip to content

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? }:

CodeHTTPMeaningRetry?
INVALID_URN400malformed URNno
UNAUTHORIZED / FORBIDDEN401 / 403bad key / origin not allowed / DRM entitlement rejected (server)no
DRM_CLIENT0 or lic.client-side DRM failure — device/CDM can't play it, or license delivery broke without a server denialsee below
NOT_FOUND404unknown URN or not entitled (indistinguishable)no
GONE410ended past the catchup windowno
TOO_EARLY425upcoming, not live yetyes (waitForLive)
RATE_LIMITED429per-IP limiterback off
UNAVAILABLE503should-be-live, origin warming upyes

Four that regularly catch people out:

  • NOT_FOUND for a match you can see. The key isn't entitled to that tournament. 404 is deliberately indistinguishable from "unknown URN".
  • UNAUTHORIZED with a valid key. Wrong environment: a pk_test_… key against a production baseUrl, or a pk_live_… key against integration.
  • FORBIDDEN on 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 case httpStatus carries the license status if there was one, and 0 otherwise. A genuine license 401/403 still surfaces as UNAUTHORIZED/FORBIDDEN, so branch on code rather than on httpStatus. Pre-flight with detectDrmSupport() 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 on no-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". Without encrypted-media, EME can't start in a cross-origin frame (mountEmbed sets 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.

ISC licensed. Bundles hls.js (Apache-2.0).