Transport
Session phases, reconnection, and consume-only viewer attach
The SDK manages the media transport for you. Invoking a function allocates a session, resolves a connect handle from the control plane, negotiates a media transport, and streams media — with regional failover and automatic reconnection. You observe it through the high-level Session phase, not a raw connection object.
Session phases
Every session exposes one of nine phases you can render directly:
idle → queued | unavailable | provisioning → connecting → live → ended | expired
↘ error| Phase | Meaning |
|---|---|
idle | Created, not yet admitted or connected |
queued | Waiting in the admission queue — phase.queue = { position, depth } |
unavailable | The control plane reports no runtime exists to serve this function (not deployed, crashed, or scaled to zero) — non-terminal; polling continues and the session recovers when a runtime appears |
provisioning | Admitted; GPU compute is being allocated/warmed |
connecting | Transport is establishing the media session |
live | First media track has arrived |
error | Admission or transport failed — phase.error = { reason, code?, kind? } |
ended | Intentionally disconnected / closed |
expired | The session reached its declared maximum length — terminal and honest, distinct from error (nothing failed; see session.endsAt) |
The waiting phases (queued/unavailable/provisioning — isWakingPhase()) also carry phase.runtime ({ state: 'unavailable' | 'starting' | 'busy', reason? }) and phase.wakingSince, one continuous wait timestamp across the whole wake. Subscribe with onPhase, which fires immediately with the current phase and on every transition — describeSessionPhase(p) gives honest default copy:
import { describeSessionPhase } from '@urun-sh/core'
session.onPhase((p) => {
statusEl.textContent = describeSessionPhase(p) // "Starting model…", queue position, …
if (p.name === 'live') hideSpinner()
})
await session.whenLive() // resolve at 'live'; reject with a typed SessionFailedErrorRender the waiting phases as real progress (a queue position, "starting model…", an elapsed wake counter) rather than one opaque spinner. See Session lifecycle for the full honest-UX toolkit.
Reconnection
Beneath the phase sits a lower-level TransportState: connecting | connected | reconnecting | renegotiating | disconnected | failed. reconnecting is the typed signal that the SDK lost the transport and is actively re-dialing (and re-resolving the connect handle, including cross-region redirects). renegotiating is the narrower signal that the session is still admitted but its media path went dead (a recycled backend) and the SDK is re-establishing onto the rebound backend for the same session id. Treat both as recoverable — render a hint, not a dead session.
What survives a reconnect. A transport drop does not by itself tear down the session: the runtime process stays resident while the SDK re-dials, so a successful reconnect re-attaches to the same warm session — no cold start, no re-invocation. The control doc re-syncs on reconnect via CRDT merge, so session state (prompt, pose, settings) is preserved rather than reset, and an in-flight session.request(...) survives because it rides the doc. If reconnection does not succeed within the session's disconnect grace window, the session is reaped and its replica freed; a later invocation then opens a fresh session (and may cold-start). State you need to outlive that window should be written to Store, not held only in the control doc.
Transport negotiation
The SDK dials QUIC/WebTransport first and falls back to WebRTC — negotiation self-gates on browser capability and server support, so most apps never think about it. session.mediaTransport reports which path carries media: 'webtransport' once QUIC won the dial, 'webrtc' after the floor or a failover, null while undecided or where negotiation never ran (Node, older browsers). AppOptions.allowWebTransport: false forces the WebRTC path.
On the WebRTC path, uRun handles restrictive networks for you: ICE servers arrive with the connect handle (always STUN; a TLS-over-TCP/443 relay only as a last resort on DPI/UDP-blocking networks). You never configure ICE servers yourself; relayed paths add a little latency, and live is your signal that media is flowing regardless of which path won.
Streams
Inbound and outbound media flow through named streams on the session:
const video = session.stream('video')
// Inbound: the latest track produced by the function
video.on('track', (track) => {
if (track) videoEl.srcObject = new MediaStream([track])
})
// Outbound: publish a local track upstream — attach() for mic audio,
// attachVideo() for camera video (a later call swaps the source via replaceTrack)
const cam = await navigator.mediaDevices.getUserMedia({ video: true })
await video.attachVideo(cam.getVideoTracks()[0])
// later: await video.detachVideo() (audio: attach() / detach())
// Seek a persisted finite stream, or return to the live edge:
await video.seek(12.5)
await video.seek('live')Streams are recorded by default (the DVR), so seek(t) addresses the retained recording window; seek('live') snaps back to the live edge. Manage retention (retain/pin/permanent/delete) through session.recordings — see Session lifecycle.
Stream names are the contract with the Python side: session.stream("video") ↔ ctx.stream("video").
Viewer attach (consume-only)
To join an already-running session as a watcher — without taking the publisher slot or allocating a GPU — use Session.attach:
import { Session } from '@urun-sh/core'
const viewer = await Session.attach(sessionId, {
baseUrl: 'https://urun.sh',
orgId: 'your-org-id',
jwt: await getCurrentUserJwt(),
})
const video = viewer.stream('video')
video.on('track', (track) => { if (track) el.srcObject = new MediaStream([track]) })A viewer watches the live producer. It never allocates a GPU, never takes the publisher slot, and never tears down the session when it disconnects. Cross-org or unknown session ids are rejected.
Related:
- App reference —
Session,stream,doc,onPhase,disconnect - React Providers — the React entry point
- Getting Started — render and steer a session