@decartai/sdk 0.0.47 → 0.0.49
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 +9 -2
- package/dist/index.js +7 -2
- package/dist/queue/polling.js +9 -4
- package/dist/queue/types.d.ts +2 -0
- package/dist/realtime/client.d.ts +7 -8
- package/dist/realtime/client.js +128 -27
- package/dist/realtime/diagnostics.d.ts +78 -0
- package/dist/realtime/subscribe-client.d.ts +2 -0
- package/dist/realtime/telemetry-reporter.js +120 -0
- package/dist/realtime/webrtc-connection.js +156 -22
- package/dist/realtime/webrtc-manager.js +39 -5
- package/dist/realtime/webrtc-stats.d.ts +59 -0
- package/dist/realtime/webrtc-stats.js +154 -0
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.js +11 -4
- package/dist/utils/errors.d.ts +5 -1
- package/dist/utils/errors.js +39 -5
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.js +37 -0
- package/package.json +5 -2
|
@@ -4,15 +4,28 @@ import mitt from "mitt";
|
|
|
4
4
|
//#region src/realtime/webrtc-connection.ts
|
|
5
5
|
const ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
6
6
|
const AVATAR_SETUP_TIMEOUT_MS = 3e4;
|
|
7
|
+
const noopDiagnostic = () => {};
|
|
7
8
|
var WebRTCConnection = class {
|
|
8
9
|
pc = null;
|
|
9
10
|
ws = null;
|
|
10
11
|
localStream = null;
|
|
11
12
|
connectionReject = null;
|
|
13
|
+
logger;
|
|
14
|
+
emitDiagnostic;
|
|
12
15
|
state = "disconnected";
|
|
13
16
|
websocketMessagesEmitter = mitt();
|
|
14
17
|
constructor(callbacks = {}) {
|
|
15
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;
|
|
16
29
|
}
|
|
17
30
|
async connect(url, localStream, timeout, integration) {
|
|
18
31
|
const deadline = Date.now() + timeout;
|
|
@@ -25,24 +38,37 @@ var WebRTCConnection = class {
|
|
|
25
38
|
});
|
|
26
39
|
connectAbort.catch(() => {});
|
|
27
40
|
this.connectionReject = (error) => rejectConnect(error);
|
|
41
|
+
const totalStart = performance.now();
|
|
28
42
|
try {
|
|
43
|
+
const wsStart = performance.now();
|
|
29
44
|
await Promise.race([new Promise((resolve, reject) => {
|
|
30
45
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("WebSocket timeout")), timeout);
|
|
31
46
|
this.ws = new WebSocket(wsUrl);
|
|
32
47
|
this.ws.onopen = () => {
|
|
33
48
|
clearTimeout(timer);
|
|
49
|
+
this.emitDiagnostic("phaseTiming", {
|
|
50
|
+
phase: "websocket",
|
|
51
|
+
durationMs: performance.now() - wsStart,
|
|
52
|
+
success: true
|
|
53
|
+
});
|
|
34
54
|
resolve();
|
|
35
55
|
};
|
|
36
56
|
this.ws.onmessage = (e) => {
|
|
37
57
|
try {
|
|
38
58
|
this.handleSignalingMessage(JSON.parse(e.data));
|
|
39
59
|
} catch (err) {
|
|
40
|
-
|
|
60
|
+
this.logger.error("Signaling message parse error", { error: String(err) });
|
|
41
61
|
}
|
|
42
62
|
};
|
|
43
63
|
this.ws.onerror = () => {
|
|
44
64
|
clearTimeout(timer);
|
|
45
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
|
+
});
|
|
46
72
|
reject(error);
|
|
47
73
|
rejectConnect(error);
|
|
48
74
|
};
|
|
@@ -53,24 +79,65 @@ var WebRTCConnection = class {
|
|
|
53
79
|
rejectConnect(/* @__PURE__ */ new Error("WebSocket closed"));
|
|
54
80
|
};
|
|
55
81
|
}), connectAbort]);
|
|
56
|
-
if (this.callbacks.
|
|
57
|
-
|
|
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
|
+
}
|
|
102
|
+
const handshakeStart = performance.now();
|
|
58
103
|
await this.setupNewPeerConnection();
|
|
59
104
|
await Promise.race([new Promise((resolve, reject) => {
|
|
60
105
|
const checkConnection = setInterval(() => {
|
|
61
106
|
if (this.state === "connected" || this.state === "generating") {
|
|
62
107
|
clearInterval(checkConnection);
|
|
108
|
+
this.emitDiagnostic("phaseTiming", {
|
|
109
|
+
phase: "webrtc-handshake",
|
|
110
|
+
durationMs: performance.now() - handshakeStart,
|
|
111
|
+
success: true
|
|
112
|
+
});
|
|
63
113
|
resolve();
|
|
64
114
|
} else if (this.state === "disconnected") {
|
|
65
115
|
clearInterval(checkConnection);
|
|
116
|
+
this.emitDiagnostic("phaseTiming", {
|
|
117
|
+
phase: "webrtc-handshake",
|
|
118
|
+
durationMs: performance.now() - handshakeStart,
|
|
119
|
+
success: false,
|
|
120
|
+
error: "Connection lost during handshake"
|
|
121
|
+
});
|
|
66
122
|
reject(/* @__PURE__ */ new Error("Connection lost during WebRTC handshake"));
|
|
67
123
|
} else if (Date.now() >= deadline) {
|
|
68
124
|
clearInterval(checkConnection);
|
|
125
|
+
this.emitDiagnostic("phaseTiming", {
|
|
126
|
+
phase: "webrtc-handshake",
|
|
127
|
+
durationMs: performance.now() - handshakeStart,
|
|
128
|
+
success: false,
|
|
129
|
+
error: "Timeout"
|
|
130
|
+
});
|
|
69
131
|
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
70
132
|
}
|
|
71
133
|
}, 100);
|
|
72
134
|
connectAbort.catch(() => clearInterval(checkConnection));
|
|
73
135
|
}), connectAbort]);
|
|
136
|
+
this.emitDiagnostic("phaseTiming", {
|
|
137
|
+
phase: "total",
|
|
138
|
+
durationMs: performance.now() - totalStart,
|
|
139
|
+
success: true
|
|
140
|
+
});
|
|
74
141
|
} finally {
|
|
75
142
|
this.connectionReject = null;
|
|
76
143
|
}
|
|
@@ -79,6 +146,7 @@ var WebRTCConnection = class {
|
|
|
79
146
|
try {
|
|
80
147
|
if (msg.type === "error") {
|
|
81
148
|
const error = new Error(msg.error);
|
|
149
|
+
error.source = "server";
|
|
82
150
|
this.callbacks.onError?.(error);
|
|
83
151
|
if (this.connectionReject) {
|
|
84
152
|
this.connectionReject(error);
|
|
@@ -141,7 +209,14 @@ var WebRTCConnection = class {
|
|
|
141
209
|
});
|
|
142
210
|
break;
|
|
143
211
|
case "ice-candidate":
|
|
144
|
-
if (msg.candidate)
|
|
212
|
+
if (msg.candidate) {
|
|
213
|
+
await this.pc.addIceCandidate(msg.candidate);
|
|
214
|
+
this.emitDiagnostic("iceCandidate", {
|
|
215
|
+
source: "remote",
|
|
216
|
+
candidateType: msg.candidate.candidate?.match(/typ (\w+)/)?.[1] ?? "unknown",
|
|
217
|
+
protocol: msg.candidate.candidate?.match(/udp|tcp/i)?.[0]?.toLowerCase() ?? "unknown"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
145
220
|
break;
|
|
146
221
|
case "ice-restart": {
|
|
147
222
|
const turnConfig = msg.turn_config;
|
|
@@ -150,7 +225,7 @@ var WebRTCConnection = class {
|
|
|
150
225
|
}
|
|
151
226
|
}
|
|
152
227
|
} catch (error) {
|
|
153
|
-
|
|
228
|
+
this.logger.error("Signaling handler error", { error: String(error) });
|
|
154
229
|
this.callbacks.onError?.(error);
|
|
155
230
|
this.connectionReject?.(error);
|
|
156
231
|
}
|
|
@@ -160,18 +235,9 @@ var WebRTCConnection = class {
|
|
|
160
235
|
this.ws.send(JSON.stringify(message));
|
|
161
236
|
return true;
|
|
162
237
|
}
|
|
163
|
-
|
|
238
|
+
this.logger.warn("Message dropped: WebSocket is not open");
|
|
164
239
|
return false;
|
|
165
240
|
}
|
|
166
|
-
async sendAvatarImage(imageBase64) {
|
|
167
|
-
return this.setImageBase64(imageBase64);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Send an image to the server (e.g., as a reference for inference).
|
|
171
|
-
* Can be called after connection is established.
|
|
172
|
-
* Pass null to clear the reference image or use a placeholder.
|
|
173
|
-
* Optionally include a prompt to send with the image.
|
|
174
|
-
*/
|
|
175
241
|
async setImageBase64(imageBase64, options) {
|
|
176
242
|
return new Promise((resolve, reject) => {
|
|
177
243
|
const timeoutId = setTimeout(() => {
|
|
@@ -249,7 +315,7 @@ var WebRTCConnection = class {
|
|
|
249
315
|
this.pc = new RTCPeerConnection({ iceServers });
|
|
250
316
|
this.setState("connecting");
|
|
251
317
|
if (this.localStream) {
|
|
252
|
-
if (this.callbacks.
|
|
318
|
+
if (this.callbacks.modelName === "live_avatar") this.pc.addTransceiver("video", { direction: "recvonly" });
|
|
253
319
|
this.localStream.getTracks().forEach((track) => {
|
|
254
320
|
if (this.pc && this.localStream) this.pc.addTrack(track, this.localStream);
|
|
255
321
|
});
|
|
@@ -271,22 +337,90 @@ var WebRTCConnection = class {
|
|
|
271
337
|
type: "ice-candidate",
|
|
272
338
|
candidate: e.candidate
|
|
273
339
|
});
|
|
340
|
+
if (e.candidate) this.emitDiagnostic("iceCandidate", {
|
|
341
|
+
source: "local",
|
|
342
|
+
candidateType: e.candidate.type ?? "unknown",
|
|
343
|
+
protocol: e.candidate.protocol ?? "unknown",
|
|
344
|
+
address: e.candidate.address ?? void 0,
|
|
345
|
+
port: e.candidate.port ?? void 0
|
|
346
|
+
});
|
|
274
347
|
};
|
|
348
|
+
let prevPcState = "new";
|
|
275
349
|
this.pc.onconnectionstatechange = () => {
|
|
276
350
|
if (!this.pc) return;
|
|
277
351
|
const s = this.pc.connectionState;
|
|
352
|
+
this.emitDiagnostic("peerConnectionStateChange", {
|
|
353
|
+
state: s,
|
|
354
|
+
previousState: prevPcState,
|
|
355
|
+
timestampMs: performance.now()
|
|
356
|
+
});
|
|
357
|
+
prevPcState = s;
|
|
358
|
+
if (s === "connected") this.emitSelectedCandidatePair();
|
|
278
359
|
const nextState = s === "connected" ? "connected" : ["connecting", "new"].includes(s) ? "connecting" : "disconnected";
|
|
279
360
|
if (this.state === "generating" && nextState !== "disconnected") return;
|
|
280
361
|
this.setState(nextState);
|
|
281
362
|
};
|
|
363
|
+
let prevIceState = "new";
|
|
282
364
|
this.pc.oniceconnectionstatechange = () => {
|
|
283
|
-
if (this.pc
|
|
365
|
+
if (!this.pc) return;
|
|
366
|
+
const newIceState = this.pc.iceConnectionState;
|
|
367
|
+
this.emitDiagnostic("iceStateChange", {
|
|
368
|
+
state: newIceState,
|
|
369
|
+
previousState: prevIceState,
|
|
370
|
+
timestampMs: performance.now()
|
|
371
|
+
});
|
|
372
|
+
prevIceState = newIceState;
|
|
373
|
+
if (newIceState === "failed") {
|
|
284
374
|
this.setState("disconnected");
|
|
285
375
|
this.callbacks.onError?.(/* @__PURE__ */ new Error("ICE connection failed"));
|
|
286
376
|
}
|
|
287
377
|
};
|
|
378
|
+
let prevSignalingState = "stable";
|
|
379
|
+
this.pc.onsignalingstatechange = () => {
|
|
380
|
+
if (!this.pc) return;
|
|
381
|
+
const newState = this.pc.signalingState;
|
|
382
|
+
this.emitDiagnostic("signalingStateChange", {
|
|
383
|
+
state: newState,
|
|
384
|
+
previousState: prevSignalingState,
|
|
385
|
+
timestampMs: performance.now()
|
|
386
|
+
});
|
|
387
|
+
prevSignalingState = newState;
|
|
388
|
+
};
|
|
288
389
|
this.handleSignalingMessage({ type: "ready" });
|
|
289
390
|
}
|
|
391
|
+
async emitSelectedCandidatePair() {
|
|
392
|
+
if (!this.pc) return;
|
|
393
|
+
try {
|
|
394
|
+
const stats = await this.pc.getStats();
|
|
395
|
+
let found = false;
|
|
396
|
+
stats.forEach((report) => {
|
|
397
|
+
if (found) return;
|
|
398
|
+
if (report.type === "candidate-pair" && report.state === "succeeded") {
|
|
399
|
+
found = true;
|
|
400
|
+
let localCandidate;
|
|
401
|
+
let remoteCandidate;
|
|
402
|
+
stats.forEach((r) => {
|
|
403
|
+
if (r.id === report.localCandidateId) localCandidate = r;
|
|
404
|
+
if (r.id === report.remoteCandidateId) remoteCandidate = r;
|
|
405
|
+
});
|
|
406
|
+
if (localCandidate && remoteCandidate) this.emitDiagnostic("selectedCandidatePair", {
|
|
407
|
+
local: {
|
|
408
|
+
candidateType: String(localCandidate.candidateType ?? "unknown"),
|
|
409
|
+
protocol: String(localCandidate.protocol ?? "unknown"),
|
|
410
|
+
address: localCandidate.address,
|
|
411
|
+
port: localCandidate.port
|
|
412
|
+
},
|
|
413
|
+
remote: {
|
|
414
|
+
candidateType: String(remoteCandidate.candidateType ?? "unknown"),
|
|
415
|
+
protocol: String(remoteCandidate.protocol ?? "unknown"),
|
|
416
|
+
address: remoteCandidate.address,
|
|
417
|
+
port: remoteCandidate.port
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
} catch {}
|
|
423
|
+
}
|
|
290
424
|
cleanup() {
|
|
291
425
|
this.pc?.close();
|
|
292
426
|
this.pc = null;
|
|
@@ -298,17 +432,17 @@ var WebRTCConnection = class {
|
|
|
298
432
|
applyCodecPreference(preferredCodecName) {
|
|
299
433
|
if (!this.pc) return;
|
|
300
434
|
if (typeof RTCRtpSender === "undefined" || typeof RTCRtpSender.getCapabilities !== "function") {
|
|
301
|
-
|
|
435
|
+
this.logger.debug("RTCRtpSender capabilities not available in this environment");
|
|
302
436
|
return;
|
|
303
437
|
}
|
|
304
438
|
const videoTransceiver = this.pc.getTransceivers().find((r) => r.sender.track?.kind === "video" || r.receiver.track?.kind === "video");
|
|
305
439
|
if (!videoTransceiver) {
|
|
306
|
-
|
|
440
|
+
this.logger.warn("Video transceiver not found for codec preference");
|
|
307
441
|
return;
|
|
308
442
|
}
|
|
309
443
|
const capabilities = RTCRtpSender.getCapabilities("video");
|
|
310
444
|
if (!capabilities) {
|
|
311
|
-
|
|
445
|
+
this.logger.warn("Video sender capabilities unavailable");
|
|
312
446
|
return;
|
|
313
447
|
}
|
|
314
448
|
const preferredCodecs = [];
|
|
@@ -319,13 +453,13 @@ var WebRTCConnection = class {
|
|
|
319
453
|
});
|
|
320
454
|
const orderedCodecs = [...preferredCodecs, ...otherCodecs];
|
|
321
455
|
if (orderedCodecs.length === 0) {
|
|
322
|
-
|
|
456
|
+
this.logger.debug("No video codecs found for preference setting");
|
|
323
457
|
return;
|
|
324
458
|
}
|
|
325
459
|
try {
|
|
326
460
|
videoTransceiver.setCodecPreferences(orderedCodecs);
|
|
327
461
|
} catch {
|
|
328
|
-
|
|
462
|
+
this.logger.debug("setCodecPreferences not supported, skipping");
|
|
329
463
|
}
|
|
330
464
|
}
|
|
331
465
|
modifyVP8Bitrate(offer) {
|
|
@@ -20,6 +20,7 @@ const RETRY_OPTIONS = {
|
|
|
20
20
|
var WebRTCManager = class {
|
|
21
21
|
connection;
|
|
22
22
|
config;
|
|
23
|
+
logger;
|
|
23
24
|
localStream = null;
|
|
24
25
|
subscribeMode = false;
|
|
25
26
|
managerState = "disconnected";
|
|
@@ -29,6 +30,12 @@ var WebRTCManager = class {
|
|
|
29
30
|
reconnectGeneration = 0;
|
|
30
31
|
constructor(config) {
|
|
31
32
|
this.config = config;
|
|
33
|
+
this.logger = config.logger ?? {
|
|
34
|
+
debug() {},
|
|
35
|
+
info() {},
|
|
36
|
+
warn() {},
|
|
37
|
+
error() {}
|
|
38
|
+
};
|
|
32
39
|
this.connection = new WebRTCConnection({
|
|
33
40
|
onRemoteStream: config.onRemoteStream,
|
|
34
41
|
onStateChange: (state) => this.handleConnectionStateChange(state),
|
|
@@ -36,9 +43,11 @@ var WebRTCManager = class {
|
|
|
36
43
|
customizeOffer: config.customizeOffer,
|
|
37
44
|
vp8MinBitrate: config.vp8MinBitrate,
|
|
38
45
|
vp8StartBitrate: config.vp8StartBitrate,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
initialPrompt: config.initialPrompt
|
|
46
|
+
modelName: config.modelName,
|
|
47
|
+
initialImage: config.initialImage,
|
|
48
|
+
initialPrompt: config.initialPrompt,
|
|
49
|
+
logger: this.logger,
|
|
50
|
+
onDiagnostic: config.onDiagnostic
|
|
42
51
|
});
|
|
43
52
|
}
|
|
44
53
|
emitState(state) {
|
|
@@ -72,8 +81,11 @@ var WebRTCManager = class {
|
|
|
72
81
|
const reconnectGeneration = ++this.reconnectGeneration;
|
|
73
82
|
this.isReconnecting = true;
|
|
74
83
|
this.emitState("reconnecting");
|
|
84
|
+
const reconnectStart = performance.now();
|
|
75
85
|
try {
|
|
86
|
+
let attemptCount = 0;
|
|
76
87
|
await pRetry(async () => {
|
|
88
|
+
attemptCount++;
|
|
77
89
|
if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) throw new AbortError("Reconnect cancelled");
|
|
78
90
|
if (!this.subscribeMode && !this.localStream) throw new AbortError("Reconnect cancelled: no local stream");
|
|
79
91
|
this.connection.cleanup();
|
|
@@ -86,7 +98,17 @@ var WebRTCManager = class {
|
|
|
86
98
|
...RETRY_OPTIONS,
|
|
87
99
|
onFailedAttempt: (error) => {
|
|
88
100
|
if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) return;
|
|
89
|
-
|
|
101
|
+
this.logger.warn("Reconnect attempt failed", {
|
|
102
|
+
error: error.message,
|
|
103
|
+
attempt: error.attemptNumber
|
|
104
|
+
});
|
|
105
|
+
this.config.onDiagnostic?.("reconnect", {
|
|
106
|
+
attempt: error.attemptNumber,
|
|
107
|
+
maxAttempts: RETRY_OPTIONS.retries + 1,
|
|
108
|
+
durationMs: performance.now() - reconnectStart,
|
|
109
|
+
success: false,
|
|
110
|
+
error: error.message
|
|
111
|
+
});
|
|
90
112
|
this.connection.cleanup();
|
|
91
113
|
},
|
|
92
114
|
shouldRetry: (error) => {
|
|
@@ -95,6 +117,12 @@ var WebRTCManager = class {
|
|
|
95
117
|
return !PERMANENT_ERRORS.some((err) => msg.includes(err));
|
|
96
118
|
}
|
|
97
119
|
});
|
|
120
|
+
this.config.onDiagnostic?.("reconnect", {
|
|
121
|
+
attempt: attemptCount,
|
|
122
|
+
maxAttempts: RETRY_OPTIONS.retries + 1,
|
|
123
|
+
durationMs: performance.now() - reconnectStart,
|
|
124
|
+
success: true
|
|
125
|
+
});
|
|
98
126
|
} catch (error) {
|
|
99
127
|
this.isReconnecting = false;
|
|
100
128
|
if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) return;
|
|
@@ -117,7 +145,10 @@ var WebRTCManager = class {
|
|
|
117
145
|
}, {
|
|
118
146
|
...RETRY_OPTIONS,
|
|
119
147
|
onFailedAttempt: (error) => {
|
|
120
|
-
|
|
148
|
+
this.logger.warn("Connection attempt failed", {
|
|
149
|
+
error: error.message,
|
|
150
|
+
attempt: error.attemptNumber
|
|
151
|
+
});
|
|
121
152
|
this.connection.cleanup();
|
|
122
153
|
},
|
|
123
154
|
shouldRetry: (error) => {
|
|
@@ -144,6 +175,9 @@ var WebRTCManager = class {
|
|
|
144
175
|
getConnectionState() {
|
|
145
176
|
return this.managerState;
|
|
146
177
|
}
|
|
178
|
+
getPeerConnection() {
|
|
179
|
+
return this.connection.getPeerConnection();
|
|
180
|
+
}
|
|
147
181
|
getWebsocketMessageEmitter() {
|
|
148
182
|
return this.connection.websocketMessagesEmitter;
|
|
149
183
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/realtime/webrtc-stats.d.ts
|
|
2
|
+
type WebRTCStats = {
|
|
3
|
+
timestamp: number;
|
|
4
|
+
video: {
|
|
5
|
+
framesDecoded: number;
|
|
6
|
+
framesDropped: number;
|
|
7
|
+
framesPerSecond: number;
|
|
8
|
+
frameWidth: number;
|
|
9
|
+
frameHeight: number;
|
|
10
|
+
bytesReceived: number;
|
|
11
|
+
packetsReceived: number;
|
|
12
|
+
packetsLost: number;
|
|
13
|
+
jitter: number;
|
|
14
|
+
/** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
|
|
15
|
+
bitrate: number;
|
|
16
|
+
freezeCount: number;
|
|
17
|
+
totalFreezesDuration: number;
|
|
18
|
+
/** Delta: packets lost since previous sample. */
|
|
19
|
+
packetsLostDelta: number;
|
|
20
|
+
/** Delta: frames dropped since previous sample. */
|
|
21
|
+
framesDroppedDelta: number;
|
|
22
|
+
/** Delta: freeze count since previous sample. */
|
|
23
|
+
freezeCountDelta: number;
|
|
24
|
+
/** Delta: freeze duration (seconds) since previous sample. */
|
|
25
|
+
freezeDurationDelta: number;
|
|
26
|
+
} | null;
|
|
27
|
+
audio: {
|
|
28
|
+
bytesReceived: number;
|
|
29
|
+
packetsReceived: number;
|
|
30
|
+
packetsLost: number;
|
|
31
|
+
jitter: number;
|
|
32
|
+
/** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
|
|
33
|
+
bitrate: number;
|
|
34
|
+
/** Delta: packets lost since previous sample. */
|
|
35
|
+
packetsLostDelta: number;
|
|
36
|
+
} | null;
|
|
37
|
+
/** Outbound video track stats (from the local camera/screen share being sent). */
|
|
38
|
+
outboundVideo: {
|
|
39
|
+
/** Why the encoder is limiting quality: "none", "bandwidth", "cpu", or "other". */
|
|
40
|
+
qualityLimitationReason: string;
|
|
41
|
+
/** Cumulative time (seconds) spent in each quality limitation state. */
|
|
42
|
+
qualityLimitationDurations: Record<string, number>;
|
|
43
|
+
bytesSent: number;
|
|
44
|
+
packetsSent: number;
|
|
45
|
+
framesPerSecond: number;
|
|
46
|
+
frameWidth: number;
|
|
47
|
+
frameHeight: number;
|
|
48
|
+
/** Estimated outbound bitrate in bits/sec, computed from bytesSent delta. */
|
|
49
|
+
bitrate: number;
|
|
50
|
+
} | null;
|
|
51
|
+
connection: {
|
|
52
|
+
/** Current round-trip time in seconds, or null if unavailable. */
|
|
53
|
+
currentRoundTripTime: number | null;
|
|
54
|
+
/** Available outgoing bitrate estimate in bits/sec, or null if unavailable. */
|
|
55
|
+
availableOutgoingBitrate: number | null;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { WebRTCStats };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//#region src/realtime/webrtc-stats.ts
|
|
2
|
+
const DEFAULT_INTERVAL_MS = 1e3;
|
|
3
|
+
const MIN_INTERVAL_MS = 500;
|
|
4
|
+
var WebRTCStatsCollector = class {
|
|
5
|
+
pc = null;
|
|
6
|
+
intervalId = null;
|
|
7
|
+
prevBytesVideo = 0;
|
|
8
|
+
prevBytesAudio = 0;
|
|
9
|
+
prevBytesSentVideo = 0;
|
|
10
|
+
prevTimestamp = 0;
|
|
11
|
+
prevPacketsLostVideo = 0;
|
|
12
|
+
prevFramesDropped = 0;
|
|
13
|
+
prevFreezeCount = 0;
|
|
14
|
+
prevFreezeDuration = 0;
|
|
15
|
+
prevPacketsLostAudio = 0;
|
|
16
|
+
onStats = null;
|
|
17
|
+
intervalMs;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
|
|
20
|
+
}
|
|
21
|
+
/** Attach to a peer connection and start polling. */
|
|
22
|
+
start(pc, onStats) {
|
|
23
|
+
this.stop();
|
|
24
|
+
this.pc = pc;
|
|
25
|
+
this.onStats = onStats;
|
|
26
|
+
this.prevBytesVideo = 0;
|
|
27
|
+
this.prevBytesAudio = 0;
|
|
28
|
+
this.prevBytesSentVideo = 0;
|
|
29
|
+
this.prevTimestamp = 0;
|
|
30
|
+
this.prevPacketsLostVideo = 0;
|
|
31
|
+
this.prevFramesDropped = 0;
|
|
32
|
+
this.prevFreezeCount = 0;
|
|
33
|
+
this.prevFreezeDuration = 0;
|
|
34
|
+
this.prevPacketsLostAudio = 0;
|
|
35
|
+
this.intervalId = setInterval(() => this.collect(), this.intervalMs);
|
|
36
|
+
}
|
|
37
|
+
/** Stop polling and release resources. */
|
|
38
|
+
stop() {
|
|
39
|
+
if (this.intervalId !== null) {
|
|
40
|
+
clearInterval(this.intervalId);
|
|
41
|
+
this.intervalId = null;
|
|
42
|
+
}
|
|
43
|
+
this.pc = null;
|
|
44
|
+
this.onStats = null;
|
|
45
|
+
}
|
|
46
|
+
isRunning() {
|
|
47
|
+
return this.intervalId !== null;
|
|
48
|
+
}
|
|
49
|
+
async collect() {
|
|
50
|
+
if (!this.pc || !this.onStats) return;
|
|
51
|
+
try {
|
|
52
|
+
const rawStats = await this.pc.getStats();
|
|
53
|
+
const stats = this.parse(rawStats);
|
|
54
|
+
this.onStats(stats);
|
|
55
|
+
} catch {
|
|
56
|
+
this.stop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
parse(rawStats) {
|
|
60
|
+
const now = performance.now();
|
|
61
|
+
const elapsed = this.prevTimestamp > 0 ? (now - this.prevTimestamp) / 1e3 : 0;
|
|
62
|
+
let video = null;
|
|
63
|
+
let audio = null;
|
|
64
|
+
let outboundVideo = null;
|
|
65
|
+
const connection = {
|
|
66
|
+
currentRoundTripTime: null,
|
|
67
|
+
availableOutgoingBitrate: null
|
|
68
|
+
};
|
|
69
|
+
rawStats.forEach((report) => {
|
|
70
|
+
if (report.type === "inbound-rtp" && report.kind === "video") {
|
|
71
|
+
const bytesReceived = report.bytesReceived ?? 0;
|
|
72
|
+
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesVideo) * 8 / elapsed : 0;
|
|
73
|
+
this.prevBytesVideo = bytesReceived;
|
|
74
|
+
const r = report;
|
|
75
|
+
const packetsLost = r.packetsLost ?? 0;
|
|
76
|
+
const framesDropped = r.framesDropped ?? 0;
|
|
77
|
+
const freezeCount = r.freezeCount ?? 0;
|
|
78
|
+
const freezeDuration = r.totalFreezesDuration ?? 0;
|
|
79
|
+
video = {
|
|
80
|
+
framesDecoded: r.framesDecoded ?? 0,
|
|
81
|
+
framesDropped,
|
|
82
|
+
framesPerSecond: r.framesPerSecond ?? 0,
|
|
83
|
+
frameWidth: r.frameWidth ?? 0,
|
|
84
|
+
frameHeight: r.frameHeight ?? 0,
|
|
85
|
+
bytesReceived,
|
|
86
|
+
packetsReceived: r.packetsReceived ?? 0,
|
|
87
|
+
packetsLost,
|
|
88
|
+
jitter: r.jitter ?? 0,
|
|
89
|
+
bitrate: Math.round(bitrate),
|
|
90
|
+
freezeCount,
|
|
91
|
+
totalFreezesDuration: freezeDuration,
|
|
92
|
+
packetsLostDelta: Math.max(0, packetsLost - this.prevPacketsLostVideo),
|
|
93
|
+
framesDroppedDelta: Math.max(0, framesDropped - this.prevFramesDropped),
|
|
94
|
+
freezeCountDelta: Math.max(0, freezeCount - this.prevFreezeCount),
|
|
95
|
+
freezeDurationDelta: Math.max(0, freezeDuration - this.prevFreezeDuration)
|
|
96
|
+
};
|
|
97
|
+
this.prevPacketsLostVideo = packetsLost;
|
|
98
|
+
this.prevFramesDropped = framesDropped;
|
|
99
|
+
this.prevFreezeCount = freezeCount;
|
|
100
|
+
this.prevFreezeDuration = freezeDuration;
|
|
101
|
+
}
|
|
102
|
+
if (report.type === "outbound-rtp" && report.kind === "video") {
|
|
103
|
+
const r = report;
|
|
104
|
+
const bytesSent = r.bytesSent ?? 0;
|
|
105
|
+
const outBitrate = elapsed > 0 ? (bytesSent - this.prevBytesSentVideo) * 8 / elapsed : 0;
|
|
106
|
+
this.prevBytesSentVideo = bytesSent;
|
|
107
|
+
outboundVideo = {
|
|
108
|
+
qualityLimitationReason: r.qualityLimitationReason ?? "none",
|
|
109
|
+
qualityLimitationDurations: r.qualityLimitationDurations ?? {},
|
|
110
|
+
bytesSent,
|
|
111
|
+
packetsSent: r.packetsSent ?? 0,
|
|
112
|
+
framesPerSecond: r.framesPerSecond ?? 0,
|
|
113
|
+
frameWidth: r.frameWidth ?? 0,
|
|
114
|
+
frameHeight: r.frameHeight ?? 0,
|
|
115
|
+
bitrate: Math.round(outBitrate)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (report.type === "inbound-rtp" && report.kind === "audio") {
|
|
119
|
+
const bytesReceived = report.bytesReceived ?? 0;
|
|
120
|
+
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesAudio) * 8 / elapsed : 0;
|
|
121
|
+
this.prevBytesAudio = bytesReceived;
|
|
122
|
+
const r = report;
|
|
123
|
+
const audioPacketsLost = r.packetsLost ?? 0;
|
|
124
|
+
audio = {
|
|
125
|
+
bytesReceived,
|
|
126
|
+
packetsReceived: r.packetsReceived ?? 0,
|
|
127
|
+
packetsLost: audioPacketsLost,
|
|
128
|
+
jitter: r.jitter ?? 0,
|
|
129
|
+
bitrate: Math.round(bitrate),
|
|
130
|
+
packetsLostDelta: Math.max(0, audioPacketsLost - this.prevPacketsLostAudio)
|
|
131
|
+
};
|
|
132
|
+
this.prevPacketsLostAudio = audioPacketsLost;
|
|
133
|
+
}
|
|
134
|
+
if (report.type === "candidate-pair") {
|
|
135
|
+
const r = report;
|
|
136
|
+
if (r.state === "succeeded") {
|
|
137
|
+
connection.currentRoundTripTime = r.currentRoundTripTime ?? null;
|
|
138
|
+
connection.availableOutgoingBitrate = r.availableOutgoingBitrate ?? null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.prevTimestamp = now;
|
|
143
|
+
return {
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
video,
|
|
146
|
+
audio,
|
|
147
|
+
outboundVideo,
|
|
148
|
+
connection
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
export { WebRTCStatsCollector };
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare const modelStateSchema: z.ZodObject<{
|
|
|
6
6
|
text: z.ZodString;
|
|
7
7
|
enhance: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
8
|
}, z.core.$strip>>;
|
|
9
|
+
image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString]>>;
|
|
9
10
|
}, z.core.$strip>;
|
|
10
11
|
type ModelState = z.infer<typeof modelStateSchema>;
|
|
11
12
|
//#endregion
|
package/dist/shared/types.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
//#region src/shared/types.ts
|
|
4
|
-
const modelStateSchema = z.object({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const modelStateSchema = z.object({
|
|
5
|
+
prompt: z.object({
|
|
6
|
+
text: z.string().min(1),
|
|
7
|
+
enhance: z.boolean().optional().default(true)
|
|
8
|
+
}).optional(),
|
|
9
|
+
image: z.union([
|
|
10
|
+
z.instanceof(Blob),
|
|
11
|
+
z.instanceof(File),
|
|
12
|
+
z.string()
|
|
13
|
+
]).optional()
|
|
14
|
+
});
|
|
8
15
|
|
|
9
16
|
//#endregion
|
|
10
17
|
export { modelStateSchema };
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ type DecartSDKError = {
|
|
|
8
8
|
declare const ERROR_CODES: {
|
|
9
9
|
readonly INVALID_API_KEY: "INVALID_API_KEY";
|
|
10
10
|
readonly INVALID_BASE_URL: "INVALID_BASE_URL";
|
|
11
|
-
readonly WEB_RTC_ERROR: "WEB_RTC_ERROR";
|
|
12
11
|
readonly PROCESSING_ERROR: "PROCESSING_ERROR";
|
|
13
12
|
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
14
13
|
readonly INVALID_OPTIONS: "INVALID_OPTIONS";
|
|
@@ -18,6 +17,11 @@ declare const ERROR_CODES: {
|
|
|
18
17
|
readonly QUEUE_RESULT_ERROR: "QUEUE_RESULT_ERROR";
|
|
19
18
|
readonly JOB_NOT_COMPLETED: "JOB_NOT_COMPLETED";
|
|
20
19
|
readonly TOKEN_CREATE_ERROR: "TOKEN_CREATE_ERROR";
|
|
20
|
+
readonly WEBRTC_WEBSOCKET_ERROR: "WEBRTC_WEBSOCKET_ERROR";
|
|
21
|
+
readonly WEBRTC_ICE_ERROR: "WEBRTC_ICE_ERROR";
|
|
22
|
+
readonly WEBRTC_TIMEOUT_ERROR: "WEBRTC_TIMEOUT_ERROR";
|
|
23
|
+
readonly WEBRTC_SERVER_ERROR: "WEBRTC_SERVER_ERROR";
|
|
24
|
+
readonly WEBRTC_SIGNALING_ERROR: "WEBRTC_SIGNALING_ERROR";
|
|
21
25
|
};
|
|
22
26
|
//#endregion
|
|
23
27
|
export { DecartSDKError, ERROR_CODES };
|