# LiveKit: local setup and how we use it

We take three things from LiveKit and build everything else ourselves:

| From LiveKit | Used by | Purpose |
|---|---|---|
| LiveKit server (open source, unmodified, self-hosted) | Infrastructure | Carries audio, video, chat and screen shares |
| `livekit-api` (Python server SDK) | Our FastAPI backend, only in `app/livekit/adapter.py` | Meeting passes (tokens), room control, removals, permissions, webhooks |
| `livekit-client` + `@livekit/components-react` | Frontend | Connection, auto-reconnect, devices, pre-join, grid, controls, chat |

**LiveKit does not give us** (we build it): the waiting room and admission, one-share-at-a-time with requests, handover and Acting Host, held places, the organisation allowlist, revocation, audit.

**Not needed in Phase 1:** Egress (recording), Agents (AI). They can be added later without changing the setup.

## 1. Run LiveKit locally

No Docker. LiveKit server is a single program: download `livekit_1.13.11_windows_amd64.zip` from the [v1.13.11 release](https://github.com/livekit/livekit/releases/tag/v1.13.11) (macOS: `brew install livekit`), unzip it, and run it from the repo root with our config:

```powershell
& "$env:USERPROFILE\livekit\livekit-server.exe" --config livekit.yaml
```

`livekit.yaml` (repo root, local only) sets port 7880, the local key pair, the webhook to `http://localhost:8001/livekit/webhook`, `auto_create: false`, `max_participants: 25` and `departure_timeout: 120` (covers the 2-minute reconnection window). PostgreSQL is a normal local install (see README).

Browsers allow camera/mic on `http://localhost` without HTTPS, so single-machine testing needs no certificates or TURN. Phones/other laptops need HTTPS (mkcert or a tunnel such as cloudflared/ngrok).

## 2. Settings (`.env`, never committed)
```
LIVEKIT_URL=ws://localhost:7880         # given to the browser
LIVEKIT_API_URL=http://localhost:7880   # used by FastAPI
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret
```
Only these values change for dev/test/live servers (e.g. `wss://rtc.<domain>`, real keys).

## 3. The adapter (`app/livekit/adapter.py`): the only LiveKit caller

```python
from datetime import timedelta
from livekit import api

class LiveKitAdapter:
    def __init__(self, url: str, key: str, secret: str):
        self.key, self.secret = key, secret
        self.lk = api.LiveKitAPI(url, key, secret)

    def make_pass(self, room: str, identity: str, name: str, can_share: bool = False) -> str:
        sources = ["camera", "microphone"] + (["screen_share"] if can_share else [])
        return (api.AccessToken(self.key, self.secret)
                .with_identity(identity)            # "{product}:{org}:{user}:{device}" -> unique per device
                .with_name(name)
                .with_ttl(timedelta(minutes=10))
                .with_grants(api.VideoGrants(
                    room_join=True, room=room,
                    can_publish=True, can_subscribe=True, can_publish_data=True,
                    can_publish_sources=sources))
                .to_jwt())

    async def create_room(self, room: str):
        await self.lk.room.create_room(api.CreateRoomRequest(name=room, max_participants=25))

    async def end_room(self, room: str):              # End Meeting
        await self.lk.room.delete_room(api.DeleteRoomRequest(room=room))

    async def remove(self, room: str, identity: str): # revocation, removal
        await self.lk.room.remove_participant(api.RoomParticipantIdentity(room=room, identity=identity))

    async def participants(self, room: str):          # capacity, Acting Host, re-check
        return (await self.lk.room.list_participants(api.ListParticipantsRequest(room=room))).participants

    # update_permissions(room, identity, can_share)  -> api.UpdateParticipantRequest(permission=...)
    # set_attributes(room, identity, {...})           -> raised hand (with time raised), role
    # mute_track(room, identity, track_sid)           -> api.MuteRoomTrackRequest (host stops a share)
    # send_data(room, payload, to=[identities])       -> api.SendDataRequest (share request, "hand lowered")

    async def close(self):
        await self.lk.aclose()
```
Check exact class names against the installed `livekit-api` version. Tests use a `FakeLiveKitAdapter` with the same methods.

Create it once in the FastAPI lifespan (`app.state.lk`) and close it on shutdown.

## 4. Webhooks from LiveKit (`app/livekit/webhooks.py`)
```python
receiver = api.WebhookReceiver(api.TokenVerifier(KEY, SECRET))

@router.post("/livekit/webhook")
async def livekit_webhook(request: Request):
    event = receiver.receive((await request.body()).decode(), request.headers.get("Authorization"))
    # participant_joined / participant_left -> attendance, held places, Acting Host
    # track_unpublished (screen share)      -> free presenter slot, grant next requester
    # room_finished                         -> close session
```
LiveKit signs every event; unsigned or wrong events are rejected.

## 5. Join response shape (backend → frontend)
```json
{ "status": "admitted", "url": "ws://localhost:7880", "token": "<10-min pass>", "mic_on": false }
{ "status": "waiting", "request_id": "..." }
```
The frontend wraps the meeting in `<LiveKitRoom serverUrl={url} token={token} connect audio={micOn} video={camOn}>` with our HelixtaHR-styled grid, control bar, People panel and chat. Waiting users never connect to LiveKit.

## 6. Screen-share handover (HEL-980)
1. Passes are issued **without** `screen_share`.
2. B selects Share → frontend calls the browser screen picker immediately (`createLocalScreenTracks`) and keeps the track unpublished; calls `POST /v1/meetings/{id}/share-requests`.
3. Backend (per-meeting lock): no presenter → grant B (`update_permissions` with screen_share) → B publishes. Presenter A exists → `send_data` request with Stop sharing to A and hosts.
4. A stops (or a host stops A via `mute_track` + revoke) → `track_unpublished` webhook → grant B → B publishes automatically.
5. B cancels → stop the local track; request removed.

## 7. First check that everything works
1. Start `livekit-server --config livekit.yaml`, then `uvicorn app.main:create_app --factory --reload --port 8001`.
2. Call `/v1/meetings` (create) as a test Host token → receive a pass.
3. Open the frontend in two browser profiles as two users → video both ways; FastAPI logs `participant_joined`.

## 8. Servers later (dev / test / live)
- One LiveKit node per environment for Phase 1 (Redis only if scaling beyond one node); a modest VM (e.g. 4 vCPU / 8 GB) should handle 25-person meetings; confirm in the load test.
- Domain + TLS certificate (`wss://`), and LiveKit's built-in **TURN** (TLS on 443 + UDP 3478) for strict office/mobile networks.
- Firewall: 443/7880 (signalling), 7881 TCP, UDP media range (or TURN-only if UDP can't be opened).
- Per-environment API key/secret in the secret store; pin the LiveKit version; health endpoint + Prometheus metrics feed monitoring.
- Owner of these servers: open (HLD decision 12).
