@decartai/sdk 0.0.48 → 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 +8 -1
- package/dist/index.js +7 -2
- package/dist/realtime/client.d.ts +4 -0
- package/dist/realtime/client.js +122 -6
- 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 +155 -15
- package/dist/realtime/webrtc-manager.js +37 -3
- package/dist/realtime/webrtc-stats.d.ts +59 -0
- package/dist/realtime/webrtc-stats.js +154 -0
- 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 +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { LogLevel, Logger, createConsoleLogger, noopLogger } from "./utils/logger.js";
|
|
1
2
|
import { ImageModelDefinition, ImageModels, Model, ModelDefinition, RealTimeModels, VideoModelDefinition, VideoModels, isImageModel, isRealtimeModel, isVideoModel, models } from "./shared/model.js";
|
|
2
3
|
import { FileInput, ProcessOptions, ReactNativeFile } from "./process/types.js";
|
|
3
4
|
import { ProcessClient } from "./process/client.js";
|
|
4
5
|
import { JobStatus, JobStatusResponse, JobSubmitResponse, QueueJobResult, QueueSubmitAndPollOptions, QueueSubmitOptions } from "./queue/types.js";
|
|
5
6
|
import { QueueClient } from "./queue/client.js";
|
|
6
7
|
import { DecartSDKError, ERROR_CODES } from "./utils/errors.js";
|
|
8
|
+
import { ConnectionPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, IceCandidateEvent, IceStateEvent, PeerConnectionStateEvent, PhaseTimingEvent, ReconnectEvent, SelectedCandidatePairEvent, SignalingStateEvent, VideoStallEvent } from "./realtime/diagnostics.js";
|
|
7
9
|
import { ConnectionState } from "./realtime/types.js";
|
|
8
10
|
import { SetInput } from "./realtime/methods.js";
|
|
9
11
|
import { RealTimeSubscribeClient, SubscribeEvents, SubscribeOptions } from "./realtime/subscribe-client.js";
|
|
12
|
+
import { WebRTCStats } from "./realtime/webrtc-stats.js";
|
|
10
13
|
import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState } from "./realtime/client.js";
|
|
11
14
|
import { ModelState } from "./shared/types.js";
|
|
12
15
|
import { CreateTokenResponse, TokensClient } from "./tokens/client.js";
|
|
@@ -17,11 +20,15 @@ type DecartClientOptions = {
|
|
|
17
20
|
apiKey?: never;
|
|
18
21
|
baseUrl?: string;
|
|
19
22
|
integration?: string;
|
|
23
|
+
logger?: Logger;
|
|
24
|
+
telemetry?: boolean;
|
|
20
25
|
} | {
|
|
21
26
|
proxy?: never;
|
|
22
27
|
apiKey?: string;
|
|
23
28
|
baseUrl?: string;
|
|
24
29
|
integration?: string;
|
|
30
|
+
logger?: Logger;
|
|
31
|
+
telemetry?: boolean;
|
|
25
32
|
};
|
|
26
33
|
/**
|
|
27
34
|
* Create a Decart API client.
|
|
@@ -121,4 +128,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
|
|
|
121
128
|
tokens: TokensClient;
|
|
122
129
|
};
|
|
123
130
|
//#endregion
|
|
124
|
-
export { 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 RealTimeSubscribeClient, type SetInput, type SubscribeEvents, type SubscribeOptions, type TokensClient, type VideoModelDefinition, type VideoModels, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models };
|
|
131
|
+
export { type ConnectionPhase, type ConnectionState, type CreateTokenResponse, DecartClientOptions, type DecartSDKError, type DiagnosticEvent, type DiagnosticEventName, type DiagnosticEvents, ERROR_CODES, type FileInput, type IceCandidateEvent, type IceStateEvent, type ImageModelDefinition, type ImageModels, type JobStatus, type JobStatusResponse, type JobSubmitResponse, type LogLevel, type Logger, type Model, type ModelDefinition, type ModelState, type PeerConnectionStateEvent, type PhaseTimingEvent, 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 RealTimeSubscribeClient, type ReconnectEvent, type SelectedCandidatePairEvent, type SetInput, type SignalingStateEvent, type SubscribeEvents, type SubscribeOptions, type TokensClient, type VideoModelDefinition, type VideoModels, type VideoStallEvent, type WebRTCStats, createConsoleLogger, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models, noopLogger };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isImageModel, isRealtimeModel, isVideoModel, models } from "./shared/mo
|
|
|
5
5
|
import { createRealTimeClient } from "./realtime/client.js";
|
|
6
6
|
import { createTokensClient } from "./tokens/client.js";
|
|
7
7
|
import { readEnv } from "./utils/env.js";
|
|
8
|
+
import { createConsoleLogger, noopLogger } from "./utils/logger.js";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
|
|
10
11
|
//#region src/index.ts
|
|
@@ -56,11 +57,15 @@ const createDecartClient = (options = {}) => {
|
|
|
56
57
|
if (isProxyMode && "proxy" in parsedOptions.data && parsedOptions.data.proxy) baseUrl = parsedOptions.data.proxy;
|
|
57
58
|
else baseUrl = parsedOptions.data.baseUrl || "https://api.decart.ai";
|
|
58
59
|
const { integration } = parsedOptions.data;
|
|
60
|
+
const logger = "logger" in options && options.logger ? options.logger : noopLogger;
|
|
61
|
+
const telemetryEnabled = "telemetry" in options && options.telemetry === false ? false : true;
|
|
59
62
|
return {
|
|
60
63
|
realtime: createRealTimeClient({
|
|
61
64
|
baseUrl: "wss://api3.decart.ai",
|
|
62
65
|
apiKey: apiKey || "",
|
|
63
|
-
integration
|
|
66
|
+
integration,
|
|
67
|
+
logger,
|
|
68
|
+
telemetryEnabled
|
|
64
69
|
}),
|
|
65
70
|
process: createProcessClient({
|
|
66
71
|
baseUrl,
|
|
@@ -81,4 +86,4 @@ const createDecartClient = (options = {}) => {
|
|
|
81
86
|
};
|
|
82
87
|
|
|
83
88
|
//#endregion
|
|
84
|
-
export { ERROR_CODES, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models };
|
|
89
|
+
export { ERROR_CODES, createConsoleLogger, createDecartClient, isImageModel, isRealtimeModel, isVideoModel, models, noopLogger };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DecartSDKError } from "../utils/errors.js";
|
|
2
|
+
import { DiagnosticEvent } from "./diagnostics.js";
|
|
2
3
|
import { ConnectionState } from "./types.js";
|
|
3
4
|
import { SetInput } from "./methods.js";
|
|
5
|
+
import { WebRTCStats } from "./webrtc-stats.js";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
|
|
6
8
|
//#region src/realtime/client.d.ts
|
|
@@ -41,6 +43,8 @@ type Events = {
|
|
|
41
43
|
generationTick: {
|
|
42
44
|
seconds: number;
|
|
43
45
|
};
|
|
46
|
+
diagnostic: DiagnosticEvent;
|
|
47
|
+
stats: WebRTCStats;
|
|
44
48
|
};
|
|
45
49
|
type RealTimeClient = {
|
|
46
50
|
set: (input: SetInput) => Promise<void>;
|
package/dist/realtime/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { classifyWebrtcError } from "../utils/errors.js";
|
|
2
2
|
import { modelDefinitionSchema } from "../shared/model.js";
|
|
3
3
|
import { modelStateSchema } from "../shared/types.js";
|
|
4
4
|
import { AudioStreamManager } from "./audio-stream-manager.js";
|
|
5
5
|
import { createEventBuffer } from "./event-buffer.js";
|
|
6
6
|
import { realtimeMethods } from "./methods.js";
|
|
7
7
|
import { decodeSubscribeToken, encodeSubscribeToken } from "./subscribe-client.js";
|
|
8
|
+
import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
|
|
8
9
|
import { WebRTCManager } from "./webrtc-manager.js";
|
|
10
|
+
import { WebRTCStatsCollector } from "./webrtc-stats.js";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
|
|
11
13
|
//#region src/realtime/client.ts
|
|
@@ -58,7 +60,7 @@ const realTimeClientConnectOptionsSchema = z.object({
|
|
|
58
60
|
customizeOffer: createAsyncFunctionSchema(z.function()).optional()
|
|
59
61
|
});
|
|
60
62
|
const createRealTimeClient = (opts) => {
|
|
61
|
-
const { baseUrl, apiKey, integration } = opts;
|
|
63
|
+
const { baseUrl, apiKey, integration, logger } = opts;
|
|
62
64
|
const connect = async (stream, options) => {
|
|
63
65
|
const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
|
|
64
66
|
if (!parsedOptions.success) throw parsedOptions.error;
|
|
@@ -71,6 +73,8 @@ const createRealTimeClient = (opts) => {
|
|
|
71
73
|
inputStream = audioStreamManager.getStream();
|
|
72
74
|
} else inputStream = stream ?? new MediaStream();
|
|
73
75
|
let webrtcManager;
|
|
76
|
+
let telemetryReporter = new NullTelemetryReporter();
|
|
77
|
+
let handleConnectionStateChange = null;
|
|
74
78
|
try {
|
|
75
79
|
const initialImage = initialState?.image ? await imageToBase64(initialState.image) : void 0;
|
|
76
80
|
const initialPrompt = initialState?.prompt ? {
|
|
@@ -82,13 +86,22 @@ const createRealTimeClient = (opts) => {
|
|
|
82
86
|
webrtcManager = new WebRTCManager({
|
|
83
87
|
webrtcUrl: `${url}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}`,
|
|
84
88
|
integration,
|
|
89
|
+
logger,
|
|
90
|
+
onDiagnostic: (name, data) => {
|
|
91
|
+
emitOrBuffer("diagnostic", {
|
|
92
|
+
name,
|
|
93
|
+
data
|
|
94
|
+
});
|
|
95
|
+
addTelemetryDiagnostic(name, data);
|
|
96
|
+
},
|
|
85
97
|
onRemoteStream,
|
|
86
98
|
onConnectionStateChange: (state) => {
|
|
87
99
|
emitOrBuffer("connectionChange", state);
|
|
100
|
+
handleConnectionStateChange?.(state);
|
|
88
101
|
},
|
|
89
102
|
onError: (error) => {
|
|
90
|
-
|
|
91
|
-
emitOrBuffer("error",
|
|
103
|
+
logger.error("WebRTC error", { error: error.message });
|
|
104
|
+
emitOrBuffer("error", classifyWebrtcError(error));
|
|
92
105
|
},
|
|
93
106
|
customizeOffer: options.customizeOffer,
|
|
94
107
|
vp8MinBitrate: 300,
|
|
@@ -100,9 +113,42 @@ const createRealTimeClient = (opts) => {
|
|
|
100
113
|
const manager = webrtcManager;
|
|
101
114
|
let sessionId = null;
|
|
102
115
|
let subscribeToken = null;
|
|
116
|
+
const pendingTelemetryDiagnostics = [];
|
|
117
|
+
let telemetryReporterReady = false;
|
|
118
|
+
const addTelemetryDiagnostic = (name, data, timestamp = Date.now()) => {
|
|
119
|
+
if (!opts.telemetryEnabled) return;
|
|
120
|
+
if (!telemetryReporterReady) {
|
|
121
|
+
pendingTelemetryDiagnostics.push({
|
|
122
|
+
name,
|
|
123
|
+
data,
|
|
124
|
+
timestamp
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
telemetryReporter.addDiagnostic({
|
|
129
|
+
name,
|
|
130
|
+
data,
|
|
131
|
+
timestamp
|
|
132
|
+
});
|
|
133
|
+
};
|
|
103
134
|
const sessionIdListener = (msg) => {
|
|
104
135
|
subscribeToken = encodeSubscribeToken(msg.session_id, msg.server_ip, msg.server_port);
|
|
105
136
|
sessionId = msg.session_id;
|
|
137
|
+
if (opts.telemetryEnabled) {
|
|
138
|
+
if (telemetryReporterReady) telemetryReporter.stop();
|
|
139
|
+
const reporter = new TelemetryReporter({
|
|
140
|
+
apiKey,
|
|
141
|
+
sessionId: msg.session_id,
|
|
142
|
+
model: options.model.name,
|
|
143
|
+
integration,
|
|
144
|
+
logger
|
|
145
|
+
});
|
|
146
|
+
reporter.start();
|
|
147
|
+
telemetryReporter = reporter;
|
|
148
|
+
telemetryReporterReady = true;
|
|
149
|
+
for (const diagnostic of pendingTelemetryDiagnostics) telemetryReporter.addDiagnostic(diagnostic);
|
|
150
|
+
pendingTelemetryDiagnostics.length = 0;
|
|
151
|
+
}
|
|
106
152
|
};
|
|
107
153
|
manager.getWebsocketMessageEmitter().on("sessionId", sessionIdListener);
|
|
108
154
|
const tickListener = (msg) => {
|
|
@@ -111,12 +157,74 @@ const createRealTimeClient = (opts) => {
|
|
|
111
157
|
manager.getWebsocketMessageEmitter().on("generationTick", tickListener);
|
|
112
158
|
await manager.connect(inputStream);
|
|
113
159
|
const methods = realtimeMethods(manager, imageToBase64);
|
|
160
|
+
let statsCollector = null;
|
|
161
|
+
let statsCollectorPeerConnection = null;
|
|
162
|
+
const STALL_FPS_THRESHOLD = .5;
|
|
163
|
+
let videoStalled = false;
|
|
164
|
+
let stallStartMs = 0;
|
|
165
|
+
const startStatsCollection = () => {
|
|
166
|
+
statsCollector?.stop();
|
|
167
|
+
videoStalled = false;
|
|
168
|
+
stallStartMs = 0;
|
|
169
|
+
statsCollector = new WebRTCStatsCollector();
|
|
170
|
+
const pc = manager.getPeerConnection();
|
|
171
|
+
statsCollectorPeerConnection = pc;
|
|
172
|
+
if (pc) statsCollector.start(pc, (stats) => {
|
|
173
|
+
emitOrBuffer("stats", stats);
|
|
174
|
+
telemetryReporter.addStats(stats);
|
|
175
|
+
const fps = stats.video?.framesPerSecond ?? 0;
|
|
176
|
+
if (!videoStalled && stats.video && fps < STALL_FPS_THRESHOLD) {
|
|
177
|
+
videoStalled = true;
|
|
178
|
+
stallStartMs = Date.now();
|
|
179
|
+
emitOrBuffer("diagnostic", {
|
|
180
|
+
name: "videoStall",
|
|
181
|
+
data: {
|
|
182
|
+
stalled: true,
|
|
183
|
+
durationMs: 0
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
addTelemetryDiagnostic("videoStall", {
|
|
187
|
+
stalled: true,
|
|
188
|
+
durationMs: 0
|
|
189
|
+
}, stallStartMs);
|
|
190
|
+
} else if (videoStalled && fps >= STALL_FPS_THRESHOLD) {
|
|
191
|
+
const durationMs = Date.now() - stallStartMs;
|
|
192
|
+
videoStalled = false;
|
|
193
|
+
emitOrBuffer("diagnostic", {
|
|
194
|
+
name: "videoStall",
|
|
195
|
+
data: {
|
|
196
|
+
stalled: false,
|
|
197
|
+
durationMs
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
addTelemetryDiagnostic("videoStall", {
|
|
201
|
+
stalled: false,
|
|
202
|
+
durationMs
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return () => {
|
|
207
|
+
statsCollector?.stop();
|
|
208
|
+
statsCollector = null;
|
|
209
|
+
statsCollectorPeerConnection = null;
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
handleConnectionStateChange = (state) => {
|
|
213
|
+
if (!opts.telemetryEnabled) return;
|
|
214
|
+
if (state !== "connected" && state !== "generating") return;
|
|
215
|
+
const peerConnection = manager.getPeerConnection();
|
|
216
|
+
if (!peerConnection || peerConnection === statsCollectorPeerConnection) return;
|
|
217
|
+
startStatsCollection();
|
|
218
|
+
};
|
|
219
|
+
if (opts.telemetryEnabled) startStatsCollection();
|
|
114
220
|
const client = {
|
|
115
221
|
set: methods.set,
|
|
116
222
|
setPrompt: methods.setPrompt,
|
|
117
223
|
isConnected: () => manager.isConnected(),
|
|
118
224
|
getConnectionState: () => manager.getConnectionState(),
|
|
119
225
|
disconnect: () => {
|
|
226
|
+
statsCollector?.stop();
|
|
227
|
+
telemetryReporter.stop();
|
|
120
228
|
stop();
|
|
121
229
|
manager.cleanup();
|
|
122
230
|
audioStreamManager?.cleanup();
|
|
@@ -142,6 +250,7 @@ const createRealTimeClient = (opts) => {
|
|
|
142
250
|
flush();
|
|
143
251
|
return client;
|
|
144
252
|
} catch (error) {
|
|
253
|
+
telemetryReporter.stop();
|
|
145
254
|
webrtcManager?.cleanup();
|
|
146
255
|
audioStreamManager?.cleanup();
|
|
147
256
|
throw error;
|
|
@@ -156,13 +265,20 @@ const createRealTimeClient = (opts) => {
|
|
|
156
265
|
webrtcManager = new WebRTCManager({
|
|
157
266
|
webrtcUrl: subscribeUrl,
|
|
158
267
|
integration,
|
|
268
|
+
logger,
|
|
269
|
+
onDiagnostic: (name, data) => {
|
|
270
|
+
emitOrBuffer("diagnostic", {
|
|
271
|
+
name,
|
|
272
|
+
data
|
|
273
|
+
});
|
|
274
|
+
},
|
|
159
275
|
onRemoteStream: options.onRemoteStream,
|
|
160
276
|
onConnectionStateChange: (state) => {
|
|
161
277
|
emitOrBuffer("connectionChange", state);
|
|
162
278
|
},
|
|
163
279
|
onError: (error) => {
|
|
164
|
-
|
|
165
|
-
emitOrBuffer("error",
|
|
280
|
+
logger.error("WebRTC subscribe error", { error: error.message });
|
|
281
|
+
emitOrBuffer("error", classifyWebrtcError(error));
|
|
166
282
|
}
|
|
167
283
|
});
|
|
168
284
|
const manager = webrtcManager;
|
|
@@ -0,0 +1,78 @@
|
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import { DecartSDKError } from "../utils/errors.js";
|
|
2
|
+
import { DiagnosticEvent } from "./diagnostics.js";
|
|
2
3
|
import { ConnectionState } from "./types.js";
|
|
3
4
|
|
|
4
5
|
//#region src/realtime/subscribe-client.d.ts
|
|
@@ -6,6 +7,7 @@ import { ConnectionState } from "./types.js";
|
|
|
6
7
|
type SubscribeEvents = {
|
|
7
8
|
connectionChange: ConnectionState;
|
|
8
9
|
error: DecartSDKError;
|
|
10
|
+
diagnostic: DiagnosticEvent;
|
|
9
11
|
};
|
|
10
12
|
type RealTimeSubscribeClient = {
|
|
11
13
|
isConnected: () => boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { VERSION } from "../version.js";
|
|
2
|
+
import { buildAuthHeaders } from "../shared/request.js";
|
|
3
|
+
|
|
4
|
+
//#region src/realtime/telemetry-reporter.ts
|
|
5
|
+
const DEFAULT_REPORT_INTERVAL_MS = 1e4;
|
|
6
|
+
const TELEMETRY_URL = "https://platform.decart.ai/api/v1/telemetry";
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of items per array (stats / diagnostics) in a single report.
|
|
9
|
+
* Matches the backend Zod schema which enforces `z.array().max(120)`.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_ITEMS_PER_REPORT = 120;
|
|
12
|
+
/** No-op reporter that silently discards all data. Used when telemetry is disabled. */
|
|
13
|
+
var NullTelemetryReporter = class {
|
|
14
|
+
start() {}
|
|
15
|
+
addStats() {}
|
|
16
|
+
addDiagnostic() {}
|
|
17
|
+
flush() {}
|
|
18
|
+
stop() {}
|
|
19
|
+
};
|
|
20
|
+
var TelemetryReporter = class {
|
|
21
|
+
apiKey;
|
|
22
|
+
sessionId;
|
|
23
|
+
model;
|
|
24
|
+
integration;
|
|
25
|
+
logger;
|
|
26
|
+
reportIntervalMs;
|
|
27
|
+
intervalId = null;
|
|
28
|
+
statsBuffer = [];
|
|
29
|
+
diagnosticsBuffer = [];
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.apiKey = options.apiKey;
|
|
32
|
+
this.sessionId = options.sessionId;
|
|
33
|
+
this.model = options.model;
|
|
34
|
+
this.integration = options.integration;
|
|
35
|
+
this.logger = options.logger;
|
|
36
|
+
this.reportIntervalMs = options.reportIntervalMs ?? DEFAULT_REPORT_INTERVAL_MS;
|
|
37
|
+
}
|
|
38
|
+
/** Start the periodic reporting timer. */
|
|
39
|
+
start() {
|
|
40
|
+
if (this.intervalId !== null) return;
|
|
41
|
+
this.intervalId = setInterval(() => this.flush(), this.reportIntervalMs);
|
|
42
|
+
}
|
|
43
|
+
/** Add a stats snapshot to the buffer. */
|
|
44
|
+
addStats(stats) {
|
|
45
|
+
this.statsBuffer.push(stats);
|
|
46
|
+
}
|
|
47
|
+
/** Add a diagnostic event to the buffer. */
|
|
48
|
+
addDiagnostic(event) {
|
|
49
|
+
this.diagnosticsBuffer.push(event);
|
|
50
|
+
}
|
|
51
|
+
/** Flush buffered data immediately. */
|
|
52
|
+
flush() {
|
|
53
|
+
this.sendReport(false);
|
|
54
|
+
}
|
|
55
|
+
/** Stop the reporter and send a final report with keepalive. */
|
|
56
|
+
stop() {
|
|
57
|
+
if (this.intervalId !== null) {
|
|
58
|
+
clearInterval(this.intervalId);
|
|
59
|
+
this.intervalId = null;
|
|
60
|
+
}
|
|
61
|
+
this.sendReport(true);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a single chunk from the front of the buffers, respecting MAX_ITEMS_PER_REPORT.
|
|
65
|
+
* Returns null when both buffers are empty.
|
|
66
|
+
*/
|
|
67
|
+
createReportChunk() {
|
|
68
|
+
if (this.statsBuffer.length === 0 && this.diagnosticsBuffer.length === 0) return null;
|
|
69
|
+
const tags = {
|
|
70
|
+
session_id: this.sessionId,
|
|
71
|
+
sdk_version: VERSION,
|
|
72
|
+
...this.model ? { model: this.model } : {},
|
|
73
|
+
...this.integration ? { integration: this.integration } : {}
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
sessionId: this.sessionId,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
sdkVersion: VERSION,
|
|
79
|
+
...this.model ? { model: this.model } : {},
|
|
80
|
+
tags,
|
|
81
|
+
stats: this.statsBuffer.splice(0, MAX_ITEMS_PER_REPORT),
|
|
82
|
+
diagnostics: this.diagnosticsBuffer.splice(0, MAX_ITEMS_PER_REPORT)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
sendReport(keepalive) {
|
|
86
|
+
if (this.statsBuffer.length === 0 && this.diagnosticsBuffer.length === 0) return;
|
|
87
|
+
try {
|
|
88
|
+
const commonHeaders = {
|
|
89
|
+
...buildAuthHeaders({
|
|
90
|
+
apiKey: this.apiKey,
|
|
91
|
+
integration: this.integration
|
|
92
|
+
}),
|
|
93
|
+
"Content-Type": "application/json"
|
|
94
|
+
};
|
|
95
|
+
let chunk = this.createReportChunk();
|
|
96
|
+
while (chunk !== null) {
|
|
97
|
+
const isLast = this.statsBuffer.length === 0 && this.diagnosticsBuffer.length === 0;
|
|
98
|
+
fetch(TELEMETRY_URL, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: commonHeaders,
|
|
101
|
+
body: JSON.stringify(chunk),
|
|
102
|
+
keepalive: keepalive && isLast
|
|
103
|
+
}).then((response) => {
|
|
104
|
+
if (!response.ok) this.logger.warn("Telemetry report rejected", {
|
|
105
|
+
status: response.status,
|
|
106
|
+
statusText: response.statusText
|
|
107
|
+
});
|
|
108
|
+
}).catch((error) => {
|
|
109
|
+
this.logger.debug("Telemetry report failed", { error: String(error) });
|
|
110
|
+
});
|
|
111
|
+
chunk = this.createReportChunk();
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.logger.debug("Telemetry report failed", { error: String(error) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
export { NullTelemetryReporter, TelemetryReporter };
|
|
@@ -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,27 +79,65 @@ var WebRTCConnection = class {
|
|
|
53
79
|
rejectConnect(/* @__PURE__ */ new Error("WebSocket closed"));
|
|
54
80
|
};
|
|
55
81
|
}), connectAbort]);
|
|
56
|
-
if (this.callbacks.initialImage)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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();
|
|
61
103
|
await this.setupNewPeerConnection();
|
|
62
104
|
await Promise.race([new Promise((resolve, reject) => {
|
|
63
105
|
const checkConnection = setInterval(() => {
|
|
64
106
|
if (this.state === "connected" || this.state === "generating") {
|
|
65
107
|
clearInterval(checkConnection);
|
|
108
|
+
this.emitDiagnostic("phaseTiming", {
|
|
109
|
+
phase: "webrtc-handshake",
|
|
110
|
+
durationMs: performance.now() - handshakeStart,
|
|
111
|
+
success: true
|
|
112
|
+
});
|
|
66
113
|
resolve();
|
|
67
114
|
} else if (this.state === "disconnected") {
|
|
68
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
|
+
});
|
|
69
122
|
reject(/* @__PURE__ */ new Error("Connection lost during WebRTC handshake"));
|
|
70
123
|
} else if (Date.now() >= deadline) {
|
|
71
124
|
clearInterval(checkConnection);
|
|
125
|
+
this.emitDiagnostic("phaseTiming", {
|
|
126
|
+
phase: "webrtc-handshake",
|
|
127
|
+
durationMs: performance.now() - handshakeStart,
|
|
128
|
+
success: false,
|
|
129
|
+
error: "Timeout"
|
|
130
|
+
});
|
|
72
131
|
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
73
132
|
}
|
|
74
133
|
}, 100);
|
|
75
134
|
connectAbort.catch(() => clearInterval(checkConnection));
|
|
76
135
|
}), connectAbort]);
|
|
136
|
+
this.emitDiagnostic("phaseTiming", {
|
|
137
|
+
phase: "total",
|
|
138
|
+
durationMs: performance.now() - totalStart,
|
|
139
|
+
success: true
|
|
140
|
+
});
|
|
77
141
|
} finally {
|
|
78
142
|
this.connectionReject = null;
|
|
79
143
|
}
|
|
@@ -82,6 +146,7 @@ var WebRTCConnection = class {
|
|
|
82
146
|
try {
|
|
83
147
|
if (msg.type === "error") {
|
|
84
148
|
const error = new Error(msg.error);
|
|
149
|
+
error.source = "server";
|
|
85
150
|
this.callbacks.onError?.(error);
|
|
86
151
|
if (this.connectionReject) {
|
|
87
152
|
this.connectionReject(error);
|
|
@@ -144,7 +209,14 @@ var WebRTCConnection = class {
|
|
|
144
209
|
});
|
|
145
210
|
break;
|
|
146
211
|
case "ice-candidate":
|
|
147
|
-
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
|
+
}
|
|
148
220
|
break;
|
|
149
221
|
case "ice-restart": {
|
|
150
222
|
const turnConfig = msg.turn_config;
|
|
@@ -153,7 +225,7 @@ var WebRTCConnection = class {
|
|
|
153
225
|
}
|
|
154
226
|
}
|
|
155
227
|
} catch (error) {
|
|
156
|
-
|
|
228
|
+
this.logger.error("Signaling handler error", { error: String(error) });
|
|
157
229
|
this.callbacks.onError?.(error);
|
|
158
230
|
this.connectionReject?.(error);
|
|
159
231
|
}
|
|
@@ -163,7 +235,7 @@ var WebRTCConnection = class {
|
|
|
163
235
|
this.ws.send(JSON.stringify(message));
|
|
164
236
|
return true;
|
|
165
237
|
}
|
|
166
|
-
|
|
238
|
+
this.logger.warn("Message dropped: WebSocket is not open");
|
|
167
239
|
return false;
|
|
168
240
|
}
|
|
169
241
|
async setImageBase64(imageBase64, options) {
|
|
@@ -265,22 +337,90 @@ var WebRTCConnection = class {
|
|
|
265
337
|
type: "ice-candidate",
|
|
266
338
|
candidate: e.candidate
|
|
267
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
|
+
});
|
|
268
347
|
};
|
|
348
|
+
let prevPcState = "new";
|
|
269
349
|
this.pc.onconnectionstatechange = () => {
|
|
270
350
|
if (!this.pc) return;
|
|
271
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();
|
|
272
359
|
const nextState = s === "connected" ? "connected" : ["connecting", "new"].includes(s) ? "connecting" : "disconnected";
|
|
273
360
|
if (this.state === "generating" && nextState !== "disconnected") return;
|
|
274
361
|
this.setState(nextState);
|
|
275
362
|
};
|
|
363
|
+
let prevIceState = "new";
|
|
276
364
|
this.pc.oniceconnectionstatechange = () => {
|
|
277
|
-
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") {
|
|
278
374
|
this.setState("disconnected");
|
|
279
375
|
this.callbacks.onError?.(/* @__PURE__ */ new Error("ICE connection failed"));
|
|
280
376
|
}
|
|
281
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
|
+
};
|
|
282
389
|
this.handleSignalingMessage({ type: "ready" });
|
|
283
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
|
+
}
|
|
284
424
|
cleanup() {
|
|
285
425
|
this.pc?.close();
|
|
286
426
|
this.pc = null;
|
|
@@ -292,17 +432,17 @@ var WebRTCConnection = class {
|
|
|
292
432
|
applyCodecPreference(preferredCodecName) {
|
|
293
433
|
if (!this.pc) return;
|
|
294
434
|
if (typeof RTCRtpSender === "undefined" || typeof RTCRtpSender.getCapabilities !== "function") {
|
|
295
|
-
|
|
435
|
+
this.logger.debug("RTCRtpSender capabilities not available in this environment");
|
|
296
436
|
return;
|
|
297
437
|
}
|
|
298
438
|
const videoTransceiver = this.pc.getTransceivers().find((r) => r.sender.track?.kind === "video" || r.receiver.track?.kind === "video");
|
|
299
439
|
if (!videoTransceiver) {
|
|
300
|
-
|
|
440
|
+
this.logger.warn("Video transceiver not found for codec preference");
|
|
301
441
|
return;
|
|
302
442
|
}
|
|
303
443
|
const capabilities = RTCRtpSender.getCapabilities("video");
|
|
304
444
|
if (!capabilities) {
|
|
305
|
-
|
|
445
|
+
this.logger.warn("Video sender capabilities unavailable");
|
|
306
446
|
return;
|
|
307
447
|
}
|
|
308
448
|
const preferredCodecs = [];
|
|
@@ -313,13 +453,13 @@ var WebRTCConnection = class {
|
|
|
313
453
|
});
|
|
314
454
|
const orderedCodecs = [...preferredCodecs, ...otherCodecs];
|
|
315
455
|
if (orderedCodecs.length === 0) {
|
|
316
|
-
|
|
456
|
+
this.logger.debug("No video codecs found for preference setting");
|
|
317
457
|
return;
|
|
318
458
|
}
|
|
319
459
|
try {
|
|
320
460
|
videoTransceiver.setCodecPreferences(orderedCodecs);
|
|
321
461
|
} catch {
|
|
322
|
-
|
|
462
|
+
this.logger.debug("setCodecPreferences not supported, skipping");
|
|
323
463
|
}
|
|
324
464
|
}
|
|
325
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),
|
|
@@ -38,7 +45,9 @@ var WebRTCManager = class {
|
|
|
38
45
|
vp8StartBitrate: config.vp8StartBitrate,
|
|
39
46
|
modelName: config.modelName,
|
|
40
47
|
initialImage: config.initialImage,
|
|
41
|
-
initialPrompt: config.initialPrompt
|
|
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/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 };
|
package/dist/utils/errors.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const ERROR_CODES = {
|
|
3
3
|
INVALID_API_KEY: "INVALID_API_KEY",
|
|
4
4
|
INVALID_BASE_URL: "INVALID_BASE_URL",
|
|
5
|
-
WEB_RTC_ERROR: "WEB_RTC_ERROR",
|
|
6
5
|
PROCESSING_ERROR: "PROCESSING_ERROR",
|
|
7
6
|
INVALID_INPUT: "INVALID_INPUT",
|
|
8
7
|
INVALID_OPTIONS: "INVALID_OPTIONS",
|
|
@@ -11,7 +10,12 @@ const ERROR_CODES = {
|
|
|
11
10
|
QUEUE_STATUS_ERROR: "QUEUE_STATUS_ERROR",
|
|
12
11
|
QUEUE_RESULT_ERROR: "QUEUE_RESULT_ERROR",
|
|
13
12
|
JOB_NOT_COMPLETED: "JOB_NOT_COMPLETED",
|
|
14
|
-
TOKEN_CREATE_ERROR: "TOKEN_CREATE_ERROR"
|
|
13
|
+
TOKEN_CREATE_ERROR: "TOKEN_CREATE_ERROR",
|
|
14
|
+
WEBRTC_WEBSOCKET_ERROR: "WEBRTC_WEBSOCKET_ERROR",
|
|
15
|
+
WEBRTC_ICE_ERROR: "WEBRTC_ICE_ERROR",
|
|
16
|
+
WEBRTC_TIMEOUT_ERROR: "WEBRTC_TIMEOUT_ERROR",
|
|
17
|
+
WEBRTC_SERVER_ERROR: "WEBRTC_SERVER_ERROR",
|
|
18
|
+
WEBRTC_SIGNALING_ERROR: "WEBRTC_SIGNALING_ERROR"
|
|
15
19
|
};
|
|
16
20
|
function createSDKError(code, message, data, cause) {
|
|
17
21
|
return {
|
|
@@ -27,8 +31,38 @@ function createInvalidApiKeyError() {
|
|
|
27
31
|
function createInvalidBaseUrlError(url) {
|
|
28
32
|
return createSDKError(ERROR_CODES.INVALID_BASE_URL, `Invalid base URL${url ? `: ${url}` : ""}`);
|
|
29
33
|
}
|
|
30
|
-
function
|
|
31
|
-
return createSDKError(ERROR_CODES.
|
|
34
|
+
function createWebrtcWebsocketError(error) {
|
|
35
|
+
return createSDKError(ERROR_CODES.WEBRTC_WEBSOCKET_ERROR, "WebSocket connection failed", void 0, error);
|
|
36
|
+
}
|
|
37
|
+
function createWebrtcIceError(error) {
|
|
38
|
+
return createSDKError(ERROR_CODES.WEBRTC_ICE_ERROR, "ICE connection failed", void 0, error);
|
|
39
|
+
}
|
|
40
|
+
function createWebrtcTimeoutError(phase, timeoutMs, cause) {
|
|
41
|
+
const hasTimeout = typeof timeoutMs === "number" && Number.isFinite(timeoutMs);
|
|
42
|
+
return createSDKError(ERROR_CODES.WEBRTC_TIMEOUT_ERROR, hasTimeout ? `${phase} timed out after ${timeoutMs}ms` : `${phase} timed out`, hasTimeout ? {
|
|
43
|
+
phase,
|
|
44
|
+
timeoutMs
|
|
45
|
+
} : { phase }, cause);
|
|
46
|
+
}
|
|
47
|
+
function createWebrtcServerError(message) {
|
|
48
|
+
return createSDKError(ERROR_CODES.WEBRTC_SERVER_ERROR, message);
|
|
49
|
+
}
|
|
50
|
+
function createWebrtcSignalingError(error) {
|
|
51
|
+
return createSDKError(ERROR_CODES.WEBRTC_SIGNALING_ERROR, "Signaling error", void 0, error);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Classify a raw WebRTC error into a specific SDK error based on its message.
|
|
55
|
+
*/
|
|
56
|
+
function classifyWebrtcError(error) {
|
|
57
|
+
const msg = error.message.toLowerCase();
|
|
58
|
+
if (error.source === "server") return createWebrtcServerError(error.message);
|
|
59
|
+
if (msg.includes("websocket")) return createWebrtcWebsocketError(error);
|
|
60
|
+
if (msg.includes("ice connection failed")) return createWebrtcIceError(error);
|
|
61
|
+
if (msg.includes("timeout") || msg.includes("timed out")) {
|
|
62
|
+
const timeoutMatch = msg.match(/(\d+)\s*ms/);
|
|
63
|
+
return createWebrtcTimeoutError("connection", timeoutMatch ? Number.parseInt(timeoutMatch[1], 10) : void 0, error);
|
|
64
|
+
}
|
|
65
|
+
return createWebrtcSignalingError(error);
|
|
32
66
|
}
|
|
33
67
|
function createInvalidInputError(message) {
|
|
34
68
|
return createSDKError(ERROR_CODES.INVALID_INPUT, message);
|
|
@@ -47,4 +81,4 @@ function createQueueResultError(message, status) {
|
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
//#endregion
|
|
50
|
-
export { ERROR_CODES, createInvalidApiKeyError, createInvalidBaseUrlError, createInvalidInputError, createModelNotFoundError, createQueueResultError, createQueueStatusError, createQueueSubmitError, createSDKError
|
|
84
|
+
export { ERROR_CODES, classifyWebrtcError, createInvalidApiKeyError, createInvalidBaseUrlError, createInvalidInputError, createModelNotFoundError, createQueueResultError, createQueueStatusError, createQueueSubmitError, createSDKError };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/utils/logger.d.ts
|
|
2
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
3
|
+
type Logger = {
|
|
4
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
5
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
6
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
7
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
8
|
+
};
|
|
9
|
+
/** A logger that discards all messages. Zero overhead. */
|
|
10
|
+
declare const noopLogger: Logger;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a console-based logger that only logs messages at or above the given level.
|
|
13
|
+
*
|
|
14
|
+
* @param minLevel - Minimum log level to output. Default: "warn".
|
|
15
|
+
*/
|
|
16
|
+
declare function createConsoleLogger(minLevel?: LogLevel): Logger;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { LogLevel, Logger, createConsoleLogger, noopLogger };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/utils/logger.ts
|
|
2
|
+
/** A logger that discards all messages. Zero overhead. */
|
|
3
|
+
const noopLogger = {
|
|
4
|
+
debug() {},
|
|
5
|
+
info() {},
|
|
6
|
+
warn() {},
|
|
7
|
+
error() {}
|
|
8
|
+
};
|
|
9
|
+
const LOG_LEVELS = {
|
|
10
|
+
debug: 0,
|
|
11
|
+
info: 1,
|
|
12
|
+
warn: 2,
|
|
13
|
+
error: 3
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a console-based logger that only logs messages at or above the given level.
|
|
17
|
+
*
|
|
18
|
+
* @param minLevel - Minimum log level to output. Default: "warn".
|
|
19
|
+
*/
|
|
20
|
+
function createConsoleLogger(minLevel = "warn") {
|
|
21
|
+
const threshold = LOG_LEVELS[minLevel];
|
|
22
|
+
const log = (level, message, data) => {
|
|
23
|
+
if (LOG_LEVELS[level] < threshold) return;
|
|
24
|
+
const prefix = "[DecartSDK]";
|
|
25
|
+
if (data) console[level](prefix, message, data);
|
|
26
|
+
else console[level](prefix, message);
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
30
|
+
info: (msg, data) => log("info", msg, data),
|
|
31
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
32
|
+
error: (msg, data) => log("error", msg, data)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { createConsoleLogger, noopLogger };
|