@ravenkash/rtc 0.1.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,622 @@
1
+ import { EffectsPipeline } from '@ravenkash/effects';
2
+
3
+ type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
4
+ interface Logger {
5
+ error(...args: unknown[]): void;
6
+ warn(...args: unknown[]): void;
7
+ info(...args: unknown[]): void;
8
+ debug(...args: unknown[]): void;
9
+ }
10
+
11
+ interface RTCClientConfig {
12
+ /** The RTC token your backend minted through Raven's Control API. Never mint one in the browser. */
13
+ token: string;
14
+ /**
15
+ * RTC infrastructure URL to connect to. It's the `endpoint` field out of
16
+ * the same mint response as `token`. Forward both through untouched;
17
+ * don't build this by hand.
18
+ */
19
+ endpoint: string;
20
+ /**
21
+ * The `iceServers` array from the same mint response. Optional so tests
22
+ * and advanced setups can skip it, but ordinarily you just forward it.
23
+ * Don't go configuring STUN/TURN by hand.
24
+ */
25
+ iceServers?: RTCIceServer[];
26
+ logLevel?: LogLevel;
27
+ /** Defaults to true. Set false to disable automatic reconnect on network loss. */
28
+ autoReconnect?: boolean;
29
+ /**
30
+ * Base URL for best-effort connection telemetry. Comes from the
31
+ * `telemetryUrl` field of the same token-mint response as `token` and
32
+ * `endpoint`; never build it yourself. Leave it out, or set
33
+ * `telemetry: false`, to turn telemetry off completely. RTC never
34
+ * depends on it either way. See docs/telemetry.md.
35
+ */
36
+ telemetryUrl?: string;
37
+ /** Defaults to true. Set false to switch telemetry off; RTC never needs it (Phase 9 spec §31). */
38
+ telemetry?: boolean;
39
+ }
40
+ interface ResolvedRTCClientConfig {
41
+ token: string;
42
+ endpoint: string;
43
+ iceServers?: RTCIceServer[];
44
+ logLevel: LogLevel;
45
+ autoReconnect: boolean;
46
+ telemetryUrl?: string;
47
+ telemetry: boolean;
48
+ }
49
+
50
+ /**
51
+ * One flattened WebRTC stats sample.
52
+ *
53
+ * Duck-typed instead of bound to any particular source, which is how this
54
+ * file survived the move off LiveKit's stats objects to reading an
55
+ * `RTCStatsReport` directly (see `internal/telemetry/rtc-stats.ts`). Every
56
+ * field is a raw WebRTC stat in the units the spec uses, so seconds, not
57
+ * milliseconds. `normalizeTrackStats` below translates into Raven's own
58
+ * vocabulary.
59
+ */
60
+ interface RawTrackStats {
61
+ type?: 'audio' | 'video';
62
+ /** Epoch ms: when this sample was taken. Bitrate is a delta against a previous one. */
63
+ timestamp: number;
64
+ /** Seconds, per the WebRTC spec. */
65
+ jitter?: number;
66
+ packetsLost?: number;
67
+ packetsSent?: number;
68
+ packetsReceived?: number;
69
+ /** Seconds. Only ever set on an outbound audio stream; WebRTC reports no RTT for video or for the receive direction. */
70
+ roundTripTime?: number;
71
+ bytesSent?: number;
72
+ bytesReceived?: number;
73
+ /** Receive-direction video only. WebRTC doesn't surface it for audio or for sending. */
74
+ mimeType?: string;
75
+ frameWidth?: number;
76
+ frameHeight?: number;
77
+ framesPerSecond?: number;
78
+ }
79
+ /**
80
+ * Raven's own normalized shape. A raw WebRTC stats object never appears on
81
+ * a public class.
82
+ *
83
+ * Every field is optional, and gets omitted, not set to `0` or
84
+ * `null` when the browser or SFU didn't report it. "0% packet loss" and
85
+ * "we don't know" are different facts, and this will not fabricate the
86
+ * first to cover for the second.
87
+ */
88
+ interface TrackStats {
89
+ kind: TrackKind;
90
+ /** 'send' for a track we published, 'receive' for one we subscribed to. */
91
+ direction: 'send' | 'receive';
92
+ /** Bytes/sec, from two consecutive samples. Undefined on a track's first sample, since there's nothing to take a delta against yet. */
93
+ bitrateBps?: number;
94
+ packetsLost?: number;
95
+ /** 0-100. The module doc explains why it's an approximation and not a WebRTC-spec figure. */
96
+ packetLossPercent?: number;
97
+ jitterMs?: number;
98
+ roundTripTimeMs?: number;
99
+ /** Only set for a received video track. See `RawTrackStats.mimeType`. */
100
+ codec?: string;
101
+ frameWidth?: number;
102
+ frameHeight?: number;
103
+ framesPerSecond?: number;
104
+ }
105
+
106
+ type TrackKind = 'camera' | 'microphone' | 'screenShare' | 'unknown';
107
+ /**
108
+ * A structural interface, not a concrete class, so a track can be backed by
109
+ * a raw `MediaStreamTrack` (what the native adapter does) or by a test
110
+ * double, with neither having to inherit anything.
111
+ */
112
+ interface TrackDelegate {
113
+ readonly mediaStreamTrack: MediaStreamTrack;
114
+ readonly mediaStream?: MediaStream;
115
+ readonly isMuted: boolean;
116
+ attach(element?: HTMLMediaElement): HTMLMediaElement;
117
+ detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[];
118
+ }
119
+ /** Base class for local and remote tracks alike. Never leaks RTCRtpSender/Receiver. */
120
+ declare abstract class Track {
121
+ readonly kind: TrackKind;
122
+ protected readonly delegate: TrackDelegate;
123
+ constructor(delegate: TrackDelegate, kind: TrackKind);
124
+ /** The underlying native track, for the rare occasion you need to go deeper. */
125
+ get mediaStreamTrack(): MediaStreamTrack;
126
+ get mediaStream(): MediaStream | undefined;
127
+ get isMuted(): boolean;
128
+ /** Attaches this track to a `<video>`/`<audio>` element, creating one if omitted. */
129
+ attach(element?: HTMLMediaElement): HTMLMediaElement;
130
+ /** Detaches this track from one element, or from all elements if omitted. */
131
+ detach(element?: HTMLMediaElement): HTMLMediaElement[];
132
+ }
133
+ interface LocalTrackDelegate extends TrackDelegate {
134
+ mute(): Promise<unknown>;
135
+ unmute(): Promise<unknown>;
136
+ /**
137
+ * Optional, so a delegate that predates stats support (or a test double
138
+ * that doesn't care) still satisfies this interface untouched. Purely
139
+ * additive, never required. The array form covers video, where a
140
+ * simulcast sender reports one `outbound-rtp` entry per encoding layer
141
+ * instead of a single stream.
142
+ */
143
+ getSenderStats?(): Promise<RawTrackStats | RawTrackStats[] | undefined>;
144
+ /**
145
+ * Swaps the underlying MediaStreamTrack on an already-published sender.
146
+ * `RTCRtpSender.replaceTrack()` manages this without renegotiating, so
147
+ * nobody else in the room sees a thing. Optional for the same reason as
148
+ * getSenderStats: a delegate predating Raven Effects still satisfies
149
+ * this interface, and LocalTrack.attachEffects() checks for it rather
150
+ * than assuming every delegate has it.
151
+ */
152
+ replaceTrack?(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<unknown>;
153
+ }
154
+ interface RemoteTrackDelegate extends TrackDelegate {
155
+ /** Optional for the same reason as `LocalTrackDelegate.getSenderStats`. */
156
+ getReceiverStats?(): Promise<RawTrackStats | undefined>;
157
+ }
158
+ /** A locally captured track (camera, microphone or screen share), published or not. */
159
+ declare class LocalTrack extends Track {
160
+ private readonly localDelegate;
161
+ private lastSample?;
162
+ private attachedEffectsPipeline?;
163
+ private preEffectsMediaStreamTrack?;
164
+ constructor(delegate: LocalTrackDelegate, kind: TrackKind);
165
+ mute(): Promise<void>;
166
+ unmute(): Promise<void>;
167
+ /** Stops the underlying device capture. Publish state belongs to Room.unpublish(). */
168
+ stop(): void;
169
+ /**
170
+ * Where Raven Effects (`@ravenkash/effects`) plugs in. The chain is
171
+ * Camera → Raven Video Track → Effects Pipeline → Processed Video Track →
172
+ * Raven RTC.
173
+ *
174
+ * Runs `pipeline` against this track's live camera feed and, if the track
175
+ * is already published, swaps the sender's `MediaStreamTrack` in place
176
+ * through the adapter's `replaceTrack()`. No renegotiation, no reconnect,
177
+ * audio and the rest of the room untouched. Camera only for now; screen
178
+ * share and microphone aren't supported.
179
+ *
180
+ * Can't run the pipeline on this device (no WebGL2, Canvas2D or
181
+ * captureStream)? It falls back to the original track on its own. The
182
+ * call keeps working either way.
183
+ */
184
+ attachEffects(pipeline: EffectsPipeline): Promise<void>;
185
+ /** Goes back to the unmodified camera track and frees the pipeline's engine resources. */
186
+ detachEffects(): Promise<void>;
187
+ /**
188
+ * Live send-side stats for this track: bitrate, packet loss, jitter, RTT
189
+ * (audio only), resolution and fps (video only).
190
+ *
191
+ * You get `undefined`, not a zeroed-out object, when the adapter can't
192
+ * supply them, either because the delegate has no `getSenderStats` or
193
+ * because the underlying call resolved to nothing. The module doc covers
194
+ * why that distinction matters.
195
+ *
196
+ * Call it periodically instead of once. `Room.getConnectionStats()` does,
197
+ * every few seconds. Bitrate needs two samples to compute, so the first
198
+ * call after a track starts always omits it.
199
+ */
200
+ getStats(): Promise<TrackStats | undefined>;
201
+ }
202
+ /** A track received from a remote participant, via the SFU. */
203
+ declare class RemoteTrack extends Track {
204
+ private readonly remoteDelegate;
205
+ private lastSample?;
206
+ constructor(delegate: RemoteTrackDelegate, kind: TrackKind);
207
+ /** Live receive-side stats. `LocalTrack.getStats()` covers the shape and the caveats. */
208
+ getStats(): Promise<TrackStats | undefined>;
209
+ }
210
+
211
+ /** Fields the local user and remote participants have in common. */
212
+ declare abstract class Participant {
213
+ private _identity;
214
+ /** Opaque application metadata set when the RTC token was minted. */
215
+ readonly metadata?: string;
216
+ constructor(identity: string, metadata?: string);
217
+ /** The RTC token's participant identity. Stable for the whole session. */
218
+ get identity(): string;
219
+ /**
220
+ * @internal Called once by the SFU adapter just after connect() resolves.
221
+ * The constructor runs before the server has confirmed identity, so this
222
+ * patches it in afterwards.
223
+ */
224
+ _setIdentity(identity: string): void;
225
+ }
226
+ declare class LocalParticipant extends Participant {
227
+ /** Tracks this participant has published, in publish order. Mutated by Room. */
228
+ readonly tracks: LocalTrack[];
229
+ }
230
+ declare class RemoteParticipant extends Participant {
231
+ /** Tracks subscribed from this participant, in subscribe order. Mutated by Room. */
232
+ readonly tracks: RemoteTrack[];
233
+ }
234
+
235
+ type SdkConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'failed';
236
+ /**
237
+ * A coarse quality signal, reported by the SFU. The client doesn't derive
238
+ * it from raw stats; it's whatever the SFU itself computed. The SFU has by
239
+ * far the better view: it sees loss and jitter on every leg of the room,
240
+ * not just this one client's.
241
+ *
242
+ * `'unknown'` covers both "not connected yet" and "the SFU has nothing to
243
+ * say", which is the honest answer either way. It's also the usual answer
244
+ * right now, since Raven's SFU doesn't compute a quality verdict yet. See
245
+ * `RavenAdapter.getConnectionQuality`.
246
+ */
247
+ type ConnectionQuality = 'excellent' | 'good' | 'poor' | 'lost' | 'unknown';
248
+ type DeviceKind = 'videoinput' | 'audioinput' | 'audiooutput';
249
+ interface DeviceInfo {
250
+ deviceId: string;
251
+ label: string;
252
+ kind: DeviceKind;
253
+ }
254
+ interface SFUAdapterEventMap {
255
+ connectionStateChanged: (state: SdkConnectionState) => void;
256
+ participantJoined: (participant: RemoteParticipant) => void;
257
+ participantLeft: (participant: RemoteParticipant) => void;
258
+ trackPublished: (kind: TrackKind, participant: RemoteParticipant) => void;
259
+ trackUnpublished: (kind: TrackKind, participant: RemoteParticipant) => void;
260
+ trackSubscribed: (track: RemoteTrack, participant: RemoteParticipant) => void;
261
+ trackUnsubscribed: (track: RemoteTrack, participant: RemoteParticipant) => void;
262
+ /** Phase 11 addition. Lets a UI show a muted indicator without polling `track.isMuted`. */
263
+ trackMuted: (kind: TrackKind, participant: RemoteParticipant) => void;
264
+ trackUnmuted: (kind: TrackKind, participant: RemoteParticipant) => void;
265
+ localTrackPublished: (track: LocalTrack) => void;
266
+ localTrackUnpublished: (track: LocalTrack) => void;
267
+ dataReceived: (payload: Uint8Array, participant?: RemoteParticipant) => void;
268
+ mediaError: (error: Error) => void;
269
+ }
270
+ /**
271
+ * The boundary between the public Room API and whatever actually
272
+ * implements the connection.
273
+ *
274
+ * This interface is the reason swapping LiveKit out for Raven's own SFU
275
+ * didn't touch the public API. `Room` and `RTCClient` are written against
276
+ * it, never against a particular implementation, so trading
277
+ * `internal/sfu/livekit-adapter.ts` for `internal/sfu/raven-adapter.ts`
278
+ * came down to one line in the default factory. It also lets tests drive
279
+ * Room/Client logic through a fake adapter instead of a real browser and
280
+ * WebRTC stack.
281
+ */
282
+ interface SFUAdapter {
283
+ readonly connectionState: SdkConnectionState;
284
+ readonly localParticipant: LocalParticipant;
285
+ readonly remoteParticipants: Map<string, RemoteParticipant>;
286
+ /** The SFU's own read on this connection's health right now. */
287
+ getConnectionQuality(): ConnectionQuality;
288
+ /**
289
+ * Live ICE and signaling state, where the implementation can see it.
290
+ *
291
+ * Optional, because not every adapter owns an `RTCPeerConnection` to read
292
+ * it from. The LiveKit adapter didn't, which is why
293
+ * `Room.getDiagnostics()` reported these as `undefined` for so long. The
294
+ * native adapter does, so a bug report now carries the states that
295
+ * actually explain a failed connection.
296
+ */
297
+ getIceConnectionState?(): string | undefined;
298
+ getSignalingState?(): string | undefined;
299
+ /**
300
+ * The SFU's own view of the connection. It can disagree with the local
301
+ * one, and that disagreement is frequently the entire diagnosis.
302
+ */
303
+ getRemoteConnectionState?(): {
304
+ iceState?: string;
305
+ peerState?: string;
306
+ };
307
+ connect(endpoint: string, token: string, iceServers?: RTCIceServer[]): Promise<void>;
308
+ disconnect(): Promise<void>;
309
+ on<E extends keyof SFUAdapterEventMap>(event: E, handler: SFUAdapterEventMap[E]): void;
310
+ off<E extends keyof SFUAdapterEventMap>(event: E, handler: SFUAdapterEventMap[E]): void;
311
+ enableCamera(enabled: boolean): Promise<LocalTrack | undefined>;
312
+ enableMicrophone(enabled: boolean): Promise<LocalTrack | undefined>;
313
+ enableScreenShare(enabled: boolean): Promise<LocalTrack | undefined>;
314
+ publish(track: LocalTrack): Promise<void>;
315
+ unpublish(track: LocalTrack): Promise<void>;
316
+ sendData(payload: Uint8Array<ArrayBuffer>): Promise<void>;
317
+ getDevices(kind?: DeviceKind): Promise<DeviceInfo[]>;
318
+ /**
319
+ * Phase 11 widened this from `'videoinput' | 'audioinput'` to the full
320
+ * `DeviceKind`. Purely additive: every existing caller passing one of the
321
+ * original two values carries on unaffected. What it buys is
322
+ * `'audiooutput'`, i.e. speaker selection, on browsers that support
323
+ * `setSinkId`.
324
+ */
325
+ setDevice(kind: DeviceKind, deviceId: string): Promise<void>;
326
+ }
327
+
328
+ /** Stable typed error codes the SDK raises. Never a raw browser DOMException. */
329
+ type RTCErrorCode = 'INVALID_TOKEN' | 'TOKEN_EXPIRED' | 'ROOM_NOT_FOUND' | 'CONNECTION_FAILED' | 'PERMISSION_DENIED' | 'CAMERA_PERMISSION_DENIED' | 'MICROPHONE_PERMISSION_DENIED' | 'DEVICE_NOT_FOUND' | 'NETWORK_ERROR' | 'SIGNALING_ERROR' | 'MEDIA_ERROR'
330
+ /**
331
+ * The platform simply can't do this. Screen sharing on a mobile browser
332
+ * with no `getDisplayMedia`, say.
333
+ *
334
+ * Added in the native-RTC release, and kept separate from `MEDIA_ERROR`
335
+ * on purpose. "This device has no such capability" is a permanent fact a
336
+ * UI should act on by hiding the button; `MEDIA_ERROR` is a failure worth
337
+ * retrying. Spec §16 requires telling the two apart. Purely additive, so
338
+ * no existing code changed meaning.
339
+ */
340
+ | 'NOT_SUPPORTED' | 'TIMEOUT';
341
+ /** The only error type this SDK throws, or emits on the `error` event. */
342
+ declare class RTCError extends Error {
343
+ readonly code: RTCErrorCode;
344
+ readonly cause?: unknown;
345
+ constructor(code: RTCErrorCode, message: string, cause?: unknown);
346
+ }
347
+ declare function isRTCError(value: unknown): value is RTCError;
348
+
349
+ /** Minimal typed pub/sub. No dependency, and it keeps the bundle small. */
350
+ declare class TypedEventEmitter<EventMap extends {
351
+ [K in keyof EventMap]: (...args: never[]) => void;
352
+ }> {
353
+ private listeners;
354
+ on<E extends keyof EventMap>(event: E, handler: EventMap[E]): this;
355
+ off<E extends keyof EventMap>(event: E, handler: EventMap[E]): this;
356
+ once<E extends keyof EventMap>(event: E, handler: EventMap[E]): this;
357
+ removeAllListeners(event?: keyof EventMap): this;
358
+ protected emit<E extends keyof EventMap>(event: E, ...args: Parameters<EventMap[E]>): void;
359
+ }
360
+
361
+ interface TelemetryClient {
362
+ readonly connectionId: string;
363
+ /**
364
+ * Fire and forget. Never hands back a promise anyone is meant to await,
365
+ * never throws. See docs/telemetry.md#reliability and Phase 9 spec §11:
366
+ * "RTC must continue working even if telemetry fails."
367
+ */
368
+ send(type: string, data?: Record<string, unknown>): void;
369
+ }
370
+
371
+ type ConnectionState = SdkConnectionState;
372
+ /**
373
+ * A diagnostic snapshot that's safe to paste straight into a bug report.
374
+ * No tokens, no secrets, nothing sensitive.
375
+ *
376
+ * Anything we don't know comes back `undefined`; nothing here is guessed.
377
+ * `iceConnectionState` and `signalingState` were always `undefined` under
378
+ * the LiveKit adapter, which never exposed them. The native adapter does,
379
+ * so they now carry the states that actually explain a failed connection.
380
+ */
381
+ interface ConnectionDiagnostics {
382
+ connectionState: ConnectionState;
383
+ iceConnectionState?: string;
384
+ signalingState?: string;
385
+ /**
386
+ * What the SFU makes of this connection. Worth sitting next to the local
387
+ * states, because the two can disagree, and "server says failed, browser
388
+ * says connected" is a diagnosis, not a contradiction.
389
+ */
390
+ remoteIceConnectionState?: string;
391
+ remotePeerConnectionState?: string;
392
+ reconnectCount: number;
393
+ sdkVersion: string;
394
+ platform: string;
395
+ browser: string;
396
+ }
397
+ /**
398
+ * Live media-quality stats. A separate `async` method rather than another
399
+ * field on `getDiagnostics()`, and that's on purpose. `getDiagnostics()`
400
+ * is synchronous and cheap so you can call it from anywhere at any time.
401
+ * Collecting real WebRTC stats is neither: it costs at least one round
402
+ * trip through the browser's stats API per track, and squashing per-track
403
+ * numbers into one flat object throws away the very thing that made them
404
+ * useful in a room full of people.
405
+ */
406
+ interface ConnectionStats {
407
+ connectionState: ConnectionState;
408
+ /** The SFU's own read on connection health. See `ConnectionQuality`. */
409
+ connectionQuality: ConnectionQuality;
410
+ /** One entry per track this side has published. */
411
+ local: TrackStats[];
412
+ /** One entry per subscribed track, across all remote participants. */
413
+ remote: TrackStats[];
414
+ }
415
+ interface RoomEventMap {
416
+ connectionStateChanged: (state: ConnectionState) => void;
417
+ connected: () => void;
418
+ disconnected: () => void;
419
+ reconnecting: () => void;
420
+ reconnected: () => void;
421
+ participantJoined: (participant: RemoteParticipant) => void;
422
+ participantLeft: (participant: RemoteParticipant) => void;
423
+ trackPublished: (kind: TrackKind, participant: RemoteParticipant) => void;
424
+ trackUnpublished: (kind: TrackKind, participant: RemoteParticipant) => void;
425
+ trackSubscribed: (track: RemoteTrack, participant: RemoteParticipant) => void;
426
+ trackUnsubscribed: (track: RemoteTrack, participant: RemoteParticipant) => void;
427
+ /** Phase 11 addition. A remote participant muted or unmuted a track they'd already published. */
428
+ trackMuted: (kind: TrackKind, participant: RemoteParticipant) => void;
429
+ trackUnmuted: (kind: TrackKind, participant: RemoteParticipant) => void;
430
+ localTrackPublished: (track: LocalTrack) => void;
431
+ localTrackUnpublished: (track: LocalTrack) => void;
432
+ dataReceived: (payload: Uint8Array, participant?: RemoteParticipant) => void;
433
+ error: (error: RTCError) => void;
434
+ }
435
+ /**
436
+ * A joined room. You get one from `client.join(roomId)`; don't build it
437
+ * yourself. It owns participant and track state plus every room-scoped
438
+ * action. No SDP, ICE candidates or RTCPeerConnection leak out here.
439
+ */
440
+ declare class Room extends TypedEventEmitter<RoomEventMap> {
441
+ readonly roomId: string;
442
+ /** Stable for the whole life of this connection. This is the id to quote in a bug report (Phase 9 spec §8). */
443
+ readonly connectionId: string;
444
+ readonly localParticipant: LocalParticipant;
445
+ private readonly adapter;
446
+ private readonly logger;
447
+ private readonly telemetry;
448
+ private reconnectCount;
449
+ private statsTimer?;
450
+ /**
451
+ * How often the stats monitor samples and reports. Often enough that a
452
+ * dashboard showing "now" isn't showing you five minutes ago, rarely
453
+ * enough that it doesn't hammer the telemetry endpoint on a call where
454
+ * dozens of participants are all doing exactly this.
455
+ */
456
+ private static readonly STATS_INTERVAL_MS;
457
+ /** @internal Use `client.join(roomId)`. The telemetry client defaults to a no-op, so tests and advanced setups can build a Room directly without wiring one up. */
458
+ constructor(adapter: SFUAdapter, roomId: string, logger: Logger, telemetry?: TelemetryClient);
459
+ get remoteParticipants(): RemoteParticipant[];
460
+ get connectionState(): ConnectionState;
461
+ private wireAdapterEvents;
462
+ /**
463
+ * A non-secret diagnostic snapshot for support and debugging.
464
+ *
465
+ * Synchronous and cheap by design, so you can call it from anywhere at
466
+ * any time, error handlers included. Live media stats are a separate
467
+ * `async` call; see `getConnectionStats()`.
468
+ */
469
+ getDiagnostics(): ConnectionDiagnostics;
470
+ /**
471
+ * Live media-quality stats for every published and subscribed track.
472
+ * RTT, jitter, packet loss, bitrate, codec, resolution/fps, and the
473
+ * SFU's own connection-quality read. `ConnectionStats` explains why this
474
+ * is kept apart from `getDiagnostics()`.
475
+ *
476
+ * Call it whenever you like, including before anything is published or
477
+ * subscribed. You just get empty `local`/`remote` arrays then, not an
478
+ * error.
479
+ */
480
+ getConnectionStats(): Promise<ConnectionStats>;
481
+ /**
482
+ * Polls `getConnectionStats()` on a timer and ships the result as
483
+ * telemetry, so the dashboard's RTC view (spec §23) has numbers to show
484
+ * without every developer wiring it up by hand. Best-effort, same as
485
+ * every other telemetry event here: failures get swallowed. A hiccup
486
+ * collecting stats is no reason to disturb the call it's describing.
487
+ */
488
+ private startStatsMonitor;
489
+ private stopStatsMonitor;
490
+ /** Captures and publishes the camera in one go. Resolves to the published track. */
491
+ enableCamera(): Promise<LocalTrack | undefined>;
492
+ /** Stops publishing and releases the camera. */
493
+ disableCamera(): Promise<void>;
494
+ /** Captures and publishes the microphone in one call. Resolves to the published track. */
495
+ enableMicrophone(): Promise<LocalTrack | undefined>;
496
+ /** Stops publishing and releases the microphone. */
497
+ disableMicrophone(): Promise<void>;
498
+ /** Captures and publishes a screen share in one call. */
499
+ enableScreenShare(): Promise<LocalTrack | undefined>;
500
+ disableScreenShare(): Promise<void>;
501
+ /** Publishes a track you made with `client.createCameraTrack()` and friends. */
502
+ publish(track: LocalTrack): Promise<void>;
503
+ unpublish(track: LocalTrack): Promise<void>;
504
+ /** Switches the active camera without republishing. */
505
+ setCameraDevice(deviceId: string): Promise<void>;
506
+ /** Switches the active microphone without republishing. */
507
+ setMicrophoneDevice(deviceId: string): Promise<void>;
508
+ /**
509
+ * Switches the audio output ("speaker") device for this room's remote
510
+ * audio elements. Phase 11 addition. Not supported everywhere: Safari
511
+ * has no `HTMLMediaElement.setSinkId`. Browsers that don't implement it
512
+ * get a `DEVICE_NOT_FOUND` throw instead of a silent no-op.
513
+ */
514
+ setSpeakerDevice(deviceId: string): Promise<void>;
515
+ /**
516
+ * Sends a small payload to everyone, or to specific people if the
517
+ * underlying SFU adapter supports targeting. Requires the token's
518
+ * `publishData` grant; throws PERMISSION_DENIED without it.
519
+ */
520
+ sendData(payload: string | Uint8Array): Promise<void>;
521
+ /**
522
+ * Resolves once the media connection is genuinely up.
523
+ *
524
+ * # Why this exists
525
+ *
526
+ * `client.join()` resolves when the **control plane** lets you in: room
527
+ * joined, you know who else is here, you can publish. The media
528
+ * connection finishes a moment later, once ICE and DTLS are done, which
529
+ * means `connectionState` sits at `'connecting'` for a short window
530
+ * after `join()` returns. That's the honest shape of an SFU connection,
531
+ * and it's why `'connected'` is an event, not something joining
532
+ * guarantees you.
533
+ *
534
+ * Most callers need none of this. `enableCamera()` and
535
+ * `enableMicrophone()` work fine inside that window, and the `connected`
536
+ * event is what you want driving a UI. This is for code that genuinely
537
+ * has to block: a test, or a flow that mustn't move on until media is
538
+ * live.
539
+ *
540
+ * Resolves straight away if already connected. Rejects on `'failed'` and
541
+ * on timeout, rather than handing back a connection that isn't there.
542
+ *
543
+ * Careful: a subscriber joining a room where nobody is publishing can
544
+ * quite legitimately stay `'connecting'`. With no tracks on either side
545
+ * there's nothing to negotiate, so waiting here times out on a
546
+ * connection that isn't broken at all. Where you can, drive the UI off
547
+ * the `connected` event instead of blocking on this.
548
+ */
549
+ waitUntilConnected(timeoutMs?: number): Promise<void>;
550
+ /** Leaves the room, stops local tracks and closes the underlying connection. */
551
+ leave(): Promise<void>;
552
+ }
553
+
554
+ type AdapterFactory = (logger: Logger, autoReconnect: boolean) => SFUAdapter;
555
+ /**
556
+ * The SDK's entry point. Holds your RTC token and endpoint and gets you
557
+ * into a room. Everything per-room lives on the `Room` it hands back.
558
+ */
559
+ declare class RTCClient {
560
+ private readonly config;
561
+ private readonly logger;
562
+ private readonly adapterFactory;
563
+ private currentRoom?;
564
+ /**
565
+ * @internal Use `createRTCClient(config)`. The second param exists purely
566
+ * so tests can inject a fake SFUAdapter without a real browser and WebRTC
567
+ * stack. Not part of the public config.
568
+ */
569
+ constructor(config: ResolvedRTCClientConfig, adapterFactory?: AdapterFactory);
570
+ /**
571
+ * Joins the room this client's token was minted for. `roomId` has to
572
+ * match that room; pass a different one and you get `ROOM_NOT_FOUND`
573
+ * straight away, before any connection is attempted.
574
+ */
575
+ join(roomId: string): Promise<Room>;
576
+ /** Leaves the most recently joined room, if there is one. Same as `.leave()` on that `Room`. */
577
+ leave(): Promise<void>;
578
+ /** Captures a camera track without joining or publishing. Pair it with `room.publish(track)`. */
579
+ createCameraTrack(deviceId?: string): Promise<LocalTrack>;
580
+ /** Captures a microphone track without joining or publishing. Pair it with `room.publish(track)`. */
581
+ createMicrophoneTrack(deviceId?: string): Promise<LocalTrack>;
582
+ /** Captures a screen-share track without joining or publishing. Pair it with `room.publish(track)`. */
583
+ createScreenShareTrack(): Promise<LocalTrack>;
584
+ /** Lists available devices. Labels only fill in once permission has been granted at least once. */
585
+ getDevices(kind?: DeviceKind): Promise<DeviceInfo[]>;
586
+ /**
587
+ * Subscribes to devices coming and going (Phase 11 addition), like a USB
588
+ * webcam being plugged in or yanked out. Returns an unsubscribe function.
589
+ *
590
+ * In an environment with no `navigator.mediaDevices` this is a no-op with
591
+ * an immediately-callable unsubscribe, rather than a throw. It's an
592
+ * optional convenience, not a capability anything depends on.
593
+ */
594
+ onDeviceChange(callback: () => void): () => void;
595
+ /** Switches the active camera on the currently joined room. */
596
+ setCamera(deviceId: string): Promise<void>;
597
+ /** Switches the active microphone on the currently joined room. */
598
+ setMicrophone(deviceId: string): Promise<void>;
599
+ /** Diagnostic snapshot of the currently joined room. See `Room.getDiagnostics()`. */
600
+ getDiagnostics(): ConnectionDiagnostics;
601
+ }
602
+ /**
603
+ * Creates an RTC client from a token your backend minted. `token` and
604
+ * `endpoint` come straight out of that same mint response. Don't build
605
+ * them by hand, and never mint a token in the browser.
606
+ */
607
+ declare function createRTCClient(config: RTCClientConfig): RTCClient;
608
+
609
+ /**
610
+ * Phase 11 addition. Feature-detects what the SDK actually needs, instead
611
+ * of keeping a user-agent allowlist that goes stale the moment you write
612
+ * it. See docs/sdk/web.md#browser-support for the documented matrix.
613
+ */
614
+ interface BrowserSupportDetails {
615
+ supported: boolean;
616
+ missing: string[];
617
+ }
618
+ declare function getBrowserSupportDetails(): BrowserSupportDetails;
619
+ /** A simple yes/no wrapper over `getBrowserSupportDetails()`. */
620
+ declare function isBrowserSupported(): boolean;
621
+
622
+ export { type BrowserSupportDetails, type ConnectionDiagnostics, type ConnectionQuality, type ConnectionState, type ConnectionStats, type DeviceInfo, type DeviceKind, LocalParticipant, LocalTrack, type LocalTrackDelegate, type LogLevel, Participant, RTCClient, type RTCClientConfig, RTCError, type RTCErrorCode, RemoteParticipant, RemoteTrack, type RemoteTrackDelegate, Room, type RoomEventMap, Track, type TrackDelegate, type TrackKind, type TrackStats, createRTCClient, getBrowserSupportDetails, isBrowserSupported, isRTCError };