@decartai/sdk 0.0.67 → 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.
Files changed (45) hide show
  1. package/README.md +55 -5
  2. package/dist/index.d.ts +7 -5
  3. package/dist/index.js +43 -28
  4. package/dist/process/client.js +1 -3
  5. package/dist/process/request.js +1 -3
  6. package/dist/queue/client.js +1 -3
  7. package/dist/queue/polling.js +1 -2
  8. package/dist/queue/request.js +1 -3
  9. package/dist/realtime/client.d.ts +23 -13
  10. package/dist/realtime/client.js +74 -244
  11. package/dist/realtime/config-realtime.js +49 -0
  12. package/dist/realtime/event-buffer.js +1 -3
  13. package/dist/realtime/initial-state-gate.js +21 -0
  14. package/dist/realtime/media-channel.js +82 -0
  15. package/dist/realtime/methods.js +12 -42
  16. package/dist/realtime/mirror-stream.js +1 -2
  17. package/dist/realtime/observability/diagnostics.d.ts +39 -0
  18. package/dist/realtime/observability/livekit-stats-provider.js +25 -0
  19. package/dist/realtime/observability/realtime-observability.js +173 -0
  20. package/dist/realtime/{telemetry-reporter.js → observability/telemetry-reporter.js} +12 -31
  21. package/dist/realtime/observability/webrtc-stats.d.ts +148 -0
  22. package/dist/realtime/observability/webrtc-stats.js +276 -0
  23. package/dist/realtime/signaling-channel.js +286 -0
  24. package/dist/realtime/stream-session.js +252 -0
  25. package/dist/realtime/subscribe-client.d.ts +2 -1
  26. package/dist/realtime/subscribe-client.js +115 -11
  27. package/dist/realtime/types.d.ts +25 -1
  28. package/dist/shared/model.d.ts +11 -1
  29. package/dist/shared/model.js +51 -14
  30. package/dist/shared/request.js +1 -3
  31. package/dist/shared/types.js +1 -3
  32. package/dist/tokens/client.js +1 -3
  33. package/dist/utils/env.js +1 -2
  34. package/dist/utils/errors.js +1 -2
  35. package/dist/utils/logger.js +1 -2
  36. package/dist/utils/media.js +43 -0
  37. package/dist/utils/platform.js +13 -0
  38. package/dist/utils/user-agent.js +1 -3
  39. package/dist/version.js +1 -2
  40. package/package.json +2 -1
  41. package/dist/realtime/diagnostics.d.ts +0 -78
  42. package/dist/realtime/webrtc-connection.js +0 -501
  43. package/dist/realtime/webrtc-manager.js +0 -189
  44. package/dist/realtime/webrtc-stats.d.ts +0 -59
  45. package/dist/realtime/webrtc-stats.js +0 -154
@@ -32,6 +32,5 @@ function createConsoleLogger(minLevel = "warn") {
32
32
  error: (msg, data) => log("error", msg, data)
33
33
  };
34
34
  }
35
-
36
35
  //#endregion
37
- export { createConsoleLogger, noopLogger };
36
+ export { createConsoleLogger, noopLogger };
@@ -0,0 +1,43 @@
1
+ //#region src/utils/media.ts
2
+ async function blobToBase64(blob) {
3
+ return new Promise((resolve, reject) => {
4
+ const reader = new FileReader();
5
+ reader.onloadend = () => {
6
+ const result = reader.result;
7
+ if (typeof result !== "string") {
8
+ reject(/* @__PURE__ */ new Error("FileReader did not return a string"));
9
+ return;
10
+ }
11
+ const base64 = result.split(",")[1];
12
+ if (!base64) {
13
+ reject(/* @__PURE__ */ new Error("Invalid data URL format"));
14
+ return;
15
+ }
16
+ resolve(base64);
17
+ };
18
+ reader.onerror = reject;
19
+ reader.readAsDataURL(blob);
20
+ });
21
+ }
22
+ async function imageToBase64(image) {
23
+ if (typeof image === "string") {
24
+ let url = null;
25
+ try {
26
+ url = new URL(image);
27
+ } catch {}
28
+ if (url?.protocol === "data:") {
29
+ const [, base64] = image.split(",", 2);
30
+ if (!base64) throw new Error("Invalid data URL image");
31
+ return base64;
32
+ }
33
+ if (url?.protocol === "http:" || url?.protocol === "https:") {
34
+ const response = await fetch(image);
35
+ if (!response.ok) throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
36
+ return blobToBase64(await response.blob());
37
+ }
38
+ return image;
39
+ }
40
+ return blobToBase64(image);
41
+ }
42
+ //#endregion
43
+ export { imageToBase64 };
@@ -0,0 +1,13 @@
1
+ //#region src/utils/platform.ts
2
+ function isDesktopSafari() {
3
+ const g = globalThis;
4
+ const ua = g?.navigator?.userAgent ?? "";
5
+ const platform = g?.navigator?.platform ?? "";
6
+ const maxTouchPoints = g?.navigator?.maxTouchPoints ?? 0;
7
+ if (!/^((?!chrome|chromium|crios|fxios|edg|firefox|opr|opera|android).)*safari/i.test(ua)) return false;
8
+ if (/iPad|iPhone|iPod/.test(ua)) return false;
9
+ if (platform === "MacIntel" && maxTouchPoints > 1) return false;
10
+ return true;
11
+ }
12
+ //#endregion
13
+ export { isDesktopSafari };
@@ -1,5 +1,4 @@
1
1
  import { VERSION } from "../version.js";
