# Personal Live API (user's own live broadcast)

A user goes live from the app without creating a match. Viewers watch,
comment and react in real time. This is separate from Match Live, which uses
YouTube and is unchanged.

| Part | Where it runs |
|---|---|
| Video (camera → viewers) | **Self-hosted LiveKit server** (open source, Apache-2.0, no per-minute fees) |
| Permissions, comments, reactions, viewer count, LIVE badges, notifications | This API + the existing websocket |

The app never sees the LiveKit secret. Each start or join returns a short-lived
token for one room. Hosts may publish; viewers may only watch.

## Why YouTube (Match Live) is not reused

- **One stream at a time.** Match Live streams to one CFA channel with one stream key, so two users could not be live together.
- **No privacy.** YouTube cannot enforce Followers-only. Anyone with the link can watch.
- **Viewers leave the app.** They are sent to YouTube, so in-app comments, reactions and moderation are impossible.
- **10–30 s delay.** Live comments would not line up with the video.

## Deploy

### 1. Database (before deploying the code)

```bash
node src/scripts/create-personal-live-tables.js   # idempotent
```

### 2. LiveKit server

Run it on a server with a public IP. It can be the API box if it has bandwidth to spare.

```bash
# API key + secret
docker run --rm livekit/livekit-server generate-keys
```

`livekit.yaml`:

```yaml
port: 7880
rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 60000
  use_external_ip: true
keys:
  <API_KEY>: <API_SECRET>
webhook:
  api_key: <API_KEY>
  urls:
    - https://api.cricketfanapp.com/webhooks/livekit
room:
  empty_timeout: 120
logging:
  level: info
```

```bash
docker run -d --name livekit --restart unless-stopped --network host \
  -v $PWD/livekit.yaml:/livekit.yaml livekit/livekit-server --config /livekit.yaml
```

- **TLS:** put `live.cricketfanapp.com` behind nginx/Caddy (HTTPS, websocket upgrade) and proxy it to `127.0.0.1:7880`.
- **Firewall:** open 443/TCP, 7881/TCP and 50000–60000/UDP. Video travels over the UDP range, so if it is closed users connect but see no video.

### 3. API env

| Var | Purpose |
|---|---|
| `LIVEKIT_URL` | `wss://live.cricketfanapp.com` (sent to the app) |
| `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` | Same pair as in `livekit.yaml` |
| `PERSONAL_LIVE_MAX_MINUTES` | Max duration, default **60**. The app shows it before Start. |
| `PERSONAL_LIVE_RECONNECT_SECONDS` | How long a dropped host has to come back, default **60** |
| `PERSONAL_LIVE_MAX_VIEWERS` | Room capacity, default **500** |

Without the three `LIVEKIT_*` vars, live is turned off. `/config` returns
`available: false`, and the app says "Live is not available right now".

These defaults are **not product decisions yet**. The brief asks for duration,
capacity, reconnect timeout and so on to be agreed before launch, and each one
is an env var so changing it needs no code change.

## Running cost (the "free" part)

There is no vendor bill. The cost is server bandwidth: the server sends one copy
of the video to every viewer.

- **Rough figure:** a 720p stream is about 1.5–2.5 Mbps per viewer, so 100 viewers need about 150–250 Mbps of upload.
- **Poor networks:** viewers on weak connections automatically get a lower layer (simulcast), which uses less.

Measure real usage in the pilot before a wide launch.

## Endpoints

All under `/api/personal-live`. Every call needs the `X-API-Key` header and `user_id` (body or query), the same as the rest of the API.

```
GET  /config                    available, max_duration_minutes, reactions
GET  /active?user_id=           lives this user may watch now (LIVE badges)
POST /start                     {user_id, title?, audience: public|followers}
GET  /:id?user_id=              status (+ summary if ended)
POST /:id/join                  {user_id} → token, live, recent comments, is_following
POST /:id/end                   {user_id} host only → summary
GET  /:id/summary?user_id=      host only
GET  /:id/comments?user_id=
POST /:id/comments              {user_id, message ≤ 300}
POST /:id/reactions             {user_id, emoji ∈ ❤️ 🔥 👏 😂 😮 🏏}
PUT  /:id/comments-enabled      {user_id, enabled} host only
POST /:id/remove-viewer         {user_id, viewer_id, block?} host only
POST /:id/report                {user_id, reason}
POST /webhooks/livekit          LiveKit → API (signed; not under /api)
```

### Errors the app shows

| Status | Meaning |
|---|---|
| `403` + `reason` | `followers_only`, `removed` or `blocked` |
| `404` | Live not found |
| `409` | Already live on another device |
| `410` | Live has ended |
| `503` | LiveKit not configured |

## Rules (all enforced on the server)

- **One active live per host.** A partial unique index guarantees it. A leftover live whose host is no longer in the room is closed when they start again.
- **Followers-only** is checked on every join, comment, reaction and websocket subscribe. A shared link does not bypass it.
- **Blocked users** (either direction) cannot see, join or find the live.
- **Removed viewers** are kicked from the room and cannot rejoin that live. "Remove and block" also adds a normal block.
- **Follower notifications** go once per live, through the existing notification flow. Blocked followers are skipped, and the user's notification setting is respected.
- **Live ends** when one of these happens:
  - the host ends it;
  - the max duration is reached;
  - the host is out of the room longer than the reconnect window.

  A 30 s sweeper enforces the last two, and still works if the webhook is missing. Everyone watching gets `personal_live_ended` with the reason, so the app never cuts the broadcast silently and LIVE badges don't go stale.

## Websocket

Same socket as the rest of the app (`wss://api.cricketfanapp.com`).

```json
{ "type": "join_personal_live", "live_id": 12, "user_id": 34 }
```

Server → client events:

| Event | Fields |
|---|---|
| `personal_live_joined` | `viewer_count`, `comments_enabled` |
| `personal_live_denied` | `reason` |
| `personal_live_viewers` | `viewer_count` (unique viewers, host excluded) |
| `personal_live_comment` | `comment` {id, user_id, name, profile_picture, message, created_at} |
| `personal_live_reaction` | `emoji`, `user_id` |
| `personal_live_comments_toggled` | `enabled` |
| `personal_live_removed` | `user_id` |
| `personal_live_ended` | `reason`, `summary` |

## Not in this release

- **Recording / replay.** This needs the LiveKit Egress service and storage. The app tells the host "Not recorded".
- **Admin screen** for live reports and terminating a live. Reports are stored in `personal_live_reports`, and `finishLive(id, 'admin')` is exported for the admin route.
- **Guest hosts, gifts and face filters** are a later phase, per the brief.

## Known limitation

Like every endpoint in this API, Personal Live trusts the `user_id` sent with a
valid `X-API-Key`. There is no per-user session token, so a modified client
could act as another user. The permission rules above are correct, but they can
only be as strong as that identity. Adding per-user auth (e.g. JWT at login) is
an app-wide change and is recommended before a wide launch.
