URL: /docs/sdk-client

---
title: "@rill/client"
description: "Browser SDK: connect, media, labeled data. Hides SDP and ICE."
icon: monitor
---

{/* Generated by docs/scripts/generate-sdk-docs.mjs via TypeDoc from sdk/client/src/index.ts. Do not edit. */}

Browser SDK. SDP, ICE, and `RTCDataChannel` stay inside the package. Mint `token` with `@rill/server`. Walkthrough: [Your app](/your-app).

```sh
npm i @rill/client
```

Until a `v*` tag exists, use this repo's `sdk/` workspace.

```ts
import { Rill } from "@rill/client";

const video = document.querySelector("video")!;

async function mintFreshToken(): Promise<string> {
  return "paste-jwt";
}

const call = await Rill.connect({
  url: "https://rill.example.com",
  token: "paste-jwt",
  onToken: () => mintFreshToken(),
});

call.on("remote", (remote) => {
  video.srcObject = remote?.camera ?? null;
});
call.on("error", ({ code }) => console.error(code));
call.on("ended", () => console.log("call ended"));

await call.microphone.enable();
await call.camera.enable();
```

## Connect

```ts
Rill.connect(opts: ConnectOptions): Promise<Call>

type ConnectOptions = {
  url: string;
  token: string;
  onToken?: () => string | Promise<string>; // Called once on `token_expired`. Return a freshly minted JWT.
  speaker?: boolean; // Hidden speaker for `remote.microphone`. Default on. Pass `false` to opt out.
};
```

- **`Rill.connect`** — Join with a short-lived JWT. Resolves after the signaling welcome + first offer/answer. `speaker` defaults to on: a hidden `<audio>` plays `remote.microphone`. Pass `onToken` to rotate once on `token_expired`.
- **`ConnectOptions`** — `Rill.connect` options. `url` is the public origin (HTTPS or the site that proxies `/v1/signal`).

## Media

```ts
call.microphone: LocalDevice
call.camera: LocalDevice
call.screen: LocalDevice
call.remote: Remote | null
call.speaker: Speaker | null

type LocalDevice = {
  enable: (deviceId?: string) => Promise<void>; // Capture this kind. Pass `deviceId` to switch if already on.
  disable: () => Promise<void>;
  readonly kind: TrackKind;
  readonly enabled: boolean;
  readonly stream: MediaStream | null; // Local MediaStream when enabled; SDK still hides the PeerConnection.
};

type Remote = {
  id: string;
  name?: string;
  microphone: MediaStream | null;
  camera: MediaStream | null;
  screen: MediaStream | null;
};
```

- **`call.microphone`** — Local microphone. `enable()` is `getUserMedia` + `replaceTrack`.
- **`call.screen`** — Local screen share (`getDisplayMedia`). Browser “stop sharing” calls `disable()`.
- **`call.remote`** — Other participant, or `null` while waiting / after they leave.
- **`call.speaker`** — Hidden speaker for `remote.microphone`, or `null` when `speaker: false`.
- **`Remote`** — Other participant. Bind `camera` / `screen` to `<video>`. Do not attach `microphone` to a second `<audio>`.

## Data

```ts
call.send(label: string, data: string | Uint8Array): Promise<void>
```

- **`call.send`** — Send on a labeled data channel. Queues while the local channel is `connecting`. Resolves once `RTCDataChannel.send` accepts the payload (not after remote delivery). A missing peer is not an error — the SFU drops live relay like unpublished camera. Max payload 128 KiB (str0m write buffer); chunk larger snapshots in the app.

## Events and hang-up

```ts
call.on<K extends keyof CallEvents>(type: K, fn: (payload: CallEvents[K]) => void): () => void
call.state: CallState
call.disconnect(): Promise<void>

type CallEvents = {
  remote: Remote | null;
  local: LocalSnapshot;
  state: CallState;
  ended: Record<string, never>;
  error: CallError;
  data: DataMessage;
};

type DataMessage = {
  label: string;
  data: string | Uint8Array;
};
```

- **`call.on`** — Subscribe. Returns unsubscribe.
  - `remote` — other participant’s streams, or `null` when they leave
  - `local` — `{ microphone, camera, screen }` enabled flags
  - `state` — `connecting` | `connected` | `reconnecting` | `disconnected`
  - `ended` — slot destroyed (`call_ended` or `server_shutdown`)
  - `error` — `{ code, message }`
  - `data` — labeled message `{ label, data }`. Do not subscribe from `useCall`.
- **`call.state`** — Signaling + ICE lifecycle.
- **`call.disconnect`** — Leave this browser. Does not destroy the call; the other person stays.
- **`CallEvents`** — Event map for `call.on`. `useCall` does not subscribe to `data`.
- **`DataMessage`** — Payload from `call.on("data")`. `data` is a string or a copy of the binary frame.

## Errors

```ts
type ErrorCode =
  | "unauthorized"
  | "forbidden"
  | "call_not_found"
  | "call_full"
  | "call_ended"
  | "participant_invalid"
  | "token_expired"
  | "overloaded"
  | "connection_failed"
  | "permission_denied"
  | "replaced"
  | "server_shutdown"
  | "unsupported"
  | "internal";
```

- **`ErrorCode`** — Public error `code` strings. `call_full` is a third join; `replaced` is last-writer-wins.
