@v4/eltv
The EuroLeague TV livestream player over Deltatre's Vesper SDK: access contract, state machine, polling and token relay. Not yet on a page.
Overview
@v4/eltv is the EuroLeague TV livestream player. It is the only package that imports Deltatre's Vesper Web SDK, @dicetechnology/vesper-sdk-web, pinned exactly at 2.0.0. It depends on @v4/ghost-video for the vendor-neutral LivestreamFrame. Club-only.
It is named for the PRODUCT, not the vendor: when the OTT RFP replaces Deltatre, the access contract and the page's import survive, and only src/vesper/ and src/client/vesper/ change.
Status (6 Nov 2026). The frontend is built and tested. The backend it talks to (the access endpoint) is not built, and the game page that will host the player does not exist yet, so nothing on the site renders the player today: livestreamSettings and livestreamLabels in apps/web/src/lib/eltv.ts have no consumer.
The access contract, the states and the architecture are documented in EuroLeague TV livestream, and its place among the club's other third-party seams in Club integrations. This page is the package surface.
Entry points
| Import | What it holds | Imports the SDK / design system |
|---|---|---|
@v4/eltv |
Pure and server-safe: the access contract, the state machine, polling, links, formatting, the token relay | The SDK by TYPE only; no design system |
@v4/eltv/client |
"use client": the Vesper mount, the access hook, the HTTP access source |
The SDK by value, through one import() in an effect; no design system |
@v4/eltv/player |
"use client": the composed LivestreamPlayer |
Both |
The root may import the SDK only as types because the SDK's bundle reads self and document the moment it is evaluated.
@v4/eltv (src/index.ts)
Access contract (
src/access.ts) —readLivestreamAccess,LIVESTREAM_ACCESS_VERSION(1; any other version is refused),LIVESTREAM_ACCESS_STATUSES:noStream,notStarted,live,watchOnEltv,ended,loginRequired,notEntitled,geoBlocked. TypesLivestreamAccess,LivestreamAccessStatus,PlaybackTokens,AccessSource(a source never rejects),AccessResult,AccessFailure,AccessRequest,AccessRequestReason,ReadAccessOptions. The backend's body is normalised once at the boundary, and a body that cannot be trusted is a failure, never a guess.Fixtures (
src/fixtures.ts) —createFixtureSource,FIXTURE_WATCH_ORIGIN,LIVESTREAM_ACCESS_FIXTURES(one raw body per status), typesFixtureAnswer,FixtureRequest. Stories and tests run the player with no backend.State machine (
src/state.ts) —resolveLivestreamState,isTerminalSignal,recheckFor; typesLivestreamState,LivestreamStateKind,LivestreamInputs,PlayerSignal,PlayerSignalKind,RecheckPhase. The precedence is fixed:- no answer yet → checking;
- a transport failure with no good answer → error;
- any non-live backend status beats any player signal (so a stream that ended never shows as an error);
- only while the backend says
livedo the player's signals decide — and a refused or failed signal earns ONE re-check with the backend before the copy is chosen, because the SDK'sload()reports every failure as the same code-lessSOURCE_ERROR, and a heartbeat 401 and 403 both arrive as the geo beacon.
Links (
src/links.ts) —accessRequestUrlwithACCESS_GAME_PLACEHOLDER({gameCode}),loginHrefwithLOGIN_RETURN_PLACEHOLDER({returnTo}),watchHref,watchOriginOf,sanitiseWatchUrl(a watch URL from the backend survives only on the ELTV origin). The return URL is built on the server fromseo.baseUrl, never from a query string.Polling (
src/polling.ts,src/settings.ts) —nextPollDelayMs,shouldResume, and the bounds:Constant Value Meaning LIVESTREAM_ACCESS_TIMEOUT_MS10 s One access request, before it counts as a failure LIVESTREAM_START_TIMEOUT_MS20 s SDK import to a loaded stream (nothing in the SDK bounds it) LIVESTREAM_POLL_MIN_MS/_MAX_MS15 s / 5 min The access poll while a stream has not started LIVESTREAM_RESUME_STALE_MS30 s A returning tab re-asks only when the last answer is older LIVESTREAM_POLL_GIVE_UP_MS3 h Stop polling a stream still not started this long after its start Settings and copy — types
LivestreamSettings,VesperSettings,VesperEnvironment(prod|stag),LivestreamAutoplay(off, the default, ormuted— design to decide),LivestreamLabels,LivestreamNoticeCopy;formatStartsAtwithFormatOptionsandStartsAtPartsfor the "Stream starts" clock.The token relay (
src/vesper/auth-relay.ts) —createRelayAuthManagerandRenewTokens. This is THE sign-in seam and the only place a Vesper token is handled. It takes tokens only from the backend's access answer, gives them only to the SDK, holds them in memory for one player mount, and renews by asking the backend again — no OIDC, no login flow, no cookies, no storage, no request of its own.Disclosed for sign-off: the SDK itself writes both tokens to
localStorage(vesper-sdk-<realm>-<env>_authTokenand_refresh) and never clears them, not even ondestroy(). That is the vendor's behaviour. If tokens are ruled out of the browser altogether, the backend answerswatchOnEltvinstead ofliveand the relay is never reached.
@v4/eltv/client (src/client/index.ts)
LivestreamMountandLivestreamMountProps— the Vesper mount. The SDK is imported by value only through the oneimport()insrc/client/vesper/load.ts, inside an effect.useLivestreamAccessandLivestreamAccessHandle— asks, polls and re-checks.httpAccessSourceandHttpAccessSourceOptions— the browser GET to the access endpoint: with credentials, only anAcceptheader (so a CORS simple request),no-store.
@v4/eltv/player (src/player/index.ts)
LivestreamPlayerandLivestreamPlayerProps—gameCode(the Competition Engine's, opaque),settings(undefinedrenders nothing),labels(resolved on the server),locale,returnUrl(built on the server), optionaltimeZone,autoplay(defaultoff; the SDK itself defaults to autoplay) andsource(fixtures in tests). Keyed bygameCode.frameViewForandFrameViewContext— maps a state onto ghost-video'sLivestreamFrameview.
src/vesper-sdk-css.d.ts declares the SDK's stylesheet as a module rather than using @ts-expect-error, because apps/web also compiles this source through transpilePackages.
Configuration
The feature is declared in apps/web/club.config.ts (features.livestream) only when the key is set. Every value is public by nature: the SDK sends them from the reader's browser (x-api-key, Realm). Per-environment values are in 1Password. Leave a variable unset, never empty.
| Variable | Required | Meaning |
|---|---|---|
NEXT_PUBLIC_VESPER_API_KEY |
The switch | Unset: OFF — no player, and the ~3.2 MB SDK is never downloaded |
NEXT_PUBLIC_VESPER_BASE_URL |
With the key | Vesper's API origin, no path or trailing slash (staging https://euroleague.api.onvesper-staging.com, production https://euroleague.api.onvesper.com) |
NEXT_PUBLIC_VESPER_ENVIRONMENT |
With the key | stag or prod. No default: a staging key must never run as production |
NEXT_PUBLIC_ELTV_WATCH_URL |
With the key | The ELTV site every link-out falls back to (https://euroleague.tv) |
NEXT_PUBLIC_ELTV_LOGIN_URL |
Optional | The login page; may contain {returnTo}. Absent: no login call to action |
NEXT_PUBLIC_ELTV_ACCESS_URL |
Optional | The livestream access endpoint, a template containing {gameCode}. Absent: the player renders nothing |
The watch, login and access URLs must all be https. The realm (dce.euroleague) is a club fact written in club.config.ts, not an environment variable. Vesper's server-held credentials belong to the access backend, never here. The schema is features.livestream in packages/template-config/src/schema.ts.
The SDK is a private npm package: NPM_TOKEN (locally in the root .env, the GitHub secret in CI) must be able to read @dicetechnology as well as @stadionhq-v3.
How the app wires it
| File | Role |
|---|---|
apps/web/src/lib/eltv.ts |
The ONE config point, server-only: livestreamSettings (undefined when off), livestreamLabels(t) (the liveStreamPlayer.* dictionary keys, plus the reused subscriptionOverlay.membersOnly.logInCTA, error.title and error.retry), and the _EnvironmentsAgree compile-time check that the schema's environments and VesperEnvironment agree both ways |
apps/web/src/lib/analytics.ts |
The watch host joins the eltv outbound destinations |
apps/web/next.config.ts |
Listed in transpilePackages |
There is no provider in the root layout: one player sits on one page, so the game page will pass livestreamSettings and livestreamLabels(t) to LivestreamPlayer as props.
Scripts and tests
test / test:watch (vitest, jsdom), lint, check-types. Suites cover the access contract, the state machine, polling, formatting, links, the token relay and signal map, the access source, the mount, the hook and the SDK loader, the player and its frame view, and a server render of each "use client" entry. The SDK's test double is src/__fixtures__/vesper.ts.