@decartai/sdk 0.0.42 → 0.0.44

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/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@ import { ProcessClient } from "./process/client.js";
4
4
  import { JobStatus, JobStatusResponse, JobSubmitResponse, QueueJobResult, QueueSubmitAndPollOptions, QueueSubmitOptions } from "./queue/types.js";
5
5
  import { QueueClient } from "./queue/client.js";
6
6
  import { DecartSDKError, ERROR_CODES } from "./utils/errors.js";
7
- import { AvatarOptions, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState } from "./realtime/client.js";
7
+ import { ConnectionState } from "./realtime/webrtc-connection.js";
8
+ import { SetInput } from "./realtime/methods.js";
9
+ import { AvatarOptions, Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState } from "./realtime/client.js";
8
10
  import { ModelState } from "./shared/types.js";
9
11
  import { CreateTokenResponse, TokensClient } from "./tokens/client.js";
10
12
 
@@ -117,4 +119,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
117
119
  tokens: TokensClient;
118
120
  };
119
121
  //#endregion
120
- export { type AvatarOptions, type CreateTokenResponse, DecartClientOptions, type DecartSDKError, ERROR_CODES, type FileInput, type ImageModelDefinition, type ImageModels, type JobStatus, type JobStatusResponse, type JobSubmitResponse, type Model, type ModelDefinition, type ModelState, type ProcessClient, type ProcessOptions, type QueueClient, type QueueJobResult, type QueueSubmitAndPollOptions, type QueueSubmitOptions, type ReactNativeFile, type RealTimeClient, type RealTimeClientConnectOptions, type RealTimeClientInitialState, type RealTimeModels, type TokensClient, type VideoModelDefinition, type VideoModels, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models };
122
+ export { type AvatarOptions, type ConnectionState, type CreateTokenResponse, DecartClientOptions, type DecartSDKError, ERROR_CODES, type FileInput, type ImageModelDefinition, type ImageModels, type JobStatus, type JobStatusResponse, type JobSubmitResponse, type Model, type ModelDefinition, type ModelState, type ProcessClient, type ProcessOptions, type QueueClient, type QueueJobResult, type QueueSubmitAndPollOptions, type QueueSubmitOptions, type ReactNativeFile, type RealTimeClient, type RealTimeClientConnectOptions, type RealTimeClientInitialState, type Events as RealTimeEvents, type RealTimeModels, type SetInput, type TokensClient, type VideoModelDefinition, type VideoModels, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models };
@@ -82,7 +82,7 @@ var AudioStreamManager = class {
82
82
  try {
83
83
  this.silenceOscillator.stop();
84
84
  } catch {}
85
- this.audioContext.close();
85
+ this.audioContext.close().catch(() => {});
86
86
  }
87
87
  };
88
88
 
@@ -1,4 +1,5 @@
1
1
  import { DecartSDKError } from "../utils/errors.js";
2
+ import { SetInput } from "./methods.js";
2
3
  import { z } from "zod";
3
4
 
4
5
  //#region src/realtime/client.d.ts
@@ -39,17 +40,18 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
39
40
  }, z.core.$strip>;
40
41
  type RealTimeClientConnectOptions = z.infer<typeof realTimeClientConnectOptionsSchema>;
41
42
  type Events = {
42
- connectionChange: "connected" | "connecting" | "disconnected";
43
+ connectionChange: "connected" | "connecting" | "disconnected" | "reconnecting";
43
44
  error: DecartSDKError;
44
45
  };
