Metadata-Version: 2.4
Name: videorouter-live
Version: 0.2.0
Summary: Python SDK for videorouter.sh: live video & world models (open, keep alive, settle a real-time session) and the /v1/videos generation API.
License: MIT
Project-URL: Homepage, https://videorouter.sh/docs/live-video
Keywords: videorouter,video,world-model,real-time,live-video,sdk
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.25
Dynamic: license-file

# videorouter-live (Python)

Python SDK for [videorouter.sh live video & world models](https://videorouter.sh/docs/live-video): open a real-time
session, keep it alive, settle it. Sync (`LiveVideo`) and asyncio (`AsyncLiveVideo`) clients; only dependency is `httpx`.

The SDK talks to the session **broker**. It never carries media: `session.connect` holds the host's short-lived
credentials, which you hand to the host's own SDK (Happy Oyster, Odyssey, Decart...) in the browser.

```python
from videorouter_live import LiveVideo

lv = LiveVideo(api_key="llmr_sk_...")            # 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:
    send_to_browser(s.connect)    # host credentials — returned once by the server
    ...                           # heartbeats run in the background; the session is settled on exit
print(s.billed_seconds, s.cost_usd, s.end_reason)
```

`lv.session(...)` creates the session, waits until it is `ready` (a world may need ~30–120 s to build; nothing is
metered while `waiting`), starts the heartbeat and settles it on exit even if your code raised.

## Browser tokens

Never put your key in a browser. Mint a short-lived, live-video-only token on your server:

```python
token = lv.create_token(models=["fal/happy-oyster-adventure"], expires_in=300,
                        max_sessions=1, max_session_duration_seconds=60)
# hand token.token to the browser SDK:  new LiveVideo({ token })
```

A token can only call `/v1/live-video` session endpoints — not billable endpoints, and not `create_token`.

## Generating videos

Both clients also wrap `/v1/videos` (API key required; browser tokens are live-video only). Creating a job is never retried.

```python
lv = LiveVideo()                                   # reads VIDEOROUTER_API_KEY
job = lv.videos.generate({"model": "heygen-video-1", "prompt": "A presenter says: Welcome!", "duration_secs": 5})
print(job.url)

queued = lv.videos.create({"model": "wan-3.0-prime", "prompt": "..."})
queued.wait(timeout=600)                           # raises VideoGenerationError / TimeoutError
```

`job.status`, `.done`, `.failed`, `.url` / `.urls`, `.error`, `.raw`, `.refresh()`, `.wait(timeout, poll_interval)`;
`AsyncLiveVideo` has the same surface as coroutines.

## API

| | |
|---|---|
| `LiveVideo(api_key=None, *, token=None, base_url=..., timeout=30, max_retries=2, http_client=None)` | `AsyncLiveVideo` has the same signature |
| `lv.models()` | `list[Model]` — `available` is `False` until a model has a verified price |
| `lv.create_token(*, models, expires_in, max_sessions, max_session_duration_seconds)` | `Token` |
| `lv.create_session(model, *, max_duration_seconds, options)` | `LiveSession` |
| `lv.get_session(id)` | `LiveSession` (never has `connect` — it is returned once) |
| `lv.session(model, ..., wait_until_ready=True, auto_heartbeat=True)` | context manager |

`LiveSession` (and `AsyncLiveSession`, where the network methods are coroutines), modelled on Reactor's client:

| | |
|---|---|
| `.id` / `.session_id`, `.model`, `.status`, `.get_status()` | `Status` is a `str` subclass: `"waiting"`, `"ready"`, `"ended"` |
| `.connect` | host credentials, or `None` |
| `.billed_seconds`, `.cost_usd`, `.end_reason`, `.recording_url`, `.world_id`, `.raw` | |
| `.refresh()` | re-read; advances a `waiting` session and returns `connect` when the world is ready |
| `.wait_until_ready(timeout=300, poll_interval=4)` | raises `TimeoutError` / `SessionStateError` |
| `.heartbeat()`, `.start_heartbeat(interval=None)`, `.stop_heartbeat()` | default interval is the server's 5 s |
| `.end(host_travel_id=None)` | settle; safe to call twice |
| `.on(event, fn)`, `.once(...)`, `.off(...)` | events: `status_changed`, `ended`, `heartbeat`, `error` |
| `.last_error` / `.get_last_error()` | last background-heartbeat error |

### Errors

Every failure raises a `LiveVideoError` subclass carrying `status`, `code`, `type`, `retry_after`:
`AuthenticationError` (401), `PermissionDeniedError` (403), `InvalidRequestError` (400),
`InsufficientCreditsError` (402), `PricingUnavailableError` (502), `NotFoundError` (404),
`RateLimitError` (429) → `ConcurrencyLimitError` / `HostCapacityError`, `UpstreamError` (5xx), plus
`SessionStateError` and `TimeoutError` raised locally.

### Retries

GETs, heartbeats and `end` are retried on network errors and 502/503/504. **Creating a session or a token is never
retried** — a retried create could open a second billed session.

## Billing in one paragraph

Opening a session holds `max_duration_seconds × price` against your credits; ending it debits the real cost
(whole seconds, at least the host's own reported duration) and releases the rest. A session that never became ready
costs nothing. If the heartbeat stops, the server settles the session itself — a forgotten tab costs at most the
silence window.
