@streaming-cdn/rtc-web 1.3.12 → 1.3.14

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.txt ADDED
@@ -0,0 +1,15 @@
1
+ STREAMING CDN SDK BINARY LICENSE
2
+
3
+ Copyright (c) 2026. All rights reserved.
4
+
5
+ The SDK binaries, public API declarations, documentation, and example code are
6
+ licensed for use with an active Streaming CDN service account and are governed
7
+ by the applicable service agreement.
8
+
9
+ Except where third-party notices state otherwise, redistribution, modification,
10
+ reverse engineering, decompilation, and creation of derivative SDK products are
11
+ not permitted without prior written authorization.
12
+
13
+ Example applications may be modified and incorporated into an application that
14
+ uses the Streaming CDN service. No ownership rights in the SDK binaries or
15
+ service are transferred.
package/NOTICE.md ADDED
@@ -0,0 +1,8 @@
1
+ # Third-party notices
2
+
3
+ The SDK uses platform and media-runtime components distributed under their own
4
+ licenses. Those components remain subject to their respective license terms.
5
+
6
+ The binary SDK license applies only to the Streaming CDN integration layer.
7
+ Applications must also comply with Apple, Google, browser, notification-service,
8
+ and media-runtime requirements applicable to the selected integration modules.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # RTC Web SDK 1.3.12
1
+ # RTC Web SDK 1.3.14
2
2
 
3
3
  The customer package contains compiled ESM/UMD JavaScript and TypeScript declarations. It intentionally excludes implementation source and source maps. Example source remains available under `examples/`.
4
4
 
@@ -152,14 +152,57 @@ side receives a terminal call state. Manual integrations must call
152
152
  `transport: "mesh"` (the default) opens one connection per pair, which suits
153
153
  direct calls and small rooms. `transport: "sfu"` sends media once to the
154
154
  platform SFU and receives each remote participant from it, which is what a
155
- large room needs — media no longer grows with the participant count. The
156
- credential, the events and the rest of the API are identical, so an
157
- application switches transports without changing its UI or call flow. SDP for
158
- the SFU travels over `/v1/rtc/sfu/*` authenticated by the same signaling
159
- token; no separate credential exists. With a single uplink there is no
160
- per-connection tier, so `peerquality` is not emitted on this transport and
161
- `setQuality` applies to the uplink directly. The React Native adapter
162
- currently ships the mesh transport.
155
+ large room needs — media no longer grows with the participant count.
156
+ `transport: "auto"` picks the SFU for `meeting` mode and the mesh for `voice`
157
+ and `video` calls. The credential, the events and the rest of the API are
158
+ identical, so an application switches transports without changing its UI or
159
+ call flow. SDP for the SFU travels over `/v1/rtc/sfu/*` authenticated by the
160
+ same signaling token; no separate credential exists.
161
+
162
+ The SFU uplink publishes three simulcast layers (720p/360p/180p-class), so the
163
+ SFU can hand every subscriber the layer their downlink carries. `setQuality`
164
+ caps which layers are encoded; there is no per-connection tier on a single
165
+ uplink, so `peerquality` is not emitted on this transport. To deliberately
166
+ receive a lower layer for one participant — a thumbnail tile has no use for
167
+ 720p — call `setRemoteQuality(participantId, "low" | "medium" | "high")` on
168
+ the room client (`getNativeClient()`); the publisher keeps sending every
169
+ layer. The React Native adapter has the same transport option and the same
170
+ `setRemoteQuality` on the client.
171
+
172
+ ## Active speaker
173
+
174
+ The client samples WebRTC audio levels once a second on both transports and
175
+ emits `activespeaker` (`{ participantId }`, or `null` when the room is
176
+ silent). The choice has hysteresis — the highlight only moves when somebody is
177
+ clearly louder than the current speaker — so two people at similar volume do
178
+ not flap the indicator. Use it to light up the speaking tile:
179
+
180
+ ```ts
181
+ media.addEventListener("activespeaker", ({ detail }) => {
182
+ highlightTile(detail.detail.participantId);
183
+ });
184
+ ```
185
+
186
+ ## Picture-in-picture
187
+
188
+ `createPictureInPictureController(media.getNativeClient())` floats a
189
+ participant's video in the browser's always-on-top window while the user works
190
+ elsewhere. `enter(participantId?)` must run inside a click handler — browsers
191
+ require a user gesture — and picks the first remote participant by default,
192
+ falling back to the local preview. `onLeave` fires when the user closes the
193
+ floating window; `supported` is false where the browser has no PiP.
194
+
195
+ ```ts
196
+ import { createPictureInPictureController } from "@streaming-cdn/rtc-web";
197
+
198
+ const pip = createPictureInPictureController(media.getNativeClient(), {
199
+ onLeave: () => showInlineVideoAgain()
200
+ });
201
+ pipButton.onclick = () => { void pip.enter(); };
202
+ ```
203
+
204
+ Android and iOS system PiP require native application support and are on the
205
+ SDK roadmap.
163
206
 
