@strevokit/meeting-sdk 0.0.0-stage → 0.8.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.
@@ -0,0 +1,361 @@
1
+ # API reference
2
+
3
+ Everything `@strevokit/meeting-sdk` exports is listed here. Anything not listed is internal and cannot be imported
4
+ (deep imports such as `@strevokit/meeting-sdk/dist/index.js` are blocked by the package's `exports` map).
5
+
6
+ ```ts
7
+ import { createMeetingEngine, MeetingError } from "@strevokit/meeting-sdk";
8
+ import type { MeetingEngine, MeetingEngineOptions /* ... */ } from "@strevokit/meeting-sdk";
9
+ ```
10
+
11
+ ## `createMeetingEngine(options)`
12
+
13
+ ```ts
14
+ function createMeetingEngine(options: MeetingEngineOptions): MeetingEngine;
15
+ ```
16
+
17
+ Creates an engine in the `idle` state. Nothing connects until you call `join()`.
18
+
19
+ Throws synchronously:
20
+
21
+ - `MeetingError` with code `join_token_invalid` when `token` is empty or is not a well-formed join token.
22
+ - `MeetingError` with code `unsupported_environment` when the browser has no `WebSocket`.
23
+ - `TypeError` when `realtimeUrl` or `joinTimeoutMs` is malformed. This is a programming error, not a runtime condition.
24
+
25
+ ## `MeetingEngineOptions`
26
+
27
+ | Option | Type | Required | Description |
28
+ |---|---|---|---|
29
+ | `token` | `string` | yes | Join token minted by your backend. Kept in memory only. |
30
+ | `realtimeUrl` | `string` | yes | Realtime endpoint base URL, `wss://host[:port]`. `ws://` is accepted only for `localhost` and `127.0.0.1`. No path, query, fragment, or credentials. |
31
+ | `joinTimeoutMs` | `number` | no | How long `join()` waits for admission before rejecting with `join_timeout`. Integer, 1000–60000. Default 15000. |
32
+ | `logger` | `MeetingLogger` | no | Diagnostics sink. Receives short messages and allow-listed fields only. |
33
+ | `getJoinToken` | `() => Promise<string>` | no | Called when automatic recovery needs a fresh join token and the in-memory token cannot be used. Resolve with a token your backend issued for the same participant; reject to end recovery. Kept in memory only and never logged. |
34
+ | `reconnectWindowMs` | `number` | no | Continuous unsuccessful recovery budget in milliseconds. Default `120000`; values outside `1000`–`600000` are clamped. |
35
+
36
+ ### `MeetingLogger`
37
+
38
+ ```ts
39
+ interface MeetingLogger {
40
+ debug?(message: string, fields?: Readonly<Record<string, string | number | boolean>>): void;
41
+ info?(message: string, fields?: Readonly<Record<string, string | number | boolean>>): void;
42
+ warn?(message: string, fields?: Readonly<Record<string, string | number | boolean>>): void;
43
+ error?(message: string, fields?: Readonly<Record<string, string | number | boolean>>): void;
44
+ }
45
+ ```
46
+
47
+ Every method is optional. The SDK never passes the join token, connection negotiation details, or raw browser
48
+ exception text to the logger. A logger that throws is ignored.
49
+
50
+ ## `MeetingEngine`
51
+
52
+ ```ts
53
+ interface MeetingEngine {
54
+ readonly state: ConnectionState;
55
+ readonly self: SelfParticipant | null;
56
+ readonly participants: readonly Participant[];
57
+ readonly localMediaState: MediaState;
58
+ readonly localMedia: LocalMedia;
59
+
60
+ remoteMedia(participantId: string): RemoteMedia;
61
+ mediaConnectionStatus(participantId: string): MediaConnectionStatus | null;
62
+ join(): Promise<void>;
63
+ leave(): Promise<void>;
64
+ endMeeting(): Promise<void>;
65
+ enableCamera(options?: MediaDeviceOptions): Promise<void>;
66
+ disableCamera(): Promise<void>;
67
+ mute(): Promise<void>;
68
+ unmute(options?: MediaDeviceOptions): Promise<void>;
69
+ startScreenShare(): Promise<void>;
70
+ stopScreenShare(): Promise<void>;
71
+ on<K extends MeetingEventType>(type: K, listener: MeetingEventListener<K>): () => void;
72
+ dispose(): void;
73
+ }
74
+ ```
75
+
76
+ ### Properties
77
+
78
+ | Property | Description |
79
+ |---|---|
80
+ | `state` | The current `ConnectionState`. |
81
+ | `self` | This participant, set when `joined` fires; `null` before. |
82
+ | `participants` | Other participants currently present. Never includes `self`. Empty until `joined`. |
83
+ | `localMediaState` | Which local captures are on (`MediaState`). |
84
+ | `localMedia` | The local `MediaStream` for each capture, or `null` when it is off (`LocalMedia`). Use it for a self-preview. |
85
+
86
+ ### Methods
87
+
88
+ | Method | Returns |
89
+ |---|---|
90
+ | `remoteMedia(participantId)` | The media currently received from that participant (`RemoteMedia`). Every field is `null` when nothing is available, including for unknown participants and before `joined`. |
91
+ | `mediaConnectionStatus(participantId)` | The status of the audio and video connection to that participant (`MediaConnectionStatus`), or `null` when there is none. |
92
+
93
+ Both are synchronous reads that never throw. They return the same values the `remote_media_changed` and
94
+ `media_connection_changed` events last reported.
95
+
96
+ | Method | Allowed in | Resolves when | Rejects with |
97
+ |---|---|---|---|
98
+ | `join()` | `idle` | Admitted to the meeting and the participant list is received. | `join_token_invalid`, `join_token_expired`, `origin_not_allowed`, `connection_failed`, `join_timeout`, `rate_limited`, `service_unavailable`, `session_replaced`, `meeting_ended`, `not_permitted`, `protocol_error`, `join_cancelled`, `unsupported_environment`, `already_joined` (in `connecting`, `joining`, `joined`, `leaving`), `engine_closed` (in `closed`) |
99
+ | `leave()` | any state | In `idle`/`closed`: immediately. In `leaving`: with the in-flight leave. In `connecting`/`joining`: the join is cancelled (`join()` rejects with `join_cancelled`). In `joined`: when the platform confirms, or after 5 seconds. | never |
100
+ | `endMeeting()` | `joined` | The meeting is ended and every connection is closed. | `not_joined`, `not_permitted`, `service_unavailable`, `rate_limited`, `engine_closed` |
101
+ | `enableCamera(options?)` / `disableCamera()` | `joined` | The camera is on and announced / off and announced. With `options.deviceId` while the camera is on: switched to that camera. | `not_joined`, `permission_denied`, `device_not_found`, `device_unavailable`, `unsupported_environment` |
102
+ | `unmute(options?)` / `mute()` | `joined` | The microphone is live and announced / silenced and announced. With `options.deviceId` while unmuted: switched to that microphone. | same as the camera |
103
+ | `startScreenShare()` / `stopScreenShare()` | `joined` | Screen sharing is on and announced / off and announced. | `not_joined`, `not_permitted` (checked before the browser prompt), `unsupported_environment` (when this meeting cannot deliver screen media, also before the prompt), `screen_share_cancelled`, `permission_denied`, `device_unavailable` |
104
+ | `on(type, listener)` | any state | Returns an unsubscribe function. | never |
105
+ | `dispose()` | any state | Releases devices, the connection, and listeners immediately. Emits nothing. Idempotent. A pending `join()` rejects with `join_cancelled`. | never |
106
+
107
+ Rules for every media method:
108
+
109
+ 1. **Idempotent.** Asking for the state you are already in resolves immediately with no device prompt.
110
+ 2. **Ordered per kind.** Calls for the same kind run in call order. A call for the state a pending call will reach
111
+ shares that pending promise.
112
+ 3. **Failure changes nothing.** On rejection, `localMediaState`, `localMedia`, and what other participants see are
113
+ unchanged.
114
+ 4. **Local change first.** On success, `local_media_changed` fires before the promise resolves.
115
+ 5. **Browser stop button.** Stopping a screen share from the browser's own control behaves like `stopScreenShare()`.
116
+ 6. **Leave releases everything.** `leave()` and `dispose()` stop every capture.
117
+
118
+ Muting keeps the microphone open and silences it, so unmuting is instant. Turning the camera off or stopping a screen
119
+ share releases the device.
120
+
121
+ ### Choosing a camera or microphone
122
+
123
+ Pass a `deviceId` from `navigator.mediaDevices.enumerateDevices()` (`videoinput` for the camera, `audioinput` for the
124
+ microphone) to `enableCamera()` or `unmute()`. Without one, the browser's default device is used, exactly as before.
125
+
126
+ | State before | Call | Result |
127
+ |---|---|---|
128
+ | Off | `enableCamera({ deviceId })` | That camera turns on and the change is announced. |
129
+ | On with the same device, or no `deviceId` given | `enableCamera(...)` | Nothing happens. |
130
+ | On with another device | `enableCamera({ deviceId })` | **Switches in place**: other participants keep seeing video, nothing is announced, and `local_media_changed` fires with the new stream. The previous device is released. |
131
+ | Muted, with another device kept open | `unmute({ deviceId })` | The chosen microphone is captured and announced; the previous one is released. |
132
+ | Unmuted with another device | `unmute({ deviceId })` | Switches in place, as for the camera. |
133
+
134
+ If the camera or an unmuted microphone ends on its own, and another device of that kind is listed, capture continues
135
+ on that device. `local_media_changed` fires with the kind still on and the new stream. The change is not announced as
136
+ stopped or started. When no other device can be opened, the kind turns off and is announced. A muted microphone that
137
+ ends is not replaced and is not unmuted. Calls made while the engine is `reconnecting` reject with `not_joined` and
138
+ do not capture.
139
+
140
+ If the chosen device is missing, the call rejects with `device_not_found`; if another app is using it, with
141
+ `device_unavailable`. Either way, the device already in use keeps running. A pending call is shared only by calls for
142
+ the same state **and** the same device. Device IDs are never logged. Read the device in use from the track:
143
+ `localMedia.camera?.getVideoTracks()[0]?.getSettings().deviceId`.
144
+
145
+ ## States: `ConnectionState`
146
+
147
+ ```ts
148
+ type ConnectionState =
149
+ | "idle"
150
+ | "connecting"
151
+ | "joining"
152
+ | "joined"
153
+ | "reconnecting"
154
+ | "leaving"
155
+ | "closed";
156
+ ```
157
+
158
+ | State | Meaning |
159
+ |---|---|
160
+ | `idle` | Created; `join()` not called yet. |
161
+ | `connecting` | Connecting and presenting the join token. |
162
+ | `joining` | The token was accepted; waiting for the participant list. |
163
+ | `joined` | In the meeting. Media methods are available. |
164
+ | `reconnecting` | Previously joined; automatic recovery of the meeting-service connection is in progress. Success returns to `joined` (same self identity; `joined` is not re-emitted). |
165
+ | `leaving` | `leave()` was called; waiting for the platform to confirm. |
166
+ | `closed` | Finished. The engine cannot be reused; create a new one with a new token. |
167
+
168
+ ## Events
169
+
170
+ Subscribe with `engine.on(type, listener)`. `state_changed` fires on every transition, before the event that caused
171
+ it (for example `state_changed` to `joined`, then `joined`). Events are delivered in the order the platform produced
172
+ them. A listener that throws does not affect other listeners or the engine.
173
+
174
+ ### `MeetingEventMap`
175
+
176
+ | Event | Payload | When |
177
+ |---|---|---|
178
+ | `state_changed` | `{ state: ConnectionState; previous: ConnectionState }` | Every state transition. |
179
+ | `joined` | `{ self: SelfParticipant; participants: readonly Participant[] }` | Admitted. `participants` holds everyone already present. |
180
+ | `participant_joined` | `{ participant: Participant }` | Someone joined after you. Never fires twice for someone already present. |
181
+ | `participant_left` | `{ participantId: string; reason: ParticipantLeftReason }` | Someone left. |
182
+ | `participant_reconnected` | `{ participantId: string }` | Someone's connection was replaced (for example, they rejoined from another tab). |
183
+ | `participant_media_changed` | `{ participantId: string; kind: MediaKind; enabled: boolean }` | Someone turned a capture on or off. Fires only for actual changes. |
184
+ | `local_media_changed` | `{ kind: MediaKind; enabled: boolean; stream: MediaStream \| null }` | Your capture turned on or off. |
185
+ | `remote_media_changed` | `{ participantId: string; kind: MediaKind; stream: MediaStream \| null }` | Media from another participant became available (`stream` set) or stopped being available (`stream` is `null`). |
186
+ | `media_connection_changed` | `{ participantId: string; status: MediaConnectionStatus \| null }` | The audio and video connection to another participant changed status. `null` means there is no connection to them any more. |
187
+ | `error` | `{ error: MeetingError }` | A problem that does not end the meeting (for example, a rejected action). |
188
+ | `closed` | `{ reason: CloseReason; error?: MeetingError }` | The engine finished. Fires once. Not fired by `dispose()`. |
189
+
190
+ **The event set is open.** New event types may be added in minor versions. Do not assume `MeetingEventMap` is closed
191
+ (for example, with an exhaustive `switch` that fails on unknown types). `on()` delivers only the types you subscribe
192
+ to, so new types never reach existing code.
193
+
194
+ ### Remote media guarantees
195
+
196
+ 1. **One event per change.** `remote_media_changed` fires exactly once each time `remoteMedia(id)[kind]` changes. For
197
+ one participant and kind, the `MediaStream` object stays the same while the connection lasts, so you can attach it
198
+ to an element once.
199
+ 2. **Clean departure.** `media_connection_changed` fires once per status change. When a participant leaves, you get a
200
+ `remote_media_changed` with `null` for each kind that was available, then a `media_connection_changed` with `null`
201
+ if they had a status, before `participant_left`.
202
+ 3. **Some meetings have no remote media yet.** In meetings whose media the platform delivers differently, this version
203
+ reports presence only: `remoteMedia()` is all `null`, `mediaConnectionStatus()` is `null`, and neither event fires.
204
+ Your code needs no change when a later version adds media there.
205
+ 4. **Device controls never change the connection status.** Turning your camera or microphone on or off never fires
206
+ `media_connection_changed`.
207
+ 5. **Leaving releases everything.** `leave()` and `dispose()` close every media connection and stop every received
208
+ track. After `leave()`, the matching `null` events fire before `closed`. After `dispose()`, nothing fires.
209
+ 6. **No connection details.** No event, error, or logger call contains connection negotiation details, network
210
+ addresses, or server credentials.
211
+
212
+ ```ts
213
+ engine.on("remote_media_changed", ({ participantId, kind, stream }) => {
214
+ if (kind === "camera") videoFor(participantId).srcObject = stream;
215
+ if (kind === "microphone") audioFor(participantId).srcObject = stream;
216
+ });
217
+ engine.on("media_connection_changed", ({ participantId, status }) => {
218
+ showBadge(participantId, status === "interrupted" ? "Connection lost — reconnecting…" : status);
219
+ });
220
+ ```
221
+
222
+ ### `MeetingEventType`
223
+
224
+ `keyof MeetingEventMap`: the union of event names above.
225
+
226
+ ### `MeetingEventListener`
227
+
228
+ ```ts
229
+ type MeetingEventListener<K extends MeetingEventType> = (event: MeetingEventMap[K]) => void;
230
+ ```
231
+
232
+ ## Data types
233
+
234
+ ### `Participant`
235
+
236
+ | Field | Type | Description |
237
+ |---|---|---|
238
+ | `id` | `string` | Opaque participant public ID. |
239
+ | `role` | `ParticipantRole` | Role assigned by your backend when the token was minted. |
240
+ | `displayName` | `string` (optional) | Display name, when the platform knows one. |
241
+ | `media` | `MediaState` | Which captures this participant has on. |
242
+
243
+ ### `SelfParticipant`
244
+
245
+ | Field | Type | Description |
246
+ |---|---|---|
247
+ | `id` | `string` | Your opaque participant public ID. |
248
+ | `role` | `ParticipantRole` | Your role. |
249
+ | `displayName` | `string` (optional) | Your display name, when known. |
250
+ | `joinedAt` | `string` | ISO-8601 UTC time you were admitted. |
251
+
252
+ ### `ParticipantRole`
253
+
254
+ `"host" | "participant"`. Only hosts may share their screen in this version.
255
+
256
+ ### `MediaKind`
257
+
258
+ `"camera" | "microphone" | "screen"`.
259
+
260
+ ### `MediaState`
261
+
262
+ `{ camera: boolean; microphone: boolean; screen: boolean }`. `true` means on: camera on, microphone unmuted, screen
263
+ being shared.
264
+
265
+ ### `LocalMedia`
266
+
267
+ `{ camera: MediaStream | null; microphone: MediaStream | null; screen: MediaStream | null }`. `null` when that capture
268
+ is off.
269
+
270
+ ### `MediaDeviceOptions`
271
+
272
+ `{ deviceId?: string }`. Which camera or microphone `enableCamera()` or `unmute()` should use. Omit `deviceId` for the
273
+ browser's default device. See [Choosing a camera or microphone](#choosing-a-camera-or-microphone).
274
+
275
+ ### `RemoteMedia`
276
+
277
+ `{ camera: MediaStream | null; microphone: MediaStream | null; screen: MediaStream | null }`. The media received from
278
+ another participant, for a `<video>` (camera or screen) or `<audio>` (microphone) element. `null` when that media is
279
+ not available. `screen` is a live display stream while the remote participant is sharing and peer media has attached
280
+ it; otherwise `null`.
281
+
282
+ ### `MediaConnectionStatus`
283
+
284
+ | Status | Meaning |
285
+ |---|---|
286
+ | `connecting` | Setting up audio and video with this participant. |
287
+ | `connected` | Audio and video are flowing. |
288
+ | `interrupted` | The connection dropped after working; it may recover on its own. |
289
+ | `failed` | The connection could not be set up or did not recover. An `error` event with `media_connection_failed` accompanies it. Rejoining starts a new attempt. |
290
+
291
+ While a peer link is alive, the SDK may periodically report aggregated media-health samples (RTT, jitter, loss, bitrates, and path/connectivity indicators) to the realtime service for operator diagnostics. Sampling failures never tear down media.
292
+
293
+ ### `ParticipantLeftReason`
294
+
295
+ | Reason | Meaning |
296
+ |---|---|
297
+ | `left` | They chose to leave. |
298
+ | `disconnected` | Their connection dropped. |
299
+ | `removed` | They were removed from the meeting. |
300
+ | `meeting_ended` | The meeting ended. |
301
+ | `other` | Any other reason. |
302
+
303
+ ## Errors
304
+
305
+ ### `MeetingError`
306
+
307
+ ```ts
308
+ class MeetingError extends Error {
309
+ readonly name: "MeetingError";
310
+ readonly code: MeetingErrorCode;
311
+ readonly retryable: boolean;
312
+ }
313
+ ```
314
+
315
+ `message` is a short English sentence that is safe to show to users. It never contains browser exception text, stack
316
+ traces, or connection details. `retryable` is `true` when trying the same action again later may succeed.
317
+
318
+ ### `MeetingErrorCode`
319
+
320
+ New codes may be added in minor versions; handle unknown codes generically (for example, show `message`).
321
+
322
+ | Code | Retryable | Default message | Raised by |
323
+ |---|---|---|---|
324
+ | `join_token_invalid` | no | This meeting link is not valid. Ask for a new one. | `createMeetingEngine()`, `join()` |
325
+ | `join_token_expired` | yes\* | This meeting link has expired. Ask for a new one. | `join()` |
326
+ | `origin_not_allowed` | no | This site is not allowed to join this meeting. | `join()` |
327
+ | `connection_failed` | yes | Could not connect to the meeting. Check your connection and try again. | `join()` |
328
+ | `join_timeout` | yes | Joining the meeting took too long. Please try again. | `join()` |
329
+ | `join_cancelled` | no | Joining was cancelled. | `join()` when `leave()` or `dispose()` is called during the join |
330
+ | `connection_lost` | yes | The connection to the meeting was lost. | `closed` after joining |
331
+ | `session_replaced` | no | You joined this meeting from another tab or device. | `join()`, `closed` |
332
+ | `service_unavailable` | yes | The meeting service is temporarily unavailable. Please try again shortly. | `join()`, `closed`, `error` |
333
+ | `rate_limited` | yes | Too many attempts. Please wait a moment and try again. | `join()`, `closed`, `error` |
334
+ | `meeting_ended` | no | This meeting has ended. | `join()` |
335
+ | `protocol_error` | no | Something went wrong in the meeting connection. | `join()`, `error`, `closed` |
336
+ | `already_joined` | no | This meeting is already joined or joining. | `join()` |
337
+ | `engine_closed` | no | This meeting session has ended. Create a new one to join again. | `join()` |
338
+ | `not_joined` | no | Join the meeting first. | media methods |
339
+ | `not_permitted` | no | You don't have permission to do that in this meeting. | `join()`, `startScreenShare()`, `error` |
340
+ | `permission_denied` | no | Camera or microphone access was blocked. Allow access in your browser settings. | media methods |
341
+ | `device_not_found` | no | No camera or microphone was found. | media methods |
342
+ | `device_unavailable` | yes | Your camera or microphone is being used by another application. | media methods |
343
+ | `screen_share_cancelled` | no | Screen sharing was cancelled. | `startScreenShare()` |
344
+ | `unsupported_environment` | no | This browser does not support meetings. Try a current version of Chrome, Edge, Firefox, or Safari. | `createMeetingEngine()`, `join()`, media methods, `error` |
345
+ | `media_connection_failed` | yes | Audio and video couldn't connect to the other participant. Check your network and try rejoining. | `error` |
346
+ | `media_participant_limit` | no | Audio and video are available with one other participant in this meeting. | `error`, once for each participant whose media this meeting cannot carry |
347
+
348
+ \* Retry only with a new token from your backend.
349
+
350
+ ### Close reasons: `CloseReason`
351
+
352
+ `closed.reason` tells you why the engine finished.
353
+
354
+ | Reason | When | `closed.error` |
355
+ |---|---|---|
356
+ | `left` | After your `leave()`. | none |
357
+ | `meeting_ended` | The meeting ended while you were in it. | none |
358
+ | `session_replaced` | You joined the same meeting from another tab or device. | `session_replaced` |
359
+ | `connection_lost` | The connection dropped after joining. | `connection_lost` |
360
+ | `join_failed` | Any failure before joining completed. `join()` rejected with the same error. | always set |
361
+ | `error` | The platform closed the connection after joining for another reason (for example `service_unavailable` or `rate_limited`). | the matching code |