@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 +38 -0
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/cjs/broadcast.d.ts +130 -0
- package/dist/cjs/broadcast.d.ts.map +1 -0
- package/dist/cjs/broadcast.js +320 -0
- package/dist/cjs/broadcast.js.map +1 -0
- package/dist/cjs/chat.d.ts +138 -0
- package/dist/cjs/chat.d.ts.map +1 -0
- package/dist/cjs/chat.js +267 -0
- package/dist/cjs/chat.js.map +1 -0
- package/dist/cjs/index.d.ts +285 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +798 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/music.d.ts +76 -0
- package/dist/cjs/music.d.ts.map +1 -0
- package/dist/cjs/music.js +176 -0
- package/dist/cjs/music.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/broadcast.d.ts +130 -0
- package/dist/esm/broadcast.d.ts.map +1 -0
- package/dist/esm/broadcast.js +313 -0
- package/dist/esm/broadcast.js.map +1 -0
- package/dist/esm/chat.d.ts +138 -0
- package/dist/esm/chat.d.ts.map +1 -0
- package/dist/esm/chat.js +262 -0
- package/dist/esm/chat.js.map +1 -0
- package/dist/esm/index.d.ts +285 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +785 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/music.d.ts +76 -0
- package/dist/esm/music.d.ts.map +1 -0
- package/dist/esm/music.js +171 -0
- package/dist/esm/music.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/package.json +60 -0
- package/src/broadcast.ts +352 -0
- package/src/chat.ts +352 -0
- package/src/index.ts +945 -0
- package/src/music.ts +200 -0
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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"}
|