2
-
3
2
  //#region src/utils/user-agent.ts
4
3
  function getRuntimeEnvironment(globalThisAny = globalThis) {
5
4
  if (globalThisAny.window) return "runtime/browser";
@@ -27,6 +26,5 @@ function buildUserAgent(integration, globalThisAny = globalThis) {
27
26
  getRuntimeEnvironment(globalThisAny)
28
27
  ].join(" ");
29
28
  }
30
-
31
29
  //#endregion
32
- export { buildUserAgent };
30
+ export { buildUserAgent };
package/dist/version.js CHANGED
@@ -5,6 +5,5 @@
5
5
  * Falls back to '0.0.0-dev' in development.
6
6
  */
7
7
  const VERSION = typeof __PACKAGE_VERSION__ !== "undefined" ? __PACKAGE_VERSION__ : "0.0.0-dev";
8
-
9
8
  //#endregion
10
- export { VERSION };
9
+ export { VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.0.67",
3
+ "version": "0.1.0",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,6 +43,7 @@
43
43
  "vitest": "^4.0.18"
44
44
  },
45
45
  "dependencies": {
46
+ "livekit-client": "^2.0.0",
46
47
  "mitt": "^3.0.1",
47
48
  "p-retry": "^6.2.1",
48
49
  "zod": "^4.0.17"
@@ -1,78 +0,0 @@
1
- //#region src/realtime/diagnostics.d.ts
2
- /** Connection phase names for timing events. */
3
- type ConnectionPhase = "websocket" | "avatar-image" | "initial-prompt" | "webrtc-handshake" | "total";
4
- type PhaseTimingEvent = {
5
- phase: ConnectionPhase;
6
- durationMs: number;
7
- success: boolean;
8
- error?: string;
9
- };
10
- type IceCandidateEvent = {
11
- source: "local" | "remote";
12
- candidateType: "host" | "srflx" | "prflx" | "relay" | "unknown";
13
- protocol: "udp" | "tcp" | "unknown";
14
- address?: string;
15
- port?: number;
16
- };
17
- type IceStateEvent = {
18
- state: string;
19
- previousState: string;
20
- timestampMs: number;
21
- };
22
- type PeerConnectionStateEvent = {
23
- state: string;
24
- previousState: string;
25
- timestampMs: number;
26
- };
27
- type SignalingStateEvent = {
28
- state: string;
29
- previousState: string;
30
- timestampMs: number;
31
- };
32
- type SelectedCandidatePairEvent = {
33
- local: {
34
- candidateType: string;
35
- protocol: string;
36
- address?: string;
37
- port?: number;
38
- };
39
- remote: {
40
- candidateType: string;
41
- protocol: string;
42
- address?: string;
43
- port?: number;
44
- };
45
- };
46
- type ReconnectEvent = {
47
- attempt: number;
48
- maxAttempts: number;
49
- durationMs: number;
50
- success: boolean;
51
- error?: string;
52
- };
53
- type VideoStallEvent = {
54
- /** True when a stall is detected, false when recovered. */
55
- stalled: boolean;
56
- /** Duration of the stall in ms (0 when stall first detected, actual duration on recovery). */
57
- durationMs: number;
58
- };
59
- /** All diagnostic event types keyed by name. */
60
- type DiagnosticEvents = {
61
- phaseTiming: PhaseTimingEvent;
62
- iceCandidate: IceCandidateEvent;
63
- iceStateChange: IceStateEvent;
64
- peerConnectionStateChange: PeerConnectionStateEvent;
65
- signalingStateChange: SignalingStateEvent;
66
- selectedCandidatePair: SelectedCandidatePairEvent;
67
- reconnect: ReconnectEvent;
68
- videoStall: VideoStallEvent;
69
- };
70
- type DiagnosticEventName = keyof DiagnosticEvents;
71
- /** A single diagnostic event with its name and typed data. */
72
- type DiagnosticEvent = { [K in DiagnosticEventName]: {
73
- name: K;
74
- data: DiagnosticEvents[K];
75
- } }[DiagnosticEventName];
76
- /** Callback for emitting diagnostic events from the connection/manager layers. */
77
- //#endregion
78
- export { ConnectionPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, IceCandidateEvent, IceStateEvent, PeerConnectionStateEvent, PhaseTimingEvent, ReconnectEvent, SelectedCandidatePairEvent, SignalingStateEvent, VideoStallEvent };
@@ -1,501 +0,0 @@
1
- import { buildUserAgent } from "../utils/user-agent.js";
2
- import mitt from "mitt";
3
-
4
- //#region src/realtime/webrtc-connection.ts
5
- const ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
6
- const SETUP_TIMEOUT_MS = 3e4;
7
- const noopDiagnostic = () => {};
8
- var WebRTCConnection = class {
9
- pc = null;
10
- ws = null;
11
- localStream = null;
12
- connectionReject = null;
13
- logger;
14
- emitDiagnostic;
15
- state = "disconnected";
16
- websocketMessagesEmitter = mitt();
17
- constructor(callbacks = {}) {
18
- this.callbacks = callbacks;
19
- this.logger = callbacks.logger ?? {
20
- debug() {},
21
- info() {},
22
- warn() {},
23
- error() {}
24
- };
25
- this.emitDiagnostic = callbacks.onDiagnostic ?? noopDiagnostic;
26
- }
27
- getPeerConnection() {
28
- return this.pc;
29
- }
30
- async connect(url, localStream, timeout, integration) {
31
- const deadline = Date.now() + timeout;
32
- this.localStream = localStream;
33
- const userAgent = encodeURIComponent(buildUserAgent(integration));
34
- const wsUrl = `${url}${url.includes("?") ? "&" : "?"}user_agent=${userAgent}`;
35
- let rejectConnect;
36
- const connectAbort = new Promise((_, reject) => {
37
- rejectConnect = reject;
38
- });
39
- connectAbort.catch(() => {});
40
- this.connectionReject = (error) => rejectConnect(error);
41
- const totalStart = performance.now();
42
- try {
43
- const wsStart = performance.now();
44
- await Promise.race([new Promise((resolve, reject) => {
45
- const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("WebSocket timeout")), timeout);
46
- this.ws = new WebSocket(wsUrl);
47
- this.ws.onopen = () => {
48
- clearTimeout(timer);
49
- this.emitDiagnostic("phaseTiming", {
50
- phase: "websocket",
51
- durationMs: performance.now() - wsStart,
52
- success: true
53
- });
54
- resolve();
55
- };
56
- this.ws.onmessage = (e) => {
57
- try {
58
- this.handleSignalingMessage(JSON.parse(e.data));
59
- } catch (err) {
60
- this.logger.error("Signaling message parse error", { error: String(err) });
61
- }
62
- };
63
- this.ws.onerror = () => {
64
- clearTimeout(timer);
65
- const error = /* @__PURE__ */ new Error("WebSocket error");
66
- this.emitDiagnostic("phaseTiming", {
67
- phase: "websocket",
68
- durationMs: performance.now() - wsStart,
69
- success: false,
70
- error: error.message
71
- });
72
- reject(error);
73
- rejectConnect(error);
74
- };
75
- this.ws.onclose = () => {
76
- this.setState("disconnected");
77
- clearTimeout(timer);
78
- reject(/* @__PURE__ */ new Error("WebSocket closed before connection was established"));
79
- rejectConnect(/* @__PURE__ */ new Error("WebSocket closed"));
80
- };
81
- }), connectAbort]);
82
- if (this.callbacks.initialImage) {
83
- const imageStart = performance.now();
84
- await Promise.race([this.setImageBase64(this.callbacks.initialImage, {
85
- prompt: this.callbacks.initialPrompt?.text,
86
- enhance: this.callbacks.initialPrompt?.enhance
87
- }), connectAbort]);
88
- this.emitDiagnostic("phaseTiming", {
89
- phase: "avatar-image",
90
- durationMs: performance.now() - imageStart,
91
- success: true
92
- });
93
- } else if (this.callbacks.initialPrompt) {
94
- const promptStart = performance.now();
95
- await Promise.race([this.sendInitialPrompt(this.callbacks.initialPrompt), connectAbort]);
96
- this.emitDiagnostic("phaseTiming", {
97
- phase: "initial-prompt",
98
- durationMs: performance.now() - promptStart,
99
- success: true
100
- });
101
- } else if (localStream) {
102
- const nullStart = performance.now();
103
- await Promise.race([this.setImageBase64(null, { prompt: null }), connectAbort]);
104
- this.emitDiagnostic("phaseTiming", {
105
- phase: "initial-prompt",
106
- durationMs: performance.now() - nullStart,
107
- success: true
108
- });
109
- }
110
- const handshakeStart = performance.now();
111
- await this.setupNewPeerConnection();
112
- await Promise.race([new Promise((resolve, reject) => {
113
- const checkConnection = setInterval(() => {
114
- if (this.state === "connected" || this.state === "generating") {
115
- clearInterval(checkConnection);
116
- this.emitDiagnostic("phaseTiming", {
117
- phase: "webrtc-handshake",
118
- durationMs: performance.now() - handshakeStart,
119
- success: true
120
- });
121
- resolve();
122
- } else if (this.state === "disconnected") {
123
- clearInterval(checkConnection);
124
- this.emitDiagnostic("phaseTiming", {
125
- phase: "webrtc-handshake",
126
- durationMs: performance.now() - handshakeStart,
127
- success: false,
128
- error: "Connection lost during handshake"
129
- });
130
- reject(/* @__PURE__ */ new Error("Connection lost during WebRTC handshake"));
131
- } else if (Date.now() >= deadline) {
132
- clearInterval(checkConnection);
133
- this.emitDiagnostic("phaseTiming", {
134
- phase: "webrtc-handshake",
135
- durationMs: performance.now() - handshakeStart,
136
- success: false,
137
- error: "Timeout"
138
- });
139
- reject(/* @__PURE__ */ new Error("Connection timeout"));
140
- }
141
- }, 100);
142
- connectAbort.catch(() => clearInterval(checkConnection));
143
- }), connectAbort]);
144
- this.emitDiagnostic("phaseTiming", {
145
- phase: "total",
146
- durationMs: performance.now() - totalStart,
147
- success: true
148
- });
149
- } finally {
150
- this.connectionReject = null;
151
- }
152
- }
153
- async handleSignalingMessage(msg) {
154
- try {
155
- if (msg.type === "error") {
156
- const error = new Error(msg.error);
157
- error.source = "server";
158
- this.callbacks.onError?.(error);
159
- if (this.connectionReject) {
160
- this.connectionReject(error);
161
- this.connectionReject = null;
162
- }
163
- return;
164
- }
165
- if (msg.type === "set_image_ack") {
166
- this.websocketMessagesEmitter.emit("setImageAck", msg);
167
- return;
168
- }
169
- if (msg.type === "prompt_ack") {
170
- this.websocketMessagesEmitter.emit("promptAck", msg);
171
- return;
172
- }
173
- if (msg.type === "generation_started") {
174
- this.setState("generating");
175
- return;
176
- }
177
- if (msg.type === "generation_tick") {
178
- this.websocketMessagesEmitter.emit("generationTick", msg);
179
- return;
180
- }
181
- if (msg.type === "generation_ended") return;
182
- if (msg.type === "session_id") {
183
- this.websocketMessagesEmitter.emit("sessionId", msg);
184
- return;
185
- }
186
- if (!this.pc) return;
187
- switch (msg.type) {
188
- case "ready": {
189
- await this.applyCodecPreference("video/VP8");
190
- const offer = await this.pc.createOffer();
191
- this.modifyVP8Bitrate(offer);
192
- await this.callbacks.customizeOffer?.(offer);
193
- await this.pc.setLocalDescription(offer);
194
- this.send({
195
- type: "offer",
196
- sdp: offer.sdp || ""
197
- });
198
- break;
199
- }
200
- case "offer": {
201
- await this.pc.setRemoteDescription({
202
- type: "offer",
203
- sdp: msg.sdp
204
- });
205
- const answer = await this.pc.createAnswer();
206
- await this.pc.setLocalDescription(answer);
207
- this.send({
208
- type: "answer",
209
- sdp: answer.sdp || ""
210
- });
211
- break;
212
- }
213
- case "answer":
214
- await this.pc.setRemoteDescription({
215
- type: "answer",
216
- sdp: msg.sdp
217
- });
218
- break;
219
- case "ice-candidate":
220
- if (msg.candidate) {
221
- await this.pc.addIceCandidate(msg.candidate);
222
- this.emitDiagnostic("iceCandidate", {
223
- source: "remote",
224
- candidateType: msg.candidate.candidate?.match(/typ (\w+)/)?.[1] ?? "unknown",
225
- protocol: msg.candidate.candidate?.match(/udp|tcp/i)?.[0]?.toLowerCase() ?? "unknown"
226
- });
227
- }
228
- break;
229
- }
230
- } catch (error) {
231
- this.logger.error("Signaling handler error", { error: String(error) });
232
- this.callbacks.onError?.(error);
233
- this.connectionReject?.(error);
234
- }
235
- }
236
- send(message) {
237
- if (this.ws?.readyState === WebSocket.OPEN) {
238
- this.ws.send(JSON.stringify(message));
239
- return true;
240
- }
241
- this.logger.warn("Message dropped: WebSocket is not open");
242
- return false;
243
- }
244
- async setImageBase64(imageBase64, options) {
245
- return new Promise((resolve, reject) => {
246
- const timeoutId = setTimeout(() => {
247
- this.websocketMessagesEmitter.off("setImageAck", listener);
248
- reject(/* @__PURE__ */ new Error("Image send timed out"));
249
- }, options?.timeout ?? SETUP_TIMEOUT_MS);
250
- const listener = (msg) => {
251
- clearTimeout(timeoutId);
252
- this.websocketMessagesEmitter.off("setImageAck", listener);
253
- if (msg.success) resolve();
254
- else reject(new Error(msg.error ?? "Failed to send image"));
255
- };
256
- this.websocketMessagesEmitter.on("setImageAck", listener);
257
- const message = {
258
- type: "set_image",
259
- image_data: imageBase64
260
- };
261
- if (options?.prompt !== void 0) message.prompt = options.prompt;
262
- if (options?.enhance !== void 0) message.enhance_prompt = options.enhance;
263
- if (!this.send(message)) {
264
- clearTimeout(timeoutId);
265
- this.websocketMessagesEmitter.off("setImageAck", listener);
266
- reject(/* @__PURE__ */ new Error("WebSocket is not open"));
267
- }
268
- });
269
- }
270
- /**
271
- * Send the initial prompt to the server before WebRTC handshake.
272
- */
273
- async sendInitialPrompt(prompt) {
274
- return new Promise((resolve, reject) => {
275
- const timeoutId = setTimeout(() => {
276
- this.websocketMessagesEmitter.off("promptAck", listener);
277
- reject(/* @__PURE__ */ new Error("Prompt send timed out"));
278
- }, SETUP_TIMEOUT_MS);
279
- const listener = (msg) => {
280
- if (msg.prompt === prompt.text) {
281
- clearTimeout(timeoutId);
282
- this.websocketMessagesEmitter.off("promptAck", listener);
283
- if (msg.success) resolve();
284
- else reject(new Error(msg.error ?? "Failed to send prompt"));
285
- }
286
- };
287
- this.websocketMessagesEmitter.on("promptAck", listener);
288
- if (!this.send({
289
- type: "prompt",
290
- prompt: prompt.text,
291
- enhance_prompt: prompt.enhance ?? true
292
- })) {
293
- clearTimeout(timeoutId);
294
- this.websocketMessagesEmitter.off("promptAck", listener);
295
- reject(/* @__PURE__ */ new Error("WebSocket is not open"));
296
- }
297
- });
298
- }
299
- setState(state) {
300
- if (this.state !== state) {
301
- this.state = state;
302
- this.callbacks.onStateChange?.(state);
303
- }
304
- }
305
- async setupNewPeerConnection() {
306
- if (this.pc) {
307
- this.pc.getSenders().forEach((sender) => {
308
- if (sender.track && this.pc) this.pc.removeTrack(sender);
309
- });
310
- this.pc.close();
311
- }
312
- this.pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
313
- this.setState("connecting");
314
- if (this.localStream) this.localStream.getTracks().forEach((track) => {
315
- if (this.pc && this.localStream) this.pc.addTrack(track, this.localStream);
316
- });
317
- else {
318
- this.pc.addTransceiver("video", { direction: "recvonly" });
319
- this.pc.addTransceiver("audio", { direction: "recvonly" });
320
- }
321
- let fallbackStream = null;
322
- this.pc.ontrack = (e) => {
323
- if (e.streams?.[0]) this.callbacks.onRemoteStream?.(e.streams[0]);
324
- else {
325
- if (!fallbackStream) fallbackStream = new MediaStream();
326
- fallbackStream.addTrack(e.track);
327
- this.callbacks.onRemoteStream?.(fallbackStream);
328
- }
329
- };
330
- this.pc.onicecandidate = (e) => {
331
- this.send({
332
- type: "ice-candidate",
333
- candidate: e.candidate
334
- });
335
- if (e.candidate) this.emitDiagnostic("iceCandidate", {
336
- source: "local",
337
- candidateType: e.candidate.type ?? "unknown",
338
- protocol: e.candidate.protocol ?? "unknown",
339
- address: e.candidate.address ?? void 0,
340
- port: e.candidate.port ?? void 0
341
- });
342
- };
343
- let prevPcState = "new";
344
- this.pc.onconnectionstatechange = () => {
345
- if (!this.pc) return;
346
- const s = this.pc.connectionState;
347
- this.emitDiagnostic("peerConnectionStateChange", {
348
- state: s,
349
- previousState: prevPcState,
350
- timestampMs: performance.now()
351
- });
352
- prevPcState = s;
353
- if (s === "connected") this.emitSelectedCandidatePair();
354
- const nextState = s === "connected" ? "connected" : ["connecting", "new"].includes(s) ? "connecting" : "disconnected";
355
- if (this.state === "generating" && nextState !== "disconnected") return;
356
- this.setState(nextState);
357
- };
358
- let prevIceState = "new";
359
- this.pc.oniceconnectionstatechange = () => {
360
- if (!this.pc) return;
361
- const newIceState = this.pc.iceConnectionState;
362
- this.emitDiagnostic("iceStateChange", {
363
- state: newIceState,
364
- previousState: prevIceState,
365
- timestampMs: performance.now()
366
- });
367
- prevIceState = newIceState;
368
- if (newIceState === "failed") {
369
- this.setState("disconnected");
370
- this.callbacks.onError?.(/* @__PURE__ */ new Error("ICE connection failed"));
371
- }
372
- };
373
- let prevSignalingState = "stable";
374
- this.pc.onsignalingstatechange = () => {
375
- if (!this.pc) return;
376
- const newState = this.pc.signalingState;
377
- this.emitDiagnostic("signalingStateChange", {
378
- state: newState,
379
- previousState: prevSignalingState,
380
- timestampMs: performance.now()
381
- });
382
- prevSignalingState = newState;
383
- };
384
- this.handleSignalingMessage({ type: "ready" });
385
- }
386
- async emitSelectedCandidatePair() {
387
- if (!this.pc) return;
388
- try {
389
- const stats = await this.pc.getStats();
390
- let found = false;
391
- stats.forEach((report) => {
392
- if (found) return;
393
- if (report.type === "candidate-pair" && report.state === "succeeded") {
394
- found = true;
395
- let localCandidate;
396
- let remoteCandidate;
397
- stats.forEach((r) => {
398
- if (r.id === report.localCandidateId) localCandidate = r;
399
- if (r.id === report.remoteCandidateId) remoteCandidate = r;
400
- });
401
- if (localCandidate && remoteCandidate) this.emitDiagnostic("selectedCandidatePair", {
402
- local: {
403
- candidateType: String(localCandidate.candidateType ?? "unknown"),
404
- protocol: String(localCandidate.protocol ?? "unknown"),
405
- address: localCandidate.address,
406
- port: localCandidate.port
407
- },
408
- remote: {
409
- candidateType: String(remoteCandidate.candidateType ?? "unknown"),
410
- protocol: String(remoteCandidate.protocol ?? "unknown"),
411
- address: remoteCandidate.address,
412
- port: remoteCandidate.port
413
- }
414
- });
415
- }
416
- });
417
- } catch {}
418
- }
419
- cleanup() {
420
- this.pc?.close();
421
- this.pc = null;
422
- this.ws?.close();
423
- this.ws = null;
424
- this.localStream = null;
425
- this.setState("disconnected");
426
- }
427
- applyCodecPreference(preferredCodecName) {
428
- if (!this.pc) return;
429
- if (typeof RTCRtpSender === "undefined" || typeof RTCRtpSender.getCapabilities !== "function") {
430
- this.logger.debug("RTCRtpSender capabilities not available in this environment");
431
- return;
432
- }
433
- const videoTransceiver = this.pc.getTransceivers().find((r) => r.sender.track?.kind === "video" || r.receiver.track?.kind === "video");
434
- if (!videoTransceiver) {
435
- this.logger.warn("Video transceiver not found for codec preference");
436
- return;
437
- }
438
- const capabilities = RTCRtpSender.getCapabilities("video");
439
- if (!capabilities) {
440
- this.logger.warn("Video sender capabilities unavailable");
441
- return;
442
- }
443
- const preferredCodecs = [];
444
- const otherCodecs = [];
445
- capabilities.codecs.forEach((codec) => {
446
- if (codec.mimeType.toLowerCase() === preferredCodecName.toLowerCase()) preferredCodecs.push(codec);
447
- else otherCodecs.push(codec);
448
- });
449
- const orderedCodecs = [...preferredCodecs, ...otherCodecs];
450
- if (orderedCodecs.length === 0) {
451
- this.logger.debug("No video codecs found for preference setting");
452
- return;
453
- }
454
- try {
455
- videoTransceiver.setCodecPreferences(orderedCodecs);
456
- } catch {
457
- this.logger.debug("setCodecPreferences not supported, skipping");
458
- }
459
- }
460
- modifyVP8Bitrate(offer) {
461
- if (!offer.sdp) return;
462
- const minBitrateInKbps = this.callbacks.vp8MinBitrate;
463
- const startBitrateInKbps = this.callbacks.vp8StartBitrate;
464
- if (minBitrateInKbps === void 0 || startBitrateInKbps === void 0) return;
465
- if (minBitrateInKbps === 0 && startBitrateInKbps === 0) return;
466
- const bitrateParams = `x-google-min-bitrate=${minBitrateInKbps};x-google-start-bitrate=${startBitrateInKbps}`;
467
- const sdpLines = offer.sdp.split("\r\n");
468
- const modifiedLines = [];
469
- for (let i = 0; i < sdpLines.length; i++) {
470
- if (sdpLines[i].includes("VP8/90000")) {
471
- const match = sdpLines[i].match(/a=rtpmap:(\d+) VP8/);
472
- if (match) {
473
- const payloadType = match[1];
474
- let fmtpIndex = -1;
475
- let insertAfterIndex = i;
476
- for (let j = i + 1; j < sdpLines.length && sdpLines[j].startsWith("a="); j++) {
477
- if (sdpLines[j].startsWith(`a=fmtp:${payloadType}`)) {
478
- fmtpIndex = j;
479
- break;
480
- }
481
- if (sdpLines[j].startsWith(`a=rtcp-fb:${payloadType}`)) insertAfterIndex = j;
482
- if (sdpLines[j].startsWith("a=rtpmap:")) break;
483
- }
484
- if (fmtpIndex !== -1) {
485
- if (!sdpLines[fmtpIndex].includes("x-google-min-bitrate")) sdpLines[fmtpIndex] += `;${bitrateParams}`;
486
- } else {
487
- for (let k = i; k <= insertAfterIndex; k++) modifiedLines.push(sdpLines[k]);
488
- modifiedLines.push(`a=fmtp:${payloadType} ${bitrateParams}`);
489
- i = insertAfterIndex;
490
- continue;
491
- }
492
- }
493
- }
494
- modifiedLines.push(sdpLines[i]);
495
- }
496
- offer.sdp = modifiedLines.join("\r\n");
497
- }
498
- };
499
-
500
- //#endregion
501
- export { WebRTCConnection };