45
46
  type RealTimeClient = {
47
+ set: (input: SetInput) => Promise<void>;
46
48
  setPrompt: (prompt: string, {
47
49
  enhance
48
50
  }?: {
49
51
  enhance?: boolean;
50
52
  }) => Promise<void>;
51
53
  isConnected: () => boolean;
52
- getConnectionState: () => "connected" | "connecting" | "disconnected";
54
+ getConnectionState: () => "connected" | "connecting" | "disconnected" | "reconnecting";
53
55
  disconnect: () => void;
54
56
  on: <K extends keyof Events>(event: K, listener: (data: Events[K]) => void) => void;
55
57
  off: <K extends keyof Events>(event: K, listener: (data: Events[K]) => void) => void;
@@ -62,4 +64,4 @@ type RealTimeClient = {
62
64
  playAudio?: (audio: Blob | File | ArrayBuffer) => Promise<void>;
63
65
  };
64
66
  //#endregion
65
- export { AvatarOptions, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState };
67
+ export { AvatarOptions, Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState };
@@ -13,13 +13,42 @@ async function blobToBase64(blob) {
13
13
  return new Promise((resolve, reject) => {
14
14
  const reader = new FileReader();
15
15
  reader.onloadend = () => {
16
- const base64 = reader.result.split(",")[1];
16
+ const result = reader.result;
17
+ if (typeof result !== "string") {
18
+ reject(/* @__PURE__ */ new Error("FileReader did not return a string"));
19
+ return;
20
+ }
21
+ const base64 = result.split(",")[1];
22
+ if (!base64) {
23
+ reject(/* @__PURE__ */ new Error("Invalid data URL format"));
24
+ return;
25
+ }
17
26
  resolve(base64);
18
27
  };
19
28
  reader.onerror = reject;
20
29
  reader.readAsDataURL(blob);
21
30
  });
22
31
  }
32
+ async function imageToBase64(image) {
33
+ if (typeof image === "string") {
34
+ let url = null;
35
+ try {
36
+ url = new URL(image);
37
+ } catch {}
38
+ if (url?.protocol === "data:") {
39
+ const [, base64] = image.split(",", 2);
40
+ if (!base64) throw new Error("Invalid data URL image");
41
+ return base64;
42
+ }
43
+ if (url?.protocol === "http:" || url?.protocol === "https:") {
44
+ const response = await fetch(image);
45
+ if (!response.ok) throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
46
+ return blobToBase64(await response.blob());
47
+ }
48
+ return image;
49
+ }
50
+ return blobToBase64(image);
51
+ }
23
52
  const realTimeClientInitialStateSchema = modelStateSchema;
24
53
  const createAsyncFunctionSchema = (schema) => z.custom((fn) => schema.implementAsync(fn));
25
54
  const avatarOptionsSchema = z.object({ avatarImage: z.union([
@@ -49,76 +78,71 @@ const createRealTimeClient = (opts) => {
49
78
  audioStreamManager = new AudioStreamManager();
50
79
  inputStream = audioStreamManager.getStream();
51
80
  } else inputStream = stream ?? new MediaStream();
52
- let avatarImageBase64;
53
- if (isAvatarLive && avatar?.avatarImage) {
54
- let imageBlob;
55
- if (typeof avatar.avatarImage === "string") imageBlob = await (await fetch(avatar.avatarImage)).blob();
56
- else imageBlob = avatar.avatarImage;
57
- avatarImageBase64 = await blobToBase64(imageBlob);
58
- }
59
- const initialPrompt = isAvatarLive && options.initialState?.prompt ? {
60
- text: options.initialState.prompt.text,
61
- enhance: options.initialState.prompt.enhance
62
- } : void 0;
63
- const webrtcManager = new WebRTCManager({
64
- webrtcUrl: `${`${baseUrl}${options.model.urlPath}`}?api_key=${apiKey}&model=${options.model.name}`,
65
- apiKey,
66
- sessionId,
67
- fps: options.model.fps,
68
- initialState,
69
- integration,
70
- onRemoteStream,
71
- onConnectionStateChange: (state) => {
72
- eventEmitter.emit("connectionChange", state);
73
- },
74
- onError: (error) => {
75
- console.error("WebRTC error:", error);
76
- eventEmitter.emit("error", createWebrtcError(error));
77
- },
78
- customizeOffer: options.customizeOffer,
79
- vp8MinBitrate: 300,
80
- vp8StartBitrate: 600,
81
- isAvatarLive,
82
- avatarImageBase64,
83
- initialPrompt
84
- });
85
- await webrtcManager.connect(inputStream);
86
- const methods = realtimeMethods(webrtcManager);
87
- if (!isAvatarLive && options.initialState?.prompt) {
88
- const { text, enhance } = options.initialState.prompt;
89
- methods.setPrompt(text, { enhance });
90
- }
91
- const client = {
92
- setPrompt: methods.setPrompt,
93
- isConnected: () => webrtcManager.isConnected(),
94
- getConnectionState: () => webrtcManager.getConnectionState(),
95
- disconnect: () => {
96
- webrtcManager.cleanup();
97
- audioStreamManager?.cleanup();
98
- },
99
- on: eventEmitter.on,
100
- off: eventEmitter.off,
101
- sessionId,
102
- setImage: async (image, options$1) => {
103
- if (image === null) return webrtcManager.setImage(null, options$1);
104
- let imageBase64;
105
- if (typeof image === "string") {
106
- let url = null;
107
- try {
108
- url = new URL(image);
109
- } catch {}
110
- if (url?.protocol === "data:") imageBase64 = image.split(",")[1];
111
- else if (url?.protocol === "http:" || url?.protocol === "https:") imageBase64 = await blobToBase64(await (await fetch(image)).blob());
112
- else imageBase64 = image;
113
- } else imageBase64 = await blobToBase64(image);
114
- return webrtcManager.setImage(imageBase64, options$1);
81
+ let webrtcManager;
82
+ try {
83
+ let avatarImageBase64;
84
+ if (isAvatarLive && avatar?.avatarImage) if (typeof avatar.avatarImage === "string") {
85
+ const response = await fetch(avatar.avatarImage);
86
+ if (!response.ok) throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
87
+ avatarImageBase64 = await blobToBase64(await response.blob());
88
+ } else avatarImageBase64 = await blobToBase64(avatar.avatarImage);
89
+ const initialPrompt = isAvatarLive && initialState?.prompt ? {
90
+ text: initialState.prompt.text,
91
+ enhance: initialState.prompt.enhance
92
+ } : void 0;
93
+ webrtcManager = new WebRTCManager({
94
+ webrtcUrl: `${`${baseUrl}${options.model.urlPath}`}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}`,
95
+ integration,
96
+ onRemoteStream,
97
+ onConnectionStateChange: (state) => {
98
+ eventEmitter.emit("connectionChange", state);
99
+ },
100
+ onError: (error) => {
101
+ console.error("WebRTC error:", error);
102
+ eventEmitter.emit("error", createWebrtcError(error));
103
+ },
104
+ customizeOffer: options.customizeOffer,
105
+ vp8MinBitrate: 300,
106
+ vp8StartBitrate: 600,
107
+ isAvatarLive,
108
+ avatarImageBase64,
109
+ initialPrompt
110
+ });
111
+ const manager = webrtcManager;
112
+ await manager.connect(inputStream);
113
+ const methods = realtimeMethods(manager, imageToBase64);
114
+ if (!isAvatarLive && initialState?.prompt) {
115
+ const { text, enhance } = initialState.prompt;
116
+ await methods.setPrompt(text, { enhance });
115
117
  }
116
- };
117
- if (isAvatarLive && audioStreamManager) {
118
- const manager = audioStreamManager;
119
- client.playAudio = (audio) => manager.playAudio(audio);
118
+ const client = {
119
+ set: methods.set,
120
+ setPrompt: methods.setPrompt,
121
+ isConnected: () => manager.isConnected(),
122
+ getConnectionState: () => manager.getConnectionState(),
123
+ disconnect: () => {
124
+ manager.cleanup();
125
+ audioStreamManager?.cleanup();
126
+ },
127
+ on: eventEmitter.on,
128
+ off: eventEmitter.off,
129
+ sessionId,
130
+ setImage: async (image, options$1) => {
131
+ if (image === null) return manager.setImage(null, options$1);
132
+ const base64 = await imageToBase64(image);
133
+ return manager.setImage(base64, options$1);
134
+ }
135
+ };
136
+ if (isAvatarLive && audioStreamManager) {
137
+ const manager$1 = audioStreamManager;
138
+ client.playAudio = (audio) => manager$1.playAudio(audio);
139
+ }
140
+ return client;
141
+ } catch (error) {
142
+ webrtcManager?.cleanup();
143
+ audioStreamManager?.cleanup();
144
+ throw error;
120
145
  }
121
- return client;
122
146
  };
123
147
  return { connect };
124
148
  };
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/realtime/methods.d.ts
4
+ declare const setInputSchema: z.ZodObject<{
5
+ prompt: z.ZodOptional<z.ZodString>;
6
+ enhance: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
7
+ image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString, z.ZodNull]>>;
8
+ }, z.core.$strip>;
9
+ type SetInput = z.input<typeof setInputSchema>;
10
+ //#endregion
11
+ export { SetInput };
@@ -2,12 +2,42 @@ import { z } from "zod";
2
2
 
3
3
  //#region src/realtime/methods.ts
4
4
  const PROMPT_TIMEOUT_MS = 15 * 1e3;
5
- const realtimeMethods = (webrtcManager) => {
5
+ const UPDATE_TIMEOUT_MS = 30 * 1e3;
6
+ const setInputSchema = z.object({
7
+ prompt: z.string().min(1).optional(),
8
+ enhance: z.boolean().optional().default(true),
9
+ image: z.union([
10
+ z.instanceof(Blob),
11
+ z.instanceof(File),
12
+ z.string(),
13
+ z.null()
14
+ ]).optional()
15
+ }).refine((data) => data.prompt !== void 0 || data.image !== void 0, { message: "At least one of 'prompt' or 'image' must be provided" });
16
+ const setPromptInputSchema = z.object({
17
+ prompt: z.string().min(1),
18
+ enhance: z.boolean().optional().default(true)
19
+ });
20
+ const realtimeMethods = (webrtcManager, imageToBase64) => {
21
+ const assertConnected = () => {
22
+ const state = webrtcManager.getConnectionState();
23
+ if (state !== "connected") throw new Error(`Cannot send message: connection is ${state}`);
24
+ };
25
+ const set = async (input) => {
26
+ assertConnected();
27
+ const parsed = setInputSchema.safeParse(input);
28
+ if (!parsed.success) throw parsed.error;
29
+ const { prompt, enhance, image } = parsed.data;
30
+ let imageBase64 = null;
31
+ if (image !== void 0 && image !== null) imageBase64 = await imageToBase64(image);
32
+ await webrtcManager.setImage(imageBase64, {
33
+ prompt,
34
+ enhance,
35
+ timeout: UPDATE_TIMEOUT_MS
36
+ });
37
+ };
6
38
  const setPrompt = async (prompt, { enhance } = {}) => {
7
- const parsedInput = z.object({
8
- prompt: z.string().min(1),
9
- enhance: z.boolean().optional().default(true)
10
- }).safeParse({
39
+ assertConnected();
40
+ const parsedInput = setPromptInputSchema.safeParse({
11
41
  prompt,
12
42
  enhance
13
43
  });
@@ -19,15 +49,15 @@ const realtimeMethods = (webrtcManager) => {
19
49
  const ackPromise = new Promise((resolve, reject) => {
20
50
  promptAckListener = (promptAckMessage) => {
21
51
  if (promptAckMessage.prompt === parsedInput.data.prompt) if (promptAckMessage.success) resolve();
22
- else reject(promptAckMessage.error);
52
+ else reject(new Error(promptAckMessage.error ?? "Failed to send prompt"));
23
53
  };
24
54
  emitter.on("promptAck", promptAckListener);
25
55
  });
26
- webrtcManager.sendMessage({
56
+ if (!webrtcManager.sendMessage({
27
57
  type: "prompt",
28
58
  prompt: parsedInput.data.prompt,
29
59
  enhance_prompt: parsedInput.data.enhance
30
- });
60
+ })) throw new Error("WebSocket is not open");
31
61
  const timeoutPromise = new Promise((_, reject) => {
32
62
  timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("Prompt timed out")), PROMPT_TIMEOUT_MS);
33
63
  });
@@ -37,7 +67,10 @@ const realtimeMethods = (webrtcManager) => {
37
67
  if (timeoutId) clearTimeout(timeoutId);
38
68
  }
39
69
  };
40
- return { setPrompt };
70
+ return {
71
+ set,
72
+ setPrompt
73
+ };
41
74
  };
42
75
 
43
76
  //#endregion
@@ -0,0 +1,7 @@
1
+ import "mitt";
2
+
3
+ //#region src/realtime/webrtc-connection.d.ts
4
+
5
+ type ConnectionState = "connecting" | "connected" | "disconnected" | "reconnecting";
6
+ //#endregion
7
+ export { ConnectionState };
@@ -19,43 +19,61 @@ var WebRTCConnection = class {
19
19
  this.localStream = localStream;
20
20
  const userAgent = encodeURIComponent(buildUserAgent(integration));
21
21
  const wsUrl = `${url}${url.includes("?") ? "&" : "?"}user_agent=${userAgent}`;
22
- await new Promise((resolve, reject) => {
23
- this.connectionReject = reject;
24
- const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("WebSocket timeout")), timeout);
25
- this.ws = new WebSocket(wsUrl);
26
- this.ws.onopen = () => {
27
- clearTimeout(timer);
28
- resolve();
29
- };
30
- this.ws.onmessage = (e) => {
31
- try {
32
- this.handleSignalingMessage(JSON.parse(e.data));
33
- } catch (err) {
34
- console.error("[WebRTC] Parse error:", err);
35
- }
36
- };
37
- this.ws.onerror = () => {
38
- clearTimeout(timer);
39
- };
40
- this.ws.onclose = () => this.setState("disconnected");
22
+ let rejectConnect;
23
+ const connectAbort = new Promise((_, reject) => {
24
+ rejectConnect = reject;
41
25
  });
42
- if (this.callbacks.avatarImageBase64) await this.sendAvatarImage(this.callbacks.avatarImageBase64);
43
- if (this.callbacks.initialPrompt) await this.sendInitialPrompt(this.callbacks.initialPrompt);
44
- await this.setupNewPeerConnection();
45
- return new Promise((resolve, reject) => {
46
- this.connectionReject = reject;
47
- const checkConnection = setInterval(() => {
48
- if (this.state === "connected") {
49
- clearInterval(checkConnection);
50
- this.connectionReject = null;
26
+ connectAbort.catch(() => {});
27
+ this.connectionReject = (error) => rejectConnect(error);
28
+ try {
29
+ await Promise.race([new Promise((resolve, reject) => {
30
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("WebSocket timeout")), timeout);
31
+ this.ws = new WebSocket(wsUrl);
32
+ this.ws.onopen = () => {
33
+ clearTimeout(timer);
51
34
  resolve();
52
- } else if (Date.now() >= deadline) {
53
- clearInterval(checkConnection);
54
- this.connectionReject = null;
55
- reject(/* @__PURE__ */ new Error("Connection timeout"));
56
- }
57
- }, 100);
58
- });
35
+ };
36
+ this.ws.onmessage = (e) => {
37
+ try {
38
+ this.handleSignalingMessage(JSON.parse(e.data));
39
+ } catch (err) {
40
+ console.error("[WebRTC] Parse error:", err);
41
+ }
42
+ };
43
+ this.ws.onerror = () => {
44
+ clearTimeout(timer);
45
+ const error = /* @__PURE__ */ new Error("WebSocket error");
46
+ reject(error);
47
+ rejectConnect(error);
48
+ };
49
+ this.ws.onclose = () => {
50
+ this.setState("disconnected");
51
+ clearTimeout(timer);
52
+ reject(/* @__PURE__ */ new Error("WebSocket closed before connection was established"));
53
+ rejectConnect(/* @__PURE__ */ new Error("WebSocket closed"));
54
+ };
55
+ }), connectAbort]);
56
+ if (this.callbacks.avatarImageBase64) await Promise.race([this.sendAvatarImage(this.callbacks.avatarImageBase64), connectAbort]);
57
+ if (this.callbacks.initialPrompt) await Promise.race([this.sendInitialPrompt(this.callbacks.initialPrompt), connectAbort]);
58
+ await this.setupNewPeerConnection();
59
+ await Promise.race([new Promise((resolve, reject) => {
60
+ const checkConnection = setInterval(() => {
61
+ if (this.state === "connected") {
62
+ clearInterval(checkConnection);
63
+ resolve();
64
+ } else if (this.state === "disconnected") {
65
+ clearInterval(checkConnection);
66
+ reject(/* @__PURE__ */ new Error("Connection lost during WebRTC handshake"));
67
+ } else if (Date.now() >= deadline) {
68
+ clearInterval(checkConnection);
69
+ reject(/* @__PURE__ */ new Error("Connection timeout"));
70
+ }
71
+ }, 100);
72
+ connectAbort.catch(() => clearInterval(checkConnection));
73
+ }), connectAbort]);
74
+ } finally {
75
+ this.connectionReject = null;
76
+ }
59
77
  }
60
78
  async handleSignalingMessage(msg) {
61
79
  try {
@@ -121,10 +139,16 @@ var WebRTCConnection = class {
121
139
  } catch (error) {
122
140
  console.error("[WebRTC] Error:", error);
123
141
  this.callbacks.onError?.(error);
142
+ this.connectionReject?.(error);
124
143
  }
125
144
  }
126
145
  send(message) {
127
- if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
146
+ if (this.ws?.readyState === WebSocket.OPEN) {
147
+ this.ws.send(JSON.stringify(message));
148
+ return true;
149
+ }
150
+ console.warn("[WebRTC] Message dropped: WebSocket is not open");
151
+ return false;
128
152
  }
129
153
  async sendAvatarImage(imageBase64) {
130
154
  return this.setImageBase64(imageBase64);
@@ -154,7 +178,11 @@ var WebRTCConnection = class {
154
178
  };
155
179
  if (options?.prompt !== void 0) message.prompt = options.prompt;
156
180
  if (options?.enhance !== void 0) message.enhance_prompt = options.enhance;
157
- this.send(message);
181
+ if (!this.send(message)) {
182
+ clearTimeout(timeoutId);
183
+ this.websocketMessagesEmitter.off("setImageAck", listener);
184
+ reject(/* @__PURE__ */ new Error("WebSocket is not open"));
185
+ }
158
186
  });
159
187
  }
160
188
  /**
@@ -175,11 +203,15 @@ var WebRTCConnection = class {
175
203
  }
176
204
  };
177
205
  this.websocketMessagesEmitter.on("promptAck", listener);
178
- this.send({
206
+ if (!this.send({
179
207
  type: "prompt",
180
208
  prompt: prompt.text,
181
209
  enhance_prompt: prompt.enhance ?? true
182
- });
210
+ })) {
211
+ clearTimeout(timeoutId);
212
+ this.websocketMessagesEmitter.off("promptAck", listener);
213
+ reject(/* @__PURE__ */ new Error("WebSocket is not open"));
214
+ }
183
215
  });
