Live video SDKs
Python SDK
A sync client (LiveVideo) and an asyncio client (AsyncLiveVideo) with the same surface; on the async client every network method is a coroutine. Read the SDK overview first for the concepts (broker model, tokens, connect, heartbeat).
videorouter-live. A live-video model can only be booked once its price is verified (see Live video & world models), and the SDKs surface that as PricingUnavailableError.pip install videorouter-live
Requires Python 3.9+. The only dependency is httpx.
Quickstart (sync)
from videorouter_live import LiveVideo
lv = LiveVideo(api_key="llmr_sk_live_...") # or set VIDEOROUTER_API_KEY
with lv.session(
"fal/happy-oyster-adventure",
max_duration_seconds=60,
options={"world": {"prompt": "a rainy cyberpunk city", "perspective": "first_person"}},
) as s:
# The session is already ready and heartbeating in the background.
send_to_browser(s.connect) # host credentials - returned once by the server
wait_for_user_to_finish()
# Settled on exit, even if your code raised.
print(s.status, s.billed_seconds, s.cost_usd, s.end_reason)
lv.session(...) creates the session, waits until it is ready, starts the heartbeat, and settles it on exit — even if your code raised. While a world is building the session is waiting and nothing is metered.
Async client
import asyncio
from videorouter_live import AsyncLiveVideo
async def main():
async with AsyncLiveVideo(api_key="llmr_sk_live_...") as lv:
async with lv.session("decart/lucy-2.5", max_duration_seconds=120,
options={"speed": "standard"}) as s:
s.on("error", lambda e: print("heartbeat problem:", e))
await hand_to_browser(s.connect)
await asyncio.sleep(30)
print(s.cost_usd)
asyncio.run(main())
Heartbeats run as an asyncio task on the running loop; handlers may be coroutine functions.
Manual lifecycle & events
s = lv.create_session("fal/happy-oyster-adventure", max_duration_seconds=60,
options={"world": {"prompt": "x", "perspective": "third_person"}})
print(s.status) # "waiting" - a world is building, nothing is metered
s.on("status_changed", lambda st: print("->", st))
s.on("ended", lambda sess: print("settled:", sess.cost_usd))
s.wait_until_ready(timeout=180, poll_interval=4) # polls; the poll that sees it ready returns s.connect
s.start_heartbeat() # daemon thread, every ~5 s
use(s.connect)
s.end(host_travel_id="...") # optional (Happy Oyster): bill on the host's own duration and fetch the replay
Use the manual flow when the session outlives one function, or when you want to own the polling. get_session(id) rebuilds a handle from an id (it never carries connect).
Browser tokens
token = lv.create_token(models=["fal/happy-oyster-adventure"], expires_in=300,
max_sessions=1, max_session_duration_seconds=60)
token.token # "llmr_sk_live_..." - give this to the browser
token.expires_at # ISO timestamp
Pass the token to the JavaScript SDK. A token can only drive live-video sessions (never billable endpoints, never create_token), is capped by the limits you set, and expires in expires_in seconds (1–21,600, default 3,600).
Generating videos
The clients also wrap the /v1/videos generation API (AsyncLiveVideo has the same surface as coroutines). Params are the API’s own fields, passed through unchanged — see Video generation. It needs an API key; a browser token is live-video only. Creating a job is never retried, so a flaky network cannot bill you twice.
from videorouter_live import LiveVideo
lv = LiveVideo() # reads VIDEOROUTER_API_KEY
# Create and wait
job = lv.videos.generate({
"model": "heygen-video-1",
"prompt": "A presenter says: 'Welcome to VideoRouter.'",
"duration_secs": 5,
})
print(job.url)
# Or step by step
queued = lv.videos.create({"model": "wan-3.0-prime", "prompt": "A robotic arm in a lab"})
queued.wait(timeout=600, poll_interval=5) # raises VideoGenerationError / TimeoutError
again = lv.videos.get(queued.id)job.status, job.done, job.failed, job.url / job.urls, job.error and job.raw are available on every job.
Handling errors
from videorouter_live import (InsufficientCreditsError, ConcurrencyLimitError, HostCapacityError,
PricingUnavailableError, LiveVideoError)
try:
s = lv.create_session("fal/happy-oyster-adventure", max_duration_seconds=60)
except InsufficientCreditsError:
... # top up, or book fewer seconds
except ConcurrencyLimitError as e:
... # end a session first; e.retry_after is set
except HostCapacityError as e:
time.sleep(e.retry_after or 5) # the host is full right now - sessions are never queued
except PricingUnavailableError:
... # model.available is False
except LiveVideoError as e:
print(e.status, e.code, e.message)
The full class table is in the overview. All classes subclass LiveVideoError and carry status, code, type, retry_after, body.
Reference
Clients
LiveVideo(api_key=None, *, token=None, base_url=..., timeout=30.0, max_retries=2, http_client=None)api_key is your server key; token is a browser-safe token from create_token. With neither, VIDEOROUTER_API_KEY is read. base_url defaults to https://videorouter.sh/api/v1. http_client lets you inject an httpx.Client (proxies, tests). Use as a context manager or call close().AsyncLiveVideo(...)Same arguments; http_client is an httpx.AsyncClient. async with or await aclose().models()list[Model]. available is False until the model has a verified price.create_token(*, models=None, expires_in=None, max_sessions=None, max_session_duration_seconds=None)Token. Server-side only.create_session(model, *, max_duration_seconds=None, options=None)LiveSession. Never retried.get_session(session_id)LiveSession (no connect).session(model, *, max_duration_seconds=None, options=None, wait_until_ready=True, auto_heartbeat=True, ready_timeout=300, poll_interval=4, heartbeat_interval=None)Context manager (async with on the async client): create → wait → heartbeat → settle on exit.LiveSession / AsyncLiveSession
id · session_idThe session id (lvs_...).modelThe model slug.status · get_status()Status: waiting, ready, ended (a str subclass, so s.status == "ready" works).connectHost credentials, or None (waiting, or a re-fetched session).stream_url · stream_headersComponent-edition models (vidu/vidu-s2-avatar-component) only, else None: the relayed WebSocket for PCM audio and transcripts, and the Authorization header it needs (a key never goes in the URL). Use any WebSocket client, e.g. websockets.connect(s.stream_url + "?conn_id=abc", additional_headers=s.stream_headers); the protocol is on the live-video page.world_id, billed_seconds, cost_usd, end_reason, recording_url, expires_atServer fields; the billing fields are set once ended.rawA copy of the last server response.refresh()Re-read. For a waiting session this advances it; the call that sees the world built returns connect.wait_until_ready(timeout=300, poll_interval=4)Raises TimeoutError or SessionStateError (ended while waiting).heartbeat()One beat. Raises SessionStateError on an ended session.start_heartbeat(interval=None) · stop_heartbeat()Background beat (daemon thread / asyncio task). Default interval: the server’s 5 s. Stops itself on 404 / 401.end(host_travel_id=None)Settle. Idempotent. host_travel_id (Happy Oyster) bills on the host’s own duration and fetches the replay.on(event, fn) · once · offEvents status_changed, ended, heartbeat, error. Unknown names raise ValueError.last_error · get_last_error()The last background-heartbeat error.Types
Modelid, kind (world / edit / ...), control, max_session_sec, available, price_per_second_usd, price_per_hour_usd, resolutions, description, speeds, fast_price_per_second_usd, rawTokentoken, expires_at, models, max_sessions, max_session_duration_secondsStatusWAITING, READY, ENDED — stringsTesting your code
import httpx
from videorouter_live import LiveVideo
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"object": "list", "data": []})
lv = LiveVideo(api_key="test", base_url="http://test/v1",
http_client=httpx.Client(transport=httpx.MockTransport(handler)))
assert lv.models() == []
The package ships a py.typed marker, so type checkers see the annotations.