@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs +2333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +622 -0
- package/dist/index.d.ts +622 -0
- package/dist/index.js +2319 -0
- package/dist/index.js.map +1 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/package.json +65 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|