164
207
  ## Quality and codecs
165
208
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Active speaker detection.
3
+ *
4
+ * A call UI wants to highlight whoever is talking without every application
5
+ * re-implementing audio-level plumbing. Levels come from WebRTC's own
6
+ * `inbound-rtp` audio stats — no AudioContext, no extra decoding — and the
7
+ * choice has hysteresis: the floor only changes hands when somebody is
8
+ * clearly louder than the current speaker, otherwise two people at similar
9
+ * volume would flap the highlight every second.
10
+ */
11
+ export interface SpeakerLevel {
12
+ participantId: string;
13
+ /** WebRTC audioLevel, 0..1. */
14
+ level: number;
15
+ }
16
+ /** Below this a level is background noise, not speech. */
17
+ export declare const SPEAKER_NOISE_FLOOR = 0.02;
18
+ /** A challenger must be this much louder than the current speaker to take over. */
19
+ export declare const SPEAKER_TAKEOVER_RATIO = 1.4;
20
+ export declare function pickActiveSpeaker(levels: SpeakerLevel[], previous: string | null, noiseFloor?: number, takeoverRatio?: number): string | null;
21
+ /** Reads the audio level of each inbound audio RTP stream, keyed by mid. */
22
+ export declare function readAudioLevels(report: Iterable<Record<string, unknown>>): Array<{
23
+ mid: string | null;
24
+ level: number;
25
+ }>;
@@ -0,0 +1,341 @@
1
+ export { createPictureInPictureController } from "./picture-in-picture";
2
+ export type { PictureInPictureController, PictureInPictureOptions } from "./picture-in-picture";
3
+ export { pickActiveSpeaker, SPEAKER_NOISE_FLOOR, SPEAKER_TAKEOVER_RATIO } from "./active-speaker";
4
+ export type { SpeakerLevel } from "./active-speaker";
5
+ export type RtcMode = "voice" | "video" | "meeting";
6
+ /**
7
+ * How media travels. `mesh` (the default) opens one connection per pair and
8
+ * suits direct calls and small rooms; `sfu` sends media once to the platform
9
+ * SFU and pulls each remote participant from it, which is what a large room
10
+ * needs. `auto` picks by product: meetings — rooms that grow — take the SFU,
11
+ * voice and video calls stay on the mesh. The rest of the public contract is
12
+ * identical either way.
13
+ */
14
+ export type RtcTransport = "mesh" | "sfu" | "auto";
15
+ export type RtcQuality = "auto" | "audio" | "low" | "medium" | "high";
16
+ export type RtcVideoCodec = "auto" | "vp8" | "h264";
17
+ export type RtcIceTransportPolicy = "all" | "relay";
18
+ export type RtcConnectionState = "initialized" | "joining" | "connected" | "reconnecting" | "disconnected" | "failed";
19
+ export interface RtcCredential {
20
+ signalingToken: string;
21
+ signalingUrl: string;
22
+ iceServers?: RTCIceServer[];
23
+ iceServersExpiresAt?: string;
24
+ roomId: string;
25
+ participantId?: string;
26
+ externalUserId?: string;
27
+ telemetryToken?: string;
28
+ telemetryEndpoint?: string;
29
+ telemetryExpiresAt?: string;
30
+ }
31
+ export interface RtcClientOptions {
32
+ credential: RtcCredential;
33
+ mode: RtcMode;
34
+ /** Reuses media acquired by a pre-join device room instead of requesting it again. */
35
+ localStream?: MediaStream | null;
36
+ /** Publishes microphone audio when true. */
37
+ audio?: boolean;
38
+ /** Publishes camera video when true. */
39
+ video?: boolean;
40
+ /** Creates an audio recvonly transceiver when local audio is not published. */
41
+ receiveAudio?: boolean;
42
+ /** Creates a video recvonly transceiver when local video is not published. */
43
+ receiveVideo?: boolean;
44
+ /** Initial outbound video quality. "auto" adapts between low, medium and high. */
45
+ quality?: RtcQuality;
46
+ /** Preferred video codec. Negotiation still retains compatible fallback codecs. */
47
+ preferredVideoCodec?: RtcVideoCodec;
48
+ /** Uses every ICE route by default. Set relay to diagnose or avoid a poor direct carrier route. */
49
+ iceTransportPolicy?: RtcIceTransportPolicy;
50
+ /** Media transport. Defaults to the peer mesh; "sfu" routes media through the platform SFU. */
51
+ transport?: RtcTransport;
52
+ /** Enables automatic quality changes when quality is "auto". */
53
+ adaptiveQuality?: boolean;
54
+ autoJoin?: boolean;
55
+ statsInterval?: number;
56
+ telemetryInterval?: number;
57
+ autoReportStats?: boolean;
58
+ telemetryEndpoint?: string;
59
+ preflightProbeUrl?: string;
60
+ peerConnection?: RTCPeerConnection | null;
61
+ statsProvider?: () => Promise<Partial<RtcQualitySample>>;
62
+ onEvent?: (event: RtcSdkEvent) => void;
63
+ }
64
+ export interface RtcSdkEvent<T = unknown> {
65
+ type: string;
66
+ timestamp: string;
67
+ detail: T;
68
+ }
69
+ export interface RtcQualitySample {
70
+ sampleType: "preflight" | "runtime" | "final";
71
+ sampledAt: string;
72
+ connectionState: string;
73
+ mediaScore: number | null;
74
+ networkScore: number;
75
+ estimatedMos: number;
76
+ rttMs: number | null;
77
+ jitterMs: number | null;
78
+ packetLossPct: number | null;
79
+ /** Packet loss since the previous sample. packetLossPct uses the same interval. */
80
+ packetLossCumulativePct: number | null;
81
+ packetsLost: number | null;
82
+ packetsReceived: number | null;
83
+ inboundBitrateBps: number | null;
84
+ outboundBitrateBps: number | null;
85
+ availableOutgoingBitrateBps: number | null;
86
+ framesPerSecond: number | null;
87
+ frameWidth: number | null;
88
+ frameHeight: number | null;
89
+ framesDropped: number | null;
90
+ freezeCount: number | null;
91
+ freezeDurationMs: number | null;
92
+ concealedSamples: number | null;
93
+ codec: string | null;
94
+ transportProtocol: string | null;
95
+ candidateType: string | null;
96
+ qualityLimitationReason: string | null;
97
+ deviceMemoryGb: number | null;
98
+ effectiveConnectionType: string | null;
99
+ }
100
+ export interface RtcStats {
101
+ state: RtcConnectionState;
102
+ participantCount: number;
103
+ audioEnabled: boolean;
104
+ videoEnabled: boolean;
105
+ screenShareEnabled: boolean;
106
+ latestMediaScore: number | null;
107
+ latestQuality: RtcQualitySample | null;
108
+ sampledAt: string;
109
+ }
110
+ export interface RtcPreflightResult {
111
+ passed: boolean;
112
+ sampledAt: string;
113
+ secureContext: boolean;
114
+ mediaPermission: "granted" | "denied" | "unavailable";
115
+ microphoneCount: number;
116
+ cameraCount: number;
117
+ probeLatencyMs: number | null;
118
+ effectiveConnectionType: string | null;
119
+ issues: string[];
120
+ }
121
+ export interface RtcParticipant {
122
+ id: string;
123
+ name: string;
124
+ picture?: string;
125
+ role: string;
126
+ }
127
+ export interface RtcRemoteStream {
128
+ participant: RtcParticipant;
129
+ stream: MediaStream;
130
+ }
131
+ export declare class RtcClient extends EventTarget {
132
+ readonly mode: RtcMode;
133
+ private readonly options;
134
+ private client;
135
+ private peerConnection;
136
+ private state;
137
+ private latestMediaScore;
138
+ private latestQuality;
139
+ private statsTimer;
140
+ private statsRunning;
141
+ private previousCounters;
142
+ private lastTelemetryAt;
143
+ private joinPromise;
144
+ private requestedQuality;
145
+ private effectiveQuality;
146
+ private poorQualitySamples;
147
+ private goodQualitySamples;
148
+ constructor(options: RtcClientOptions);
149
+ initialize(): Promise<this>;
150
+ join(): Promise<void>;
151
+ leave(): Promise<void>;
152
+ setMuted(muted: boolean): Promise<void>;
153
+ setCameraEnabled(enabled: boolean): Promise<void>;
154
+ setVideoTrack(track: MediaStreamTrack): Promise<void>;
155
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
156
+ setDevice(device: MediaDeviceInfo): Promise<void>;
157
+ setQuality(quality: RtcQuality): Promise<void>;
158
+ setPreferredVideoCodec(codec: RtcVideoCodec): Promise<void>;
159
+ getDevices(): Promise<MediaDeviceInfo[]>;
160
+ getParticipants(): ReadonlyArray<RtcParticipant>;
161
+ getLocalStream(): MediaStream | null;
162
+ getRemoteStreams(): RtcRemoteStream[];
163
+ setPeerConnection(peerConnection: RTCPeerConnection | null): void;
164
+ sendChatMessage(message: string, participantIds?: string[]): Promise<void>;
165
+ sendControlMessage(action: string, detail?: Record<string, unknown>): Promise<void>;
166
+ createPoll(question: string, options: string[], config?: {
167
+ anonymous?: boolean;
168
+ hideVotes?: boolean;
169
+ }): Promise<void>;
170
+ votePoll(pollId: string, optionIndex: number): Promise<void>;
171
+ requestStageAccess(): Promise<void>;
172
+ cancelStageAccess(): Promise<void>;
173
+ joinStage(): Promise<void>;
174
+ leaveStage(): Promise<void>;
175
+ grantStageAccess(participantIds: string[]): Promise<void>;
176
+ denyStageAccess(participantIds: string[]): Promise<void>;
177
+ startRecording(allowMultiple?: boolean): Promise<void>;
178
+ stopRecording(recordingId?: string): Promise<void>;
179
+ pauseRecording(recordingId?: string): Promise<void>;
180
+ resumeRecording(recordingId?: string): Promise<void>;
181
+ runPreflight(options?: {
182
+ requestMedia?: boolean;
183
+ probeUrl?: string;
184
+ }): Promise<RtcPreflightResult>;
185
+ collectQualitySample(sampleType?: RtcQualitySample["sampleType"]): Promise<RtcQualitySample>;
186
+ reportQuality(sample: Partial<RtcQualitySample>): Promise<void>;
187
+ getStats(): RtcStats;
188
+ /** Escape hatch for advanced integrations. Prefer the stable SDK methods when possible. */
189
+ getNativeClient<T = unknown>(): T | null;
190
+ destroy(): void;
191
+ private handleNativeEvent;
192
+ private startStats;
193
+ private sampleAndReport;
194
+ private stopStats;
195
+ private adaptQuality;
196
+ private applyEffectiveQuality;
197
+ private requireClient;
198
+ private emit;
199
+ }
200
+ export declare function createRtcClient(options: RtcClientOptions): Promise<RtcClient>;
201
+ export interface RtcIncomingCall {
202
+ id: string;
203
+ mode: "voice" | "video";
204
+ status: string;
205
+ caller: {
206
+ id: string;
207
+ name: string;
208
+ };
209
+ metadata?: Record<string, unknown>;
210
+ createdAt?: string;
211
+ expiresAt?: string;
212
+ }
213
+ export interface RtcPresenceUser {
214
+ externalUserId: string;
215
+ displayName: string;
216
+ platform: "web" | "ios" | "android";
217
+ devices: number;
218
+ connectedAt: string;
219
+ }
220
+ export type RtcClientTokenReason = "initial" | "expiring" | "reconnect" | "unauthorized";
221
+ export interface RtcClientIdentityToken {
222
+ token: string;
223
+ expiresAt?: string;
224
+ }
225
+ export type RtcClientTokenProvider = (context: {
226
+ reason: RtcClientTokenReason;
227
+ previousExpiresAt?: string;
228
+ }) => Promise<string | RtcClientIdentityToken>;
229
+ export interface RtcIncomingClientOptions {
230
+ /** Fixed short-lived token. Prefer tokenProvider for production applications. */
231
+ token?: string;
232
+ /** Expiration timestamp for a fixed token, when known. */
233
+ tokenExpiresAt?: string;
234
+ /** Fetches a fresh short-lived token from the application's trusted backend. */
235
+ tokenProvider?: RtcClientTokenProvider;
236
+ /** Refresh lead time. Defaults to five minutes and scales down for short-lived tokens. */
237
+ tokenRefreshSkewMs?: number;
238
+ signalingUrl?: string;
239
+ apiBaseUrl?: string;
240
+ autoConnect?: boolean;
241
+ mount?: HTMLElement | string | null;
242
+ reconnect?: boolean;
243
+ reportSignalingDiagnostics?: boolean;
244
+ /** Enables call audio. Defaults to true. */
245
+ sounds?: boolean;
246
+ /** Custom incoming ringtone URL. A built-in tone is used when omitted. */
247
+ ringtoneUrl?: string;
248
+ /** Custom outgoing ringback URL. A built-in tone is used when omitted. */
249
+ ringbackUrl?: string;
250
+ /** How long terminal call states remain visible in the built-in UI. Defaults to 1600 ms. */
251
+ terminalStateDurationMs?: number;
252
+ onEvent?: (event: RtcSdkEvent) => void;
253
+ }
254
+ export interface RtcCallActionResult {
255
+ call: Record<string, unknown>;
256
+ credential?: RtcCredential;
257
+ callerCredential?: RtcCredential;
258
+ }
259
+ /**
260
+ * Unlock call audio from a click/tap handler before awaiting network work.
261
+ * Browsers may otherwise block incoming ringtones started by signaling events.
262
+ */
263
+ export declare function unlockRtcCallAudio(): Promise<boolean>;
264
+ export declare class RtcIncomingClient extends EventTarget {
265
+ private readonly options;
266
+ private socket;
267
+ private stopped;
268
+ private reconnectAttempt;
269
+ private reconnectTimer;
270
+ private heartbeatTimer;
271
+ private heartbeatDeadline;
272
+ private currentCall;
273
+ private outgoingCallId;
274
+ private mount;
275
+ private terminalTimer;
276
+ private tokenRefreshTimer;
277
+ private tokenRequest;
278
+ private currentToken;
279
+ private currentTokenExpiresAt?;
280
+ private readonly callAudio;
281
+ private readonly mediaClients;
282
+ private readonly activeCallIds;
283
+ private readonly seenInvitationIds;
284
+ private startCallRequest;
285
+ private lastDisconnect;
286
+ constructor(options: RtcIncomingClientOptions);
287
+ connect(): this;
288
+ setMount(target: HTMLElement | string | null): void;
289
+ /** Binds media lifetime to a call. Terminal call states automatically leave and destroy it. */
290
+ attachMediaClient(callId: string, media: RtcClient): () => void;
291
+ /**
292
+ * Rings everyone in `targets` as a single call and connects whoever answers
293
+ * first; the rest stop ringing on their own. `target` still works and means a
294
+ * call with one recipient.
295
+ */
296
+ startCall(input: {
297
+ mode: "voice" | "video";
298
+ target?: {
299
+ id: string;
300
+ name: string;
301
+ };
302
+ targets?: Array<{
303
+ id: string;
304
+ name: string;
305
+ }>;
306
+ metadata?: Record<string, unknown>;
307
+ idempotencyKey?: string;
308
+ }): Promise<RtcCallActionResult>;
309
+ accept(callId?: string | undefined): Promise<RtcCallActionResult>;
310
+ reject(callId?: string | undefined): Promise<RtcCallActionResult>;
311
+ busy(callId?: string | undefined): Promise<RtcCallActionResult>;
312
+ end(callId: string): Promise<RtcCallActionResult>;
313
+ cancel(callId: string): Promise<RtcCallActionResult>;
314
+ /** Injects an incoming call received through the application's own push or signaling channel. */
315
+ handleIncomingCall(call: RtcIncomingCall): void;
316
+ /** Applies a call state received through the application's own push or signaling channel. */
317
+ handleCallUpdate(call: {
318
+ id: string;
319
+ status: string;
320
+ }): void;
321
+ disconnect(): void;
322
+ destroy(): void;
323
+ private openSocket;
324
+ private handleSignal;
325
+ private callAction;
326
+ private request;
327
+ private renderIncoming;
328
+ private dismiss;
329
+ private showTerminal;
330
+ private clearUi;
331
+ private scheduleReconnect;
332
+ private resolveToken;
333
+ private tokenNeedsRefresh;
334
+ private refreshSkew;
335
+ private scheduleTokenRefresh;
336
+ private releaseMedia;
337
+ private reportSignalingDiagnostic;
338
+ private emit;
339
+ }
340
+ export declare function createRtcIncomingClient(options: RtcIncomingClientOptions): RtcIncomingClient;
341
+ export declare const version = "1.3.14";
@@ -0,0 +1,104 @@
1
+ export interface NativeRoomIdentity {
2
+ participantId: string;
3
+ externalUserId: string;
4
+ displayName: string;
5
+ platform: string;
6
+ }
7
+ export interface NativeRoomCredential {
8
+ signalingToken: string;
9
+ signalingUrl: string;
10
+ roomId: string;
11
+ participantId: string;
12
+ externalUserId?: string;
13
+ iceServers?: RTCIceServer[];
14
+ iceServersExpiresAt?: string;
15
+ }
16
+ export interface NativeRoomOptions {
17
+ credential: NativeRoomCredential;
18
+ audio: boolean;
19
+ video: boolean;
20
+ receiveAudio: boolean;
21
+ receiveVideo: boolean;
22
+ quality?: NativeRoomQuality;
23
+ preferredVideoCodec?: NativeRoomVideoCodec;
24
+ iceTransportPolicy?: RTCIceTransportPolicy;
25
+ localStream?: MediaStream | null;
26
+ onEvent: (type: string, detail: unknown) => void;
27
+ }
28
+ export type NativeRoomQuality = "audio" | "low" | "medium" | "high";
29
+ export type NativeRoomVideoCodec = "auto" | "vp8" | "h264";
30
+ export declare class NativeRoomClient {
31
+ private readonly options;
32
+ private socket;
33
+ private localStream;
34
+ private screenStream;
35
+ private readonly peers;
36
+ private joinPromise;
37
+ private resolveJoin;
38
+ private rejectJoin;
39
+ private joined;
40
+ private stopped;
41
+ private reconnectAttempt;
42
+ private reconnectTimer;
43
+ private heartbeatTimer;
44
+ private qualityTimer;
45
+ private speakerTimer;
46
+ private activeSpeaker;
47
+ private heartbeatDeadline;
48
+ private quality;
49
+ private preferredVideoCodec;
50
+ private signalQueue;
51
+ private socketGeneration;
52
+ constructor(options: NativeRoomOptions);
53
+ initialize(): Promise<void>;
54
+ join(): Promise<void>;
55
+ leave(): Promise<void>;
56
+ setMuted(muted: boolean): void;
57
+ setCameraEnabled(enabled: boolean): Promise<void>;
58
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
59
+ setDevice(device: MediaDeviceInfo): Promise<void>;
60
+ setQuality(quality: NativeRoomQuality): Promise<void>;
61
+ private static clampToCeiling;
62
+ setPreferredVideoCodec(codec: NativeRoomVideoCodec): Promise<void>;
63
+ getParticipants(): NativeRoomIdentity[];
64
+ getLocalStream(): MediaStream | null;
65
+ setVideoTrack(track: MediaStreamTrack): Promise<void>;
66
+ getRemoteStreams(): Array<{
67
+ participant: NativeRoomIdentity;
68
+ stream: MediaStream;
69
+ }>;
70
+ getPrimaryPeerConnection(): RTCPeerConnection | null;
71
+ sendChat(message: string, participantIds?: string[]): void;
72
+ sendControl(action: string, detail?: Record<string, unknown>): void;
73
+ private connect;
74
+ private openSocket;
75
+ private handleSignal;
76
+ private ensurePeer;
77
+ private createOffer;
78
+ private performOffer;
79
+ private shouldInitiateOffer;
80
+ private renegotiateAll;
81
+ private replaceVideoTrack;
82
+ private applyVideoQuality;
83
+ /**
84
+ * Applies this connection's own tier. The room-wide setting is a ceiling
85
+ * rather than an instruction: asking for audio keeps every link on audio, but
86
+ * asking for high lets each link settle wherever it can actually hold.
87
+ */
88
+ private applyPeerVideoQuality;
89
+ private startQualitySampling;
90
+ private stopQualitySampling;
91
+ private sampleActiveSpeaker;
92
+ private samplePeerQuality;
93
+ /** A new link starts one step below the ceiling so it proves itself upward. */
94
+ private static startingQuality;
95
+ private applyCodecPreference;
96
+ private removePeer;
97
+ private resetPeers;
98
+ private startHeartbeat;
99
+ private stopHeartbeat;
100
+ private clearConnectionTimers;
101
+ private scheduleReconnect;
102
+ private send;
103
+ private stopStream;
104
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Per-connection quality for a peer mesh.
3
+ *
4
+ * A mesh has no forwarding server, so there is nothing to send several
5
+ * qualities to — simulcast solves a problem this topology does not have. What
6
+ * it does have is one connection per participant, each with its own link, and
7
+ * until now a single room-wide quality was applied to all of them. One
8
+ * participant on a poor connection therefore either dragged the whole room down
9
+ * or was left with a stream their link could not carry. Each connection is
10
+ * rated on its own evidence instead.
11
+ *
12
+ * The ladder matches `sdk/react-native/src/quality.ts`, including the parts
13
+ * that look wrong until you read its tests: `"none"` is a truthy string and has
14
+ * to be normalised, and the step-up gate compares the bandwidth estimate
15
+ * against the *current* tier's ceiling on purpose. A field report argued the
16
+ * latter suppresses upgrades; the same report's data showed the estimate
17
+ * reaching 1,029,490 bps while pinned to a 600,000 tier, so the encoder cap was
18
+ * not holding it down, and relaxing the gate made the ladder climb to a tier the
19
+ * link could not carry.
20
+ */
21
+ export type PeerQuality = "audio" | "low" | "medium" | "high";
22
+ export interface PeerQualityTier {
23
+ active: boolean;
24
+ maxBitrate: number;
25
+ maxFramerate: number;
26
+ scaleResolutionDownBy: number;
27
+ }
28
+ export declare const peerQualityTiers: Record<PeerQuality, PeerQualityTier>;
29
+ export interface PeerQualityThresholds {
30
+ warmupSamples: number;
31
+ poorSamplesToStepDown: number;
32
+ goodSamplesToStepUp: number;
33
+ criticalLossPct: number;
34
+ criticalRttMs: number;
35
+ poorLossPct: number;
36
+ poorRttMs: number;
37
+ goodLossPct: number;
38
+ goodRttMs: number;
39
+ sustainRatio: number;
40
+ }
41
+ export declare const defaultPeerQualityThresholds: PeerQualityThresholds;
42
+ export interface PeerQualitySample {
43
+ packetLossPct: number | null;
44
+ rttMs: number | null;
45
+ availableOutgoingBitrateBps: number | null;
46
+ qualityLimitationReason: string | null;
47
+ }
48
+ export interface PeerQualityState {
49
+ quality: Exclude<PeerQuality, "audio">;
50
+ poorSamples: number;
51
+ goodSamples: number;
52
+ samplesSeen: number;
53
+ changed: boolean;
54
+ }
55
+ /** `"none"` is what a healthy sender reports, and it is a truthy string. */
56
+ export declare function normalizeLimitation(reason: string | null | undefined): "" | "cpu" | "bandwidth" | "other";
57
+ /**
58
+ * Chooses this connection's tier. `ceiling` is what the application asked for:
59
+ * adaptation may move below it but never above, so an app that asked for audio
60
+ * stays on audio.
61
+ */
62
+ export declare function adaptPeerQuality(current: Exclude<PeerQuality, "audio">, sample: PeerQualitySample, poorSamples: number, goodSamples: number, options?: {
63
+ samplesSeen?: number;
64
+ ceiling?: PeerQuality;
65
+ thresholds?: Partial<PeerQualityThresholds>;
66
+ }): PeerQualityState;
67
+ export interface PeerCounters {
68
+ packetsSent: number;
69
+ packetsLost: number;
70
+ timestamp: number;
71
+ }
72
+ /**
73
+ * Reads one connection's outbound video health. Loss is a delta against the
74
+ * previous read rather than the session total, so a connection that struggled
75
+ * early is not held down by history it has recovered from.
76
+ */
77
+ export declare function readPeerSample(report: Map<string, Record<string, unknown>> | Iterable<[string, Record<string, unknown>]>, previous: PeerCounters | null): {
78
+ sample: PeerQualitySample;
79
+ counters: PeerCounters | null;
80
+ };