184
216
  }
185
217
  setState(state) {
@@ -196,13 +228,14 @@ var WebRTCConnection = class {
196
228
  });
197
229
  this.pc.close();
198
230
  }
199
- const iceServers = ICE_SERVERS;
231
+ const iceServers = [...ICE_SERVERS];
200
232
  if (turnConfig) iceServers.push({
201
233
  urls: turnConfig.server_url,
202
234
  credential: turnConfig.credential,
203
235
  username: turnConfig.username
204
236
  });
205
237
  this.pc = new RTCPeerConnection({ iceServers });
238
+ this.setState("connecting");
206
239
  if (this.callbacks.isAvatarLive) this.pc.addTransceiver("video", { direction: "recvonly" });
207
240
  this.localStream.getTracks().forEach((track) => {
208
241
  if (this.pc && this.localStream) this.pc.addTrack(track, this.localStream);
@@ -221,7 +254,12 @@ var WebRTCConnection = class {
221
254
  const s = this.pc.connectionState;
222
255
  this.setState(s === "connected" ? "connected" : ["connecting", "new"].includes(s) ? "connecting" : "disconnected");
223
256
  };
224
- this.pc.oniceconnectionstatechange = () => {};
257
+ this.pc.oniceconnectionstatechange = () => {
258
+ if (this.pc?.iceConnectionState === "failed") {
259
+ this.setState("disconnected");
260
+ this.callbacks.onError?.(/* @__PURE__ */ new Error("ICE connection failed"));
261
+ }
262
+ };
225
263
  this.handleSignalingMessage({ type: "ready" });
226
264
  }
227
265
  cleanup() {
@@ -232,8 +270,12 @@ var WebRTCConnection = class {
232
270
  this.localStream = null;
233
271
  this.setState("disconnected");
234
272
  }
235
- async applyCodecPreference(preferredCodecName) {
273
+ applyCodecPreference(preferredCodecName) {
236
274
  if (!this.pc) return;
275
+ if (typeof RTCRtpSender === "undefined" || typeof RTCRtpSender.getCapabilities !== "function") {
276
+ console.warn("RTCRtpSender capabilities are not available in this environment.");
277
+ return;
278
+ }
237
279
  const videoTransceiver = this.pc.getTransceivers().find((r) => r.sender.track?.kind === "video" || r.receiver.track?.kind === "video");
238
280
  if (!videoTransceiver) {
239
281
  console.error("Could not find video transceiver. Ensure track is added to peer connection.");
@@ -255,12 +297,17 @@ var WebRTCConnection = class {
255
297
  console.warn("No video codecs found to set preferences for.");
256
298
  return;
257
299
  }
258
- await videoTransceiver.setCodecPreferences(orderedCodecs);
300
+ try {
301
+ videoTransceiver.setCodecPreferences(orderedCodecs);
302
+ } catch {
303
+ console.warn("[WebRTC] setCodecPreferences not supported, skipping codec preference.");
304
+ }
259
305
  }
260
306
  modifyVP8Bitrate(offer) {
261
307
  if (!offer.sdp) return;
262
308
  const minBitrateInKbps = this.callbacks.vp8MinBitrate;
263
309
  const startBitrateInKbps = this.callbacks.vp8StartBitrate;
310
+ if (minBitrateInKbps === void 0 || startBitrateInKbps === void 0) return;
264
311
  if (minBitrateInKbps === 0 && startBitrateInKbps === 0) return;
265
312
  const bitrateParams = `x-google-min-bitrate=${minBitrateInKbps};x-google-start-bitrate=${startBitrateInKbps}`;
266
313
  const sdpLines = offer.sdp.split("\r\n");
@@ -1,5 +1,5 @@
1
1
  import { WebRTCConnection } from "./webrtc-connection.js";
2
- import pRetry from "p-retry";
2
+ import pRetry, { AbortError } from "p-retry";
3
3
 
4
4
  //#region src/realtime/webrtc-manager.ts
5
5
  const PERMANENT_ERRORS = [
@@ -10,14 +10,27 @@ const PERMANENT_ERRORS = [
10
10
  "invalid api key",
11
11
  "unauthorized"
12
12
  ];
13
+ const CONNECTION_TIMEOUT = 6e4 * 5;
14
+ const RETRY_OPTIONS = {
15
+ retries: 5,
16
+ factor: 2,
17
+ minTimeout: 1e3,
18
+ maxTimeout: 1e4
19
+ };
13
20
  var WebRTCManager = class {
14
21
  connection;
15
22
  config;
23
+ localStream = null;
24
+ managerState = "disconnected";
25
+ hasConnected = false;
26
+ isReconnecting = false;
27
+ intentionalDisconnect = false;
28
+ reconnectGeneration = 0;
16
29
  constructor(config) {
17
30
  this.config = config;
18
31
  this.connection = new WebRTCConnection({
19
32
  onRemoteStream: config.onRemoteStream,
20
- onStateChange: config.onConnectionStateChange,
33
+ onStateChange: (state) => this.handleConnectionStateChange(state),
21
34
  onError: config.onError,
22
35
  customizeOffer: config.customizeOffer,
23
36
  vp8MinBitrate: config.vp8MinBitrate,
@@ -27,36 +40,107 @@ var WebRTCManager = class {
27
40
  initialPrompt: config.initialPrompt
28
41
  });
29
42
  }
43
+ emitState(state) {
44
+ if (this.managerState !== state) {
45
+ this.managerState = state;
46
+ if (state === "connected") this.hasConnected = true;
47
+ this.config.onConnectionStateChange?.(state);
48
+ }
49
+ }
50
+ handleConnectionStateChange(state) {
51
+ if (this.intentionalDisconnect) {
52
+ this.emitState("disconnected");
53
+ return;
54
+ }
55
+ if (this.isReconnecting) {
56
+ if (state === "connected") {
57
+ this.isReconnecting = false;
58
+ this.emitState("connected");
59
+ }
60
+ return;
61
+ }
62
+ if (state === "disconnected" && !this.intentionalDisconnect && this.hasConnected) {
63
+ this.reconnect();
64
+ return;
65
+ }
66
+ this.emitState(state);
67
+ }
68
+ async reconnect() {
69
+ if (this.isReconnecting || this.intentionalDisconnect || !this.localStream) return;
70
+ const reconnectGeneration = ++this.reconnectGeneration;
71
+ this.isReconnecting = true;
72
+ this.emitState("reconnecting");
73
+ try {
74
+ await pRetry(async () => {
75
+ if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) throw new AbortError("Reconnect cancelled");
76
+ const stream = this.localStream;
77
+ if (!stream) throw new AbortError("Reconnect cancelled: no local stream");
78
+ this.connection.cleanup();
79
+ await this.connection.connect(this.config.webrtcUrl, stream, CONNECTION_TIMEOUT, this.config.integration);
80
+ if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) {
81
+ this.connection.cleanup();
82
+ throw new AbortError("Reconnect cancelled");
83
+ }
84
+ }, {
85
+ ...RETRY_OPTIONS,
86
+ onFailedAttempt: (error) => {
87
+ if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) return;
88
+ console.error(`[WebRTC] Reconnect attempt failed: ${error.message}`);
89
+ this.connection.cleanup();
90
+ },
91
+ shouldRetry: (error) => {
92
+ if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) return false;
93
+ const msg = error.message.toLowerCase();
94
+ return !PERMANENT_ERRORS.some((err) => msg.includes(err));
95
+ }
96
+ });
97
+ } catch (error) {
98
+ this.isReconnecting = false;
99
+ if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) return;
100
+ this.emitState("disconnected");
101
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
102
+ }
103
+ }
30
104
  async connect(localStream) {
105
+ this.localStream = localStream;
106
+ this.intentionalDisconnect = false;
107
+ this.hasConnected = false;
108
+ this.isReconnecting = false;
109
+ this.reconnectGeneration += 1;
110
+ this.emitState("connecting");
31
111
  return pRetry(async () => {
32
- await this.connection.connect(this.config.webrtcUrl, localStream, 6e4 * 5, this.config.integration);
112
+ if (this.intentionalDisconnect) throw new AbortError("Connect cancelled");
113
+ await this.connection.connect(this.config.webrtcUrl, localStream, CONNECTION_TIMEOUT, this.config.integration);
33
114
  return true;
34
115
  }, {
35
- retries: 5,
36
- factor: 2,
37
- minTimeout: 1e3,
38
- maxTimeout: 1e4,
116
+ ...RETRY_OPTIONS,
39
117
  onFailedAttempt: (error) => {
40
118
  console.error(`[WebRTC] Failed to connect: ${error.message}`);
41
119
  this.connection.cleanup();
42
120
  },
43
121
  shouldRetry: (error) => {
122
+ if (this.intentionalDisconnect) return false;
44
123
  const msg = error.message.toLowerCase();
45
124
  return !PERMANENT_ERRORS.some((err) => msg.includes(err));
46
125
  }
47
126
  });
48
127
  }
49
128
  sendMessage(message) {
50
- this.connection.send(message);
129
+ return this.connection.send(message);
51
130
  }
52
131
  cleanup() {
132
+ this.intentionalDisconnect = true;
133
+ this.isReconnecting = false;
134
+ this.reconnectGeneration += 1;
53
135
  this.connection.cleanup();
136
+ this.localStream = null;
137
+ this.emitState("disconnected");
54
138
  }
55
139
  isConnected() {
56
- return this.connection.state === "connected";
140
+ return this.managerState === "connected";
57
141
  }
58
142
  getConnectionState() {
59
- return this.connection.state;
143
+ return this.managerState;
60
144
  }
61
145
  getWebsocketMessageEmitter() {
62
146
  return this.connection.websocketMessagesEmitter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",