@voltkit-rtc/client 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ First release on npm, as `@voltkit-rtc/client`. Earlier builds were shared as
6
+ a tarball under another name; only the package name changed.
7
+
8
+ - **Chat.** `StreamClient` gains `sendMessage`, `getMessages`, `deleteMessage`,
9
+ `muteChat`, `setTyping` and `getChatState`, and the `message`,
10
+ `messageDeleted`, `chatMuted` and `typing` events.
11
+ - **`ChatClient`** for chat with no media, on its own connection.
12
+ - **`BroadcastPublisher`** for publishing a live broadcast over WHIP, H.264
13
+ only so the media server never has to transcode.
14
+ - **Music.** `publishMusic`, `setMusicVolume`, `setMusicEnabled` and `stopMusic`
15
+ publish music playing in the page as a track of its own; `setMicEnabled` no
16
+ longer touches it. `track` events carry `source`, and there is a new
17
+ `trackMuted` event. `BroadcastPublisher` mixes music into a broadcast with
18
+ `setMusic`, `setMusicEnabled`, `setMusicVolume` and `clearMusic`.
19
+ - Ships both ES module and CommonJS builds, each with its own type
20
+ declarations, so `import` and `require` work from JavaScript and TypeScript
21
+ under every module resolution mode.
22
+ - `usageLimit` event: the project ran out of minutes or hit a usage limit, and
23
+ the session will be ended at `endsAt`. A server-ended session (close code
24
+ 4003) is final and is not reconnected. Joins refused because the account is
25
+ out of minutes, over a limit or suspended stop reconnecting.
26
+ - `forceMute(identity, kind?, paused?)` for moderators, and the `forceMuted`
27
+ event carries `kind` and `paused`: only what a moderator switched off is
28
+ paused locally (an audio mute includes in-app music).
29
+ - A demoted publisher stops publishing and releases the camera and
30
+ microphone; the server closes a demoted publisher's tracks itself.
31
+ - `close()` emits `closed` with `'left'`.
32
+ - `JoinResult` includes the room's `metadata`, slot `claims` and
33
+ `blockedSlots`.
34
+
35
+ ## 0.1.0
36
+
37
+ Rooms, roles, moderation, simulcast publishing and reconnection. Not
38
+ published.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VoltKit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # @voltkit-rtc/client
2
+
3
+ Browser SDK for [VoltKit RTC](https://voltkit.in/rtc): realtime audio and video
4
+ rooms, chat, and live broadcast publishing.
5
+
6
+ | | |
7
+ | --- | --- |
8
+ | **Rooms** | `StreamClient` — join, publish camera and mic, receive everyone else, moderate |
9
+ | **Chat** | on the room's own connection, or `ChatClient` for chat with no media |
10
+ | **Broadcast** | `BroadcastPublisher` — go live to an audience of any size over WHIP |
11
+
12
+ Your backend mints a short-lived token with
13
+ [`@voltkit-rtc/server`](https://voltkit.in/docs/server-sdk) and
14
+ hands the page `{ token, wsUrl }`. This package turns that into a live session.
15
+ The API secret never reaches the browser.
16
+
17
+ ```bash
18
+ npm install @voltkit-rtc/client
19
+ ```
20
+
21
+ Works from JavaScript and TypeScript, with ES modules or CommonJS, and with any
22
+ bundler (Vite, webpack, Next.js, …). Types are included. Nothing here is tied to
23
+ a framework.
24
+
25
+ ```ts
26
+ import { StreamClient } from '@voltkit-rtc/client'; // ESM or TypeScript
27
+ // const { StreamClient } = require('@voltkit-rtc/client'); // CommonJS
28
+ ```
29
+
30
+ ## Rooms
31
+
32
+ ```ts
33
+ import { StreamClient } from '@voltkit-rtc/client';
34
+
35
+ const { token, wsUrl } = await fetch(`/api/shows/${id}/join`, { method: 'POST' }).then((r) => r.json());
36
+ const client = new StreamClient(wsUrl, token);
37
+
38
+ // Wire the events BEFORE connecting: connect() subscribes to everyone already
39
+ // publishing, and those tracks arrive during the call.
40
+ client.on('track', ({ identity, kind, track }) => attach(identity, kind, track));
41
+ client.on('trackEnded', ({ identity }) => detach(identity));
42
+ client.on('participantJoined', (p) => addToRoster(p));
43
+ client.on('participantLeft', (p) => removeFromRoster(p.identity));
44
+ client.on('streamInterrupted', () => showSpinner()); // the publisher is reconnecting; stay put
45
+ client.on('closed', (reason) => leaveScreen(reason)); // terminal; nothing follows
46
+
47
+ const joined = await client.connect();
48
+
49
+ if (joined.role === 'host' || joined.role === 'cohost') {
50
+ const media = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
51
+ await client.publish(media); // simulcast: 180p / 360p / 720p
52
+ }
53
+ ```
54
+
55
+ Always call `client.close()` when leaving. A client that is dropped without
56
+ closing keeps its reconnect loop running.
57
+
58
+ ### Publishing
59
+
60
+ ```ts
61
+ await client.publish(media);
62
+ await client.setMicEnabled(false);
63
+ await client.setCamEnabled(false);
64
+ await client.unpublish();
65
+
66
+ client.publishing; client.micEnabled; client.camEnabled; client.local; // current state
67
+ ```
68
+
69
+ A `viewer` cannot publish. That is enforced at the media server, not in this
70
+ package — a tampered client gets an error, not a stream.
71
+
72
+ ### Music
73
+
74
+ A host can play music to the room from inside the page. It does not go through
75
+ the microphone: it is taken straight from the element that is playing it and
76
+ published as a track of its own, so the audience hears it clean.
77
+
78
+ ```ts
79
+ const song = document.querySelector('audio')!; // <audio src="/song.mp3" crossorigin="anonymous">
80
+
81
+ await client.publishMusic(song, { volume: 0.8 }); // call it from a click
82
+ await song.play();
83
+
84
+ client.setMusicVolume(0.4); // for the audience
85
+ await client.setMusicEnabled(false); // music off, host still talking
86
+ await client.setMicEnabled(false); // mic off, music still playing
87
+ await client.stopMusic();
88
+ ```
89
+
90
+ `setMicEnabled` and `setMusicEnabled` are independent, in both directions.
91
+ `publishMusic` also takes a `MediaStream` or a `MediaStreamTrack`.
92
+
93
+ The host keeps hearing the music (`monitor`, on by default for an element). A
94
+ file from another origin must be served with CORS headers and loaded with
95
+ `crossorigin="anonymous"`, or the browser hands the page silence.
96
+
97
+ Viewers receive it as a second audio track, and `source` says which is which:
98
+
99
+ ```ts
100
+ client.on('track', ({ identity, kind, source, track }) => {
101
+ if (source === 'music') musicAudio.srcObject = new MediaStream([track]); // its own element = its own volume
102
+ else attach(identity, kind, track);
103
+ });
104
+ client.on('trackMuted', ({ source, paused }) => {
105
+ if (source === 'music') showMusicPaused(paused);
106
+ });
107
+
108
+ musicAudio.muted = true; // "mute the music" for this viewer only
109
+ ```
110
+
111
+ ### Moderation
112
+
113
+ ```ts
114
+ await client.raiseHand();
115
+ await client.lowerHand();
116
+ await client.muteParticipant(identity); // moderating roles: asks them to mute
117
+ await client.forceMute(identity, 'video'); // moderating roles: camera off, locked until released
118
+ await client.forceMute(identity, 'video', false); // release
119
+ await client.promote(identity, 'cohost'); // the promoted client reconnects by itself
120
+ await client.kick(identity, 'spam');
121
+ await client.endRoom(); // host only
122
+ await client.getRoomState();
123
+ ```
124
+
125
+ ### Events
126
+
127
+ | Event | Fires when |
128
+ | --- | --- |
129
+ | `track` / `trackEnded` | someone's audio or video is ready to render, or went away; `source` is `mic`, `music`, `camera` or `screen` |
130
+ | `trackMuted` | someone paused or resumed a track — muted their mic, switched their music off |
131
+ | `participantJoined` / `participantLeft` | roster changes |
132
+ | `roleChanged` | someone was promoted or demoted |
133
+ | `handRaised` | a hand went up or down |
134
+ | `forceMuted` | a moderator switched **your** mic or camera off |
135
+ | `streamInterrupted` | media stopped but the connection is fine — the publisher is reconnecting |
136
+ | `localMediaChanged` / `localStreamChanged` | your own mic, camera or stream changed |
137
+ | `connected` / `disconnected` | the socket came up or dropped; reconnection is automatic |
138
+ | `kicked` / `roomClosed` | you were removed, or the host ended the room |
139
+ | `error` | something failed that the SDK could not recover from by itself |
140
+ | `usageLimit` | your project ran out of minutes or hit a usage limit; the session ends at `endsAt` unless it is topped up |
141
+ | `closed` | terminal — `replaced`, `kicked`, `room-closed` or `gave-up`; your own `close()` does not fire it |
142
+
143
+ `on()` returns a function that removes the listener.
144
+
145
+ ### Reconnection
146
+
147
+ Handled for you. A dropped socket is retried with backoff, the session is
148
+ rejoined, and if you were publishing, your media is published again. `closed`
149
+ with `gave-up` means it stopped trying.
150
+
151
+ `replaced` means the same identity joined the same room somewhere else, and
152
+ this connection lost. Do not reconnect on it: the two would take turns evicting
153
+ each other.
154
+
155
+ ## Chat
156
+
157
+ Every room has a chat channel of the same name. `StreamClient` carries it on the
158
+ connection it already holds:
159
+
160
+ ```ts
161
+ client.on('message', (m) => append(m));
162
+ client.on('messageDeleted', ({ id }) => remove(id));
163
+ client.on('typing', ({ identity, typing }) => showTyping(identity, typing));
164
+ client.on('chatMuted', ({ identity, muted }) => lockComposer(identity, muted));
165
+
166
+ await client.sendMessage('hello everyone');
167
+ await client.sendMessage('psst', { to: 'user-42' }); // direct: only the two of you see it
168
+ await client.sendMessage('', { type: 'reaction', metadata: { emoji: 'heart' }, ephemeral: true }); // not stored
169
+
170
+ const older = await client.getMessages({ limit: 50, before: oldest.id }); // page backwards
171
+ await client.setTyping(true); // safe on every keystroke
172
+ const { members, muted } = await client.getChatState();
173
+ ```
174
+
175
+ Your own message comes back on `message` like everyone else's. Pass
176
+ `clientMsgId` to match it to one you rendered optimistically.
177
+
178
+ **`text` is delivered exactly as it was sent.** Put it in the page with
179
+ `textContent`, or let your framework escape it. Never with `innerHTML`.
180
+
181
+ The host and moderators can also do this (a co-host is refused with `forbidden`):
182
+
183
+ ```ts
184
+ await client.deleteMessage(messageId); // anyone's; everyone else, only their own
185
+ await client.muteChat('user-42'); // survives that user reconnecting
186
+ await client.muteChat('user-42', false);
187
+ ```
188
+
189
+ A muted sender is refused with the code `chat-muted`; a client sending too fast
190
+ with `rate-limited`. Show both to the user rather than retrying.
191
+
192
+ ### Chat without media
193
+
194
+ An audience watching a broadcast has no reason to join a room. `ChatClient`
195
+ opens a connection that carries chat and nothing else, into the same
196
+ conversation:
197
+
198
+ ```ts
199
+ import { ChatClient } from '@voltkit-rtc/client';
200
+
201
+ // { token, wsUrl } from your backend: createChatToken()
202
+ const chat = new ChatClient(wsUrl, token);
203
+ chat.on('message', append);
204
+ chat.on('closed', (reason) => showEnded(reason));
205
+
206
+ const joined = await chat.connect(); // joined.messages: recent history, oldest first
207
+ await chat.sendMessage('first!');
208
+ // ...
209
+ chat.close();
210
+ ```
211
+
212
+ It has the same methods and events as the chat half of `StreamClient`.
213
+
214
+ ## Broadcast
215
+
216
+ A broadcast is one publisher and an audience of any size, served as LL-HLS
217
+ through a CDN. Use it when the audience watches and does not talk back; use a
218
+ room when they do.
219
+
220
+ ```ts
221
+ import { BroadcastPublisher } from '@voltkit-rtc/client';
222
+
223
+ // publish.whipUrl from your backend: createStream()
224
+ const publisher = new BroadcastPublisher(whipUrl);
225
+ publisher.on('state', (s) => setState(s)); // connecting, live, interrupted, failed, stopped
226
+ publisher.on('warning', console.warn);
227
+
228
+ preview.srcObject = await publisher.start(); // camera + mic; or start({ stream: yours })
229
+
230
+ publisher.setMicEnabled(false);
231
+ publisher.setCamEnabled(false);
232
+ await publisher.stop(); // off air, camera released
233
+ ```
234
+
235
+ The `whipUrl` carries the publish token. Treat it like a password: fetch it when
236
+ the user clicks "go live", and keep it out of logs and analytics.
237
+
238
+ `start()` rejects with `WhipError` if the media server refuses. `isRefused`
239
+ means the URL itself was not accepted — expired, for another stream, or the
240
+ account is out of minutes — and a retry with the same URL is refused again.
241
+
242
+ Video is offered as **H.264 only**. The media server repackages H.264 into HLS
243
+ untouched; any other codec makes it transcode every frame. A browser with no
244
+ H.264 encoder still publishes, and `warning` says so.
245
+
246
+ To publish something other than the camera — a screen share, a canvas — pass
247
+ your own stream: `publisher.start({ stream })`. It stays yours; `stop()` does
248
+ not end its tracks.
249
+
250
+ ### Music on a broadcast
251
+
252
+ A broadcast carries one audio track, so music is mixed with the microphone in
253
+ the browser before it is sent. It still never passes through the microphone.
254
+
255
+ ```ts
256
+ await publisher.setMusic(song, { volume: 0.8 }); // while live, or start({ music: song })
257
+ publisher.setMusicVolume(0.4);
258
+ publisher.setMusicEnabled(false); // music off, mic untouched
259
+ publisher.setMicEnabled(false); // mic off, music keeps playing
260
+ publisher.clearMusic();
261
+ ```
262
+
263
+ Adding music to a broadcast that is already live does not interrupt it.
264
+
265
+ ### Watching
266
+
267
+ Viewers need nothing from this package. Safari plays the `llhls` URL natively;
268
+ everywhere else, use the `hls.js` package:
269
+
270
+ ```ts
271
+ import Hls from 'hls.js';
272
+
273
+ if (video.canPlayType('application/vnd.apple.mpegurl')) {
274
+ video.src = llhlsUrl;
275
+ } else {
276
+ const hls = new Hls({ lowLatencyMode: true });
277
+ hls.loadSource(llhlsUrl);
278
+ hls.attachMedia(video);
279
+ }
280
+ ```
281
+
282
+ ## Errors
283
+
284
+ Server refusals reject with `RpcError` (rooms) or `ChatRpcError` (`ChatClient`),
285
+ both carrying a machine-readable `code`: `forbidden`, `bad-params`,
286
+ `chat-muted`, `rate-limited`, `no-message`, and so on. The full list is in the
287
+ [API reference](https://voltkit.in/docs/api-reference).
288
+
289
+ ## Help
290
+
291
+ Documentation: https://voltkit.in/docs. Questions and bug reports:
292
+ contact@voltkit.in.
293
+
294
+ ## Licence
295
+
296
+ MIT
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Publishing a broadcast stream.
3
+ *
4
+ * A broadcast is one publisher and an audience of any size, delivered over
5
+ * LL-HLS and WebRTC. Publishing is WHIP: one HTTP exchange of SDP, then media
6
+ * straight to the media server. Viewers need nothing from this package -- they
7
+ * play the stream's LL-HLS URL in any HLS player.
8
+ */
9
+ import { type MusicInput, type MusicOptions } from './music.js';
10
+ /** The media server refused, or could not be reached. */
11
+ export declare class WhipError extends Error {
12
+ readonly status: number;
13
+ /**
14
+ * @param status HTTP status, or 0 when the request never got an answer.
15
+ */
16
+ constructor(status: number, message: string);
17
+ /**
18
+ * The publish URL was not accepted: its token is missing, expired, for a
19
+ * different stream, or the account behind it cannot go on air. The same URL
20
+ * will be refused again -- get a new one from your backend.
21
+ */
22
+ get isRefused(): boolean;
23
+ }
24
+ export type BroadcastState = 'idle' | 'connecting'
25
+ /** Media is flowing to the media server. */
26
+ | 'live'
27
+ /** The connection dropped. WebRTC may recover it; if it reaches `failed`, start again. */
28
+ | 'interrupted' | 'failed' | 'stopped';
29
+ export interface BroadcastEvents {
30
+ state: (state: BroadcastState) => void;
31
+ /**
32
+ * Publishing worked, but something that affects cost or quality did not
33
+ * apply -- most importantly, H.264 was not available in this browser.
34
+ */
35
+ warning: (message: string) => void;
36
+ }
37
+ export interface BroadcastStartOptions {
38
+ /** Media you already hold. It stays yours: `stop()` will not end its tracks. */
39
+ stream?: MediaStream;
40
+ /** Used to acquire camera and microphone when `stream` is not given. */
41
+ constraints?: MediaStreamConstraints;
42
+ /** Music playing inside the page, to go on air from the first second. See `setMusic`. */
43
+ music?: MusicInput;
44
+ musicOptions?: MusicOptions;
45
+ }
46
+ /**
47
+ * Send an SDP offer to a WHIP endpoint and return the answer, plus the session
48
+ * resource the server names for ending it.
49
+ */
50
+ export declare function whipOffer(whipUrl: string, offerSdp: string, timeoutMs?: number): Promise<{
51
+ answerSdp: string;
52
+ resource: string | null;
53
+ }>;
54
+ /**
55
+ * Tell the server the session is over. Best effort: the media server also
56
+ * notices the connection going away, so a failure here is not an error.
57
+ */
58
+ export declare function whipEnd(resource: string): Promise<void>;
59
+ /**
60
+ * Publishes camera and microphone to one broadcast stream.
61
+ *
62
+ * ```ts
63
+ * const publisher = new BroadcastPublisher(stream.publish.whipUrl);
64
+ * publisher.on('state', (s) => setState(s));
65
+ * const media = await publisher.start();
66
+ * video.srcObject = media;
67
+ * // ...
68
+ * await publisher.stop();
69
+ * ```
70
+ *
71
+ * The `whipUrl` comes from your backend (`POST /v1/streams`) and already
72
+ * carries the publish token. Treat it like a password.
73
+ */
74
+ export declare class BroadcastPublisher {
75
+ private readonly whipUrl;
76
+ private pc;
77
+ private media;
78
+ private ownsMedia;
79
+ private resource;
80
+ private audioSender;
81
+ private micTrack;
82
+ /** Present only while music is in use: the broadcast carries one audio track, so the two are mixed. */
83
+ private mixer;
84
+ private micOn;
85
+ private current;
86
+ private readonly listeners;
87
+ constructor(whipUrl: string);
88
+ get state(): BroadcastState;
89
+ /** What is being published, for a local preview. */
90
+ get localStream(): MediaStream | null;
91
+ on<K extends keyof BroadcastEvents>(event: K, fn: BroadcastEvents[K]): () => void;
92
+ private emit;
93
+ private set;
94
+ /** Go on air. Resolves once the media server has accepted the stream. */
95
+ start(options?: BroadcastStartOptions): Promise<MediaStream>;
96
+ /**
97
+ * Put H.264 first, and offer nothing else for video.
98
+ *
99
+ * The media server repackages H.264 into HLS untouched. Any other codec
100
+ * makes it decode and re-encode every frame, which is the entire cost of a
101
+ * stream -- so a publisher that negotiates VP8 works, and quietly costs many
102
+ * times more.
103
+ */
104
+ private preferH264;
105
+ /** Mute or unmute the microphone without leaving the air. Music keeps playing. */
106
+ setMicEnabled(enabled: boolean): void;
107
+ /**
108
+ * Put music from inside the page on air, mixed with the microphone.
109
+ *
110
+ * A broadcast carries a single audio track, so the two are mixed here, in
111
+ * the browser, before they are sent. The music never passes through the
112
+ * microphone, so the audience hears it clean, and each has its own switch:
113
+ * `setMusicEnabled` and `setMicEnabled`.
114
+ */
115
+ setMusic(input: MusicInput, options?: MusicOptions): Promise<void>;
116
+ /** Switch the music off or on for the audience. The microphone is untouched. */
117
+ setMusicEnabled(enabled: boolean): void;
118
+ /** 0 to 1, for the audience. */
119
+ setMusicVolume(volume: number): void;
120
+ /** Take the music off air. The microphone carries on. */
121
+ clearMusic(): void;
122
+ get musicEnabled(): boolean;
123
+ get micEnabled(): boolean;
124
+ /** Turn the camera on or off without leaving the air. */
125
+ setCamEnabled(enabled: boolean): void;
126
+ /** Go off air and release the camera and microphone. Idempotent. */
127
+ stop(): Promise<void>;
128
+ private teardown;
129
+ }
130
+ //# sourceMappingURL=broadcast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/broadcast.ts"],"names":[],"mappings":"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"}