# @koi-video/voice-realtime-sdk

[中文文档](https://cdn.jsdelivr.net/npm/@koi-video/voice-realtime-sdk@latest/README.zh-CN.md)

Browser SDK for real-time voice: REST session (`/start`, `/stop`) with Robot-Key / Robot-Token, plus WebRTC (mic, remote audio, stream messages). The Agora Web SDK is a normal dependency of this package.

---

## Installation

```bash
pnpm add @koi-video/voice-realtime-sdk
```

```js
import { ChatClient, VoiceEvents } from '@koi-video/voice-realtime-sdk';
```

| Export | Description |
|--------|-------------|
| `ChatClient` | Main client. |
| `VoiceEvents` | Event name constants (see [Events](#events)). |

---

### Minimal flow (methods + events)

```js
import { ChatClient, VoiceEvents } from '@koi-video/voice-realtime-sdk';

const client = new ChatClient(
  {
    robotKey: process.env.ROBOT_KEY,
    robotToken: process.env.ROBOT_TOKEN
  },
  {
    userName: 'demo-user-1'
  }
);

client.on(VoiceEvents.SESSION_CREATED, ({ sessionId }) => {
  console.log('SESSION_CREATED', sessionId);
});

client.on(VoiceEvents.SESSION_STARTED, ({ sessionId }) => {
  console.log('SESSION_STARTED', sessionId);
});

client.on(VoiceEvents.SESSION_ENDED, ({ sessionId, reason }) => {
  console.log('SESSION_ENDED', sessionId, reason);
});

client.on(VoiceEvents.USER_MESSAGE, ({ sessionId, content, segmentId, raw }) => {
  console.log('user:', content, segmentId, raw);
});

client.on(VoiceEvents.ROBOT_MESSAGE, ({ sessionId, content, segmentId, raw }) => {
  console.log('bot:', content, segmentId, raw);
});

client.on(VoiceEvents.INTERRUPT, ({ sessionId }) => {
  console.log('INTERRUPT', sessionId);
});

client.on(VoiceEvents.AUDIO_MUTED, ({ sessionId }) => {
  console.log('AUDIO_MUTED', sessionId);
});

client.on(VoiceEvents.AUDIO_UNMUTED, ({ sessionId }) => {
  console.log('AUDIO_UNMUTED', sessionId);
});

client.on(VoiceEvents.ERROR, err => {
  console.error(err.code, err.message);
});

client.on(VoiceEvents.ALL, (eventName, data) => {
  console.log('ALL', eventName, data);
});

const sessionId = await client.startVoiceChat();

client.interrupt();
await client.setAudioEnabled({ bool: false });
await client.setAudioEnabled({ bool: true });
await client.stopVoiceChat();
```

---

## Events

Subscribe with `client.on(VoiceEvents.XXX, handler)` or `client.on(VoiceEvents.ALL, (eventName, data) => ...)`.

### `VoiceEvents`

| Constant | When / payload |
|----------|------------------|
| `SESSION_CREATED` | After `/start` + RTC ready — `{ sessionId }`. |
| `SESSION_STARTED` | Immediately after `SESSION_CREATED`, same `{ sessionId }`. |
| `SESSION_ENDED` | Stop, RTC disconnect, remote presence timeout, or stop failure — `{ sessionId, reason }`. |
| `USER_MESSAGE` | Stream `topic === 'chat'`, user — `{ sessionId, content, segmentId?, timestamp?, raw }`. `raw` is the full parsed server JSON for that message; `content` / `segmentId` / `timestamp` are derived for convenience. |
| `ROBOT_MESSAGE` | Stream `topic === 'chat'`, assistant — same shape. |
| `AUDIO_MUTED` / `AUDIO_UNMUTED` | After `setAudioEnabled` — `{ sessionId }`. |
| `INTERRUPT` | Stream `topic === 'interrupt'` — `{ sessionId }`. |
| `ERROR` | API / RTC / parse errors — `code`, `message`, optional `sessionId`. |
| `ALL` | Every event: `(eventName, data)`. |

On `ALL`, you may also see `FLOW_DEBUG`, `SIDE_INFO`, `STREAM_MESSAGE` as the first argument.

Typical `ERROR.code` values: `HTTP_*`, `NETWORK`, `RTC_ERROR`, `STREAM_PARSE`, `CHAT_ERROR`, `RTC_STOP`, `STOP_API`, `REMOTE_JOIN_TIMEOUT` (no remote user joined within **15s** after local RTC join), `REMOTE_REJOIN_TIMEOUT` (after the **last** remote left, no remote rejoined within **30s**).

---

## Demo (this repo)

```bash
cd packages/voice-realtime-sdk
pnpm install
pnpm demo
pnpm demo:build
pnpm demo:preview
```

See `demo/` (Vite + optional HTTPS in `demo/vite.config.js`).
