@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 +4 -2
- package/dist/realtime/audio-stream-manager.js +1 -1
- package/dist/realtime/client.d.ts +5 -3
- package/dist/realtime/client.js +93 -69
- package/dist/realtime/methods.d.ts +11 -0
- package/dist/realtime/methods.js +42 -9
- package/dist/realtime/webrtc-connection.d.ts +7 -0
- package/dist/realtime/webrtc-connection.js +90 -43
- package/dist/realtime/webrtc-manager.js +94 -10
- package/package.json +1 -1
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 {
|
|
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 };
|
|
@@ -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 };
|
package/dist/realtime/client.js
CHANGED
|
@@ -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
|
|
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
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
if (typeof avatar.avatarImage === "string")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 };
|
package/dist/realtime/methods.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
8
|
-
|
|
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 {
|
|
70
|
+
return {
|
|
71
|
+
set,
|
|
72
|
+
setPrompt
|
|
73
|
+
};
|
|
41
74
|
};
|
|
42
75
|
|
|
43
76
|
//#endregion
|
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
140
|
+
return this.managerState === "connected";
|
|
57
141
|
}
|
|
58
142
|
getConnectionState() {
|
|
59
|
-
return this.
|
|
143
|
+
return this.managerState;
|
|
60
144
|
}
|
|
61
145
|
getWebsocketMessageEmitter() {
|
|
62
146
|
return this.connection.websocketMessagesEmitter;
|