@decartai/sdk 0.0.67 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +43 -28
- package/dist/process/client.js +1 -3
- package/dist/process/request.js +1 -3
- package/dist/queue/client.js +1 -3
- package/dist/queue/polling.js +1 -2
- package/dist/queue/request.js +1 -3
- package/dist/realtime/client.d.ts +23 -13
- package/dist/realtime/client.js +74 -244
- package/dist/realtime/config-realtime.js +49 -0
- package/dist/realtime/event-buffer.js +1 -3
- package/dist/realtime/initial-state-gate.js +21 -0
- package/dist/realtime/media-channel.js +82 -0
- package/dist/realtime/methods.js +12 -42
- package/dist/realtime/mirror-stream.js +1 -2
- package/dist/realtime/observability/diagnostics.d.ts +39 -0
- package/dist/realtime/observability/livekit-stats-provider.js +25 -0
- package/dist/realtime/observability/realtime-observability.js +173 -0
- package/dist/realtime/{telemetry-reporter.js → observability/telemetry-reporter.js} +12 -31
- package/dist/realtime/observability/webrtc-stats.d.ts +148 -0
- package/dist/realtime/observability/webrtc-stats.js +276 -0
- package/dist/realtime/signaling-channel.js +286 -0
- package/dist/realtime/stream-session.js +252 -0
- package/dist/realtime/subscribe-client.d.ts +2 -1
- package/dist/realtime/subscribe-client.js +115 -11
- package/dist/realtime/types.d.ts +25 -1
- package/dist/shared/model.d.ts +11 -1
- package/dist/shared/model.js +51 -14
- package/dist/shared/request.js +1 -3
- package/dist/shared/types.js +1 -3
- package/dist/tokens/client.js +1 -3
- package/dist/utils/env.js +1 -2
- package/dist/utils/errors.js +1 -2
- package/dist/utils/logger.js +1 -2
- package/dist/utils/media.js +43 -0
- package/dist/utils/platform.js +13 -0
- package/dist/utils/user-agent.js +1 -3
- package/dist/version.js +1 -2
- package/package.json +2 -1
- package/dist/realtime/diagnostics.d.ts +0 -78
- package/dist/realtime/webrtc-connection.js +0 -501
- package/dist/realtime/webrtc-manager.js +0 -189
- package/dist/realtime/webrtc-stats.d.ts +0 -59
- package/dist/realtime/webrtc-stats.js +0 -154
package/dist/realtime/client.js
CHANGED
|
@@ -1,86 +1,48 @@
|
|
|
1
1
|
import { classifyWebrtcError } from "../utils/errors.js";
|
|
2
|
-
import { modelDefinitionSchema } from "../shared/model.js";
|
|
2
|
+
import { modelDefinitionSchema, resolveFpsNumber } from "../shared/model.js";
|
|
3
3
|
import { modelStateSchema } from "../shared/types.js";
|
|
4
|
+
import { createConsoleLogger } from "../utils/logger.js";
|
|
5
|
+
import { imageToBase64 } from "../utils/media.js";
|
|
6
|
+
import { isDesktopSafari } from "../utils/platform.js";
|
|
4
7
|
import { createEventBuffer } from "./event-buffer.js";
|
|
5
8
|
import { realtimeMethods } from "./methods.js";
|
|
6
9
|
import { createMirroredStream, shouldMirrorTrack } from "./mirror-stream.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { WebRTCManager } from "./webrtc-manager.js";
|
|
10
|
-
import { WebRTCStatsCollector } from "./webrtc-stats.js";
|
|
10
|
+
import { RealtimeObservability } from "./observability/realtime-observability.js";
|
|
11
|
+
import { StreamSession } from "./stream-session.js";
|
|
11
12
|
import { z } from "zod";
|
|
12
|
-
|
|
13
13
|
//#region src/realtime/client.ts
|
|
14
|
-
async function blobToBase64(blob) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const reader = new FileReader();
|
|
17
|
-
reader.onloadend = () => {
|
|
18
|
-
const result = reader.result;
|
|
19
|
-
if (typeof result !== "string") {
|
|
20
|
-
reject(/* @__PURE__ */ new Error("FileReader did not return a string"));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const base64 = result.split(",")[1];
|
|
24
|
-
if (!base64) {
|
|
25
|
-
reject(/* @__PURE__ */ new Error("Invalid data URL format"));
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
resolve(base64);
|
|
29
|
-
};
|
|
30
|
-
reader.onerror = reject;
|
|
31
|
-
reader.readAsDataURL(blob);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
async function imageToBase64(image) {
|
|
35
|
-
if (typeof image === "string") {
|
|
36
|
-
let url = null;
|
|
37
|
-
try {
|
|
38
|
-
url = new URL(image);
|
|
39
|
-
} catch {}
|
|
40
|
-
if (url?.protocol === "data:") {
|
|
41
|
-
const [, base64] = image.split(",", 2);
|
|
42
|
-
if (!base64) throw new Error("Invalid data URL image");
|
|
43
|
-
return base64;
|
|
44
|
-
}
|
|
45
|
-
if (url?.protocol === "http:" || url?.protocol === "https:") {
|
|
46
|
-
const response = await fetch(image);
|
|
47
|
-
if (!response.ok) throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
|
48
|
-
return blobToBase64(await response.blob());
|
|
49
|
-
}
|
|
50
|
-
return image;
|
|
51
|
-
}
|
|
52
|
-
return blobToBase64(image);
|
|
53
|
-
}
|
|
54
14
|
const realTimeClientInitialStateSchema = modelStateSchema;
|
|
55
|
-
const createAsyncFunctionSchema = (schema) => z.custom((fn) => schema.implementAsync(fn));
|
|
56
15
|
const realTimeClientConnectOptionsSchema = z.object({
|
|
57
16
|
model: modelDefinitionSchema,
|
|
58
17
|
onRemoteStream: z.custom((val) => typeof val === "function", { message: "onRemoteStream must be a function" }),
|
|
18
|
+
onConnectionChange: z.custom((val) => typeof val === "function", { message: "onConnectionChange must be a function" }).optional(),
|
|
19
|
+
onQueuePosition: z.custom((val) => typeof val === "function", { message: "onQueuePosition must be a function" }).optional(),
|
|
59
20
|
initialState: realTimeClientInitialStateSchema.optional(),
|
|
60
|
-
|
|
61
|
-
mirror: z.union([z.literal("auto"), z.boolean()]).optional()
|
|
21
|
+
queryParams: z.record(z.string(), z.string()).optional(),
|
|
22
|
+
mirror: z.union([z.literal("auto"), z.boolean()]).optional(),
|
|
23
|
+
resolution: z.enum(["720p", "1080p"]).optional()
|
|
62
24
|
});
|
|
63
25
|
const createRealTimeClient = (opts) => {
|
|
64
|
-
const { baseUrl, apiKey, integration
|
|
26
|
+
const { baseUrl, apiKey, integration } = opts;
|
|
27
|
+
const logger = opts.logger ?? createConsoleLogger("info");
|
|
65
28
|
const connect = async (stream, options) => {
|
|
66
29
|
const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
|
|
67
30
|
if (!parsedOptions.success) throw parsedOptions.error;
|
|
68
|
-
const { onRemoteStream, initialState } = parsedOptions.data;
|
|
31
|
+
const { onRemoteStream, onConnectionChange, onQueuePosition, initialState, resolution } = parsedOptions.data;
|
|
69
32
|
const mirror = parsedOptions.data.mirror ?? false;
|
|
70
33
|
let inputStream = stream ?? new MediaStream();
|
|
71
34
|
let mirroredStream;
|
|
72
35
|
if (mirror !== false) try {
|
|
73
36
|
const firstVideoTrack = inputStream.getVideoTracks?.()[0];
|
|
74
37
|
if (firstVideoTrack && (mirror === true || shouldMirrorTrack(firstVideoTrack))) {
|
|
75
|
-
mirroredStream = createMirroredStream(inputStream, { fps: options.model.fps });
|
|
38
|
+
mirroredStream = createMirroredStream(inputStream, { fps: resolveFpsNumber(options.model.fps) });
|
|
76
39
|
inputStream = mirroredStream.stream;
|
|
77
40
|
} else if (mirror === true && !firstVideoTrack) logger.warn("mirror: true requested but no video track was found on the input stream");
|
|
78
41
|
} catch (error) {
|
|
79
42
|
logger.warn("Failed to mirror input stream; falling back to un-mirrored input", { error: error instanceof Error ? error.message : String(error) });
|
|
80
43
|
}
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
let handleConnectionStateChange = null;
|
|
44
|
+
let session;
|
|
45
|
+
let observability;
|
|
84
46
|
try {
|
|
85
47
|
const initialImage = initialState?.image ? await imageToBase64(initialState.image) : void 0;
|
|
86
48
|
const initialPrompt = initialState?.prompt ? {
|
|
@@ -89,149 +51,64 @@ const createRealTimeClient = (opts) => {
|
|
|
89
51
|
} : void 0;
|
|
90
52
|
const url = `${baseUrl}${options.model.urlPath}`;
|
|
91
53
|
const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer();
|
|
92
|
-
|
|
93
|
-
|
|
54
|
+
observability = new RealtimeObservability({
|
|
55
|
+
telemetryEnabled: opts.telemetryEnabled,
|
|
56
|
+
apiKey,
|
|
57
|
+
model: options.model.name,
|
|
94
58
|
integration,
|
|
95
59
|
logger,
|
|
96
|
-
onDiagnostic: (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
},
|
|
112
|
-
customizeOffer: options.customizeOffer,
|
|
113
|
-
vp8MinBitrate: 300,
|
|
114
|
-
vp8StartBitrate: 600,
|
|
60
|
+
onDiagnostic: (event) => emitOrBuffer("diagnostic", event),
|
|
61
|
+
onStats: (stats) => emitOrBuffer("stats", stats)
|
|
62
|
+
});
|
|
63
|
+
const safariCodec = isDesktopSafari() ? "vp8" : void 0;
|
|
64
|
+
session = new StreamSession({
|
|
65
|
+
url: `${url}?${new URLSearchParams({
|
|
66
|
+
...safariCodec ? { livekit_server_codec: safariCodec } : {},
|
|
67
|
+
...options.queryParams ?? {},
|
|
68
|
+
api_key: apiKey,
|
|
69
|
+
model: options.model.name,
|
|
70
|
+
...resolution ? { resolution } : {}
|
|
71
|
+
}).toString()}`,
|
|
72
|
+
integration,
|
|
73
|
+
observability,
|
|
74
|
+
localStream: inputStream,
|
|
115
75
|
initialImage,
|
|
116
|
-
initialPrompt
|
|
76
|
+
initialPrompt,
|
|
77
|
+
logger,
|
|
78
|
+
videoCodec: safariCodec
|
|
117
79
|
});
|
|
118
|
-
const manager = webrtcManager;
|
|
119
80
|
let sessionId = null;
|
|
120
81
|
let subscribeToken = null;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (telemetryReporterReady) telemetryReporter.stop();
|
|
144
|
-
const reporter = new TelemetryReporter({
|
|
145
|
-
apiKey,
|
|
146
|
-
sessionId: msg.session_id,
|
|
147
|
-
model: options.model.name,
|
|
148
|
-
integration,
|
|
149
|
-
logger
|
|
150
|
-
});
|
|
151
|
-
reporter.start();
|
|
152
|
-
telemetryReporter = reporter;
|
|
153
|
-
telemetryReporterReady = true;
|
|
154
|
-
for (const diagnostic of pendingTelemetryDiagnostics) telemetryReporter.addDiagnostic(diagnostic);
|
|
155
|
-
pendingTelemetryDiagnostics.length = 0;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
manager.getWebsocketMessageEmitter().on("sessionId", sessionIdListener);
|
|
159
|
-
const tickListener = (msg) => {
|
|
160
|
-
emitOrBuffer("generationTick", { seconds: msg.seconds });
|
|
161
|
-
};
|
|
162
|
-
manager.getWebsocketMessageEmitter().on("generationTick", tickListener);
|
|
163
|
-
await manager.connect(inputStream);
|
|
164
|
-
const methods = realtimeMethods(manager, imageToBase64);
|
|
165
|
-
let statsCollector = null;
|
|
166
|
-
let statsCollectorPeerConnection = null;
|
|
167
|
-
const STALL_FPS_THRESHOLD = .5;
|
|
168
|
-
let videoStalled = false;
|
|
169
|
-
let stallStartMs = 0;
|
|
170
|
-
const startStatsCollection = () => {
|
|
171
|
-
statsCollector?.stop();
|
|
172
|
-
videoStalled = false;
|
|
173
|
-
stallStartMs = 0;
|
|
174
|
-
statsCollector = new WebRTCStatsCollector();
|
|
175
|
-
const pc = manager.getPeerConnection();
|
|
176
|
-
statsCollectorPeerConnection = pc;
|
|
177
|
-
if (pc) statsCollector.start(pc, (stats) => {
|
|
178
|
-
emitOrBuffer("stats", stats);
|
|
179
|
-
telemetryReporter.addStats(stats);
|
|
180
|
-
const fps = stats.video?.framesPerSecond ?? 0;
|
|
181
|
-
if (!videoStalled && stats.video && fps < STALL_FPS_THRESHOLD) {
|
|
182
|
-
videoStalled = true;
|
|
183
|
-
stallStartMs = Date.now();
|
|
184
|
-
emitOrBuffer("diagnostic", {
|
|
185
|
-
name: "videoStall",
|
|
186
|
-
data: {
|
|
187
|
-
stalled: true,
|
|
188
|
-
durationMs: 0
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
addTelemetryDiagnostic("videoStall", {
|
|
192
|
-
stalled: true,
|
|
193
|
-
durationMs: 0
|
|
194
|
-
}, stallStartMs);
|
|
195
|
-
} else if (videoStalled && fps >= STALL_FPS_THRESHOLD) {
|
|
196
|
-
const durationMs = Date.now() - stallStartMs;
|
|
197
|
-
videoStalled = false;
|
|
198
|
-
emitOrBuffer("diagnostic", {
|
|
199
|
-
name: "videoStall",
|
|
200
|
-
data: {
|
|
201
|
-
stalled: false,
|
|
202
|
-
durationMs
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
addTelemetryDiagnostic("videoStall", {
|
|
206
|
-
stalled: false,
|
|
207
|
-
durationMs
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
return () => {
|
|
212
|
-
statsCollector?.stop();
|
|
213
|
-
statsCollector = null;
|
|
214
|
-
statsCollectorPeerConnection = null;
|
|
215
|
-
};
|
|
216
|
-
};
|
|
217
|
-
handleConnectionStateChange = (state) => {
|
|
218
|
-
if (!opts.telemetryEnabled) return;
|
|
219
|
-
if (state !== "connected" && state !== "generating") return;
|
|
220
|
-
const peerConnection = manager.getPeerConnection();
|
|
221
|
-
if (!peerConnection || peerConnection === statsCollectorPeerConnection) return;
|
|
222
|
-
startStatsCollection();
|
|
223
|
-
};
|
|
224
|
-
if (opts.telemetryEnabled) startStatsCollection();
|
|
82
|
+
session.on("remoteStream", onRemoteStream);
|
|
83
|
+
session.on("connectionChange", (state) => {
|
|
84
|
+
emitOrBuffer("connectionChange", state);
|
|
85
|
+
onConnectionChange?.(state);
|
|
86
|
+
});
|
|
87
|
+
session.on("queuePosition", (qp) => {
|
|
88
|
+
emitOrBuffer("queuePosition", qp);
|
|
89
|
+
onQueuePosition?.(qp);
|
|
90
|
+
});
|
|
91
|
+
session.on("sessionStarted", ({ sessionId: id, subscribeToken: token }) => {
|
|
92
|
+
sessionId = id;
|
|
93
|
+
subscribeToken = token;
|
|
94
|
+
observability?.sessionStarted(id);
|
|
95
|
+
});
|
|
96
|
+
session.on("generationTick", (e) => emitOrBuffer("generationTick", e));
|
|
97
|
+
session.on("generationEnded", (e) => emitOrBuffer("generationEnded", e));
|
|
98
|
+
session.on("error", (error) => {
|
|
99
|
+
logger.error("Realtime error", { error: error.message });
|
|
100
|
+
emitOrBuffer("error", classifyWebrtcError(error));
|
|
101
|
+
});
|
|
102
|
+
const activeSession = session;
|
|
103
|
+
await activeSession.connect();
|
|
225
104
|
const client = {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
getConnectionState: () => manager.getConnectionState(),
|
|
105
|
+
...realtimeMethods(activeSession, imageToBase64),
|
|
106
|
+
isConnected: () => activeSession.isConnected(),
|
|
107
|
+
getConnectionState: () => activeSession.getConnectionState(),
|
|
230
108
|
disconnect: () => {
|
|
231
|
-
|
|
232
|
-
telemetryReporter.stop();
|
|
109
|
+
observability?.stop();
|
|
233
110
|
stop();
|
|
234
|
-
|
|
111
|
+
activeSession.disconnect();
|
|
235
112
|
mirroredStream?.dispose();
|
|
236
113
|
},
|
|
237
114
|
on: eventEmitter.on,
|
|
@@ -242,70 +119,23 @@ const createRealTimeClient = (opts) => {
|
|
|
242
119
|
get subscribeToken() {
|
|
243
120
|
return subscribeToken;
|
|
244
121
|
},
|
|
245
|
-
|
|
246
|
-
|
|
122
|
+
getSubscribeToken: () => subscribeToken,
|
|
123
|
+
setImage: async (image, imgOptions) => {
|
|
124
|
+
if (image === null) return activeSession.setImage(null, imgOptions);
|
|
247
125
|
const base64 = await imageToBase64(image);
|
|
248
|
-
return
|
|
126
|
+
return activeSession.setImage(base64, imgOptions);
|
|
249
127
|
}
|
|
250
128
|
};
|
|
251
129
|
flush();
|
|
252
130
|
return client;
|
|
253
131
|
} catch (error) {
|
|
254
|
-
|
|
255
|
-
|
|
132
|
+
observability?.stop();
|
|
133
|
+
session?.disconnect();
|
|
256
134
|
mirroredStream?.dispose();
|
|
257
135
|
throw error;
|
|
258
136
|
}
|
|
259
137
|
};
|
|
260
|
-
|
|
261
|
-
const { sid, ip, port } = decodeSubscribeToken(options.token);
|
|
262
|
-
const subscribeUrl = `${baseUrl}/subscribe/${encodeURIComponent(sid)}?IP=${encodeURIComponent(ip)}&port=${encodeURIComponent(port)}&api_key=${encodeURIComponent(apiKey)}`;
|
|
263
|
-
const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer();
|
|
264
|
-
let webrtcManager;
|
|
265
|
-
try {
|
|
266
|
-
webrtcManager = new WebRTCManager({
|
|
267
|
-
webrtcUrl: subscribeUrl,
|
|
268
|
-
integration,
|
|
269
|
-
logger,
|
|
270
|
-
onDiagnostic: (name, data) => {
|
|
271
|
-
emitOrBuffer("diagnostic", {
|
|
272
|
-
name,
|
|
273
|
-
data
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
onRemoteStream: options.onRemoteStream,
|
|
277
|
-
onConnectionStateChange: (state) => {
|
|
278
|
-
emitOrBuffer("connectionChange", state);
|
|
279
|
-
},
|
|
280
|
-
onError: (error) => {
|
|
281
|
-
logger.error("WebRTC subscribe error", { error: error.message });
|
|
282
|
-
emitOrBuffer("error", classifyWebrtcError(error));
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
const manager = webrtcManager;
|
|
286
|
-
await manager.connect(null);
|
|
287
|
-
const client = {
|
|
288
|
-
isConnected: () => manager.isConnected(),
|
|
289
|
-
getConnectionState: () => manager.getConnectionState(),
|
|
290
|
-
disconnect: () => {
|
|
291
|
-
stop();
|
|
292
|
-
manager.cleanup();
|
|
293
|
-
},
|
|
294
|
-
on: eventEmitter.on,
|
|
295
|
-
off: eventEmitter.off
|
|
296
|
-
};
|
|
297
|
-
flush();
|
|
298
|
-
return client;
|
|
299
|
-
} catch (error) {
|
|
300
|
-
webrtcManager?.cleanup();
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
return {
|
|
305
|
-
connect,
|
|
306
|
-
subscribe
|
|
307
|
-
};
|
|
138
|
+
return { connect };
|
|
308
139
|
};
|
|
309
|
-
|
|
310
140
|
//#endregion
|
|
311
|
-
export { createRealTimeClient };
|
|
141
|
+
export { createRealTimeClient };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/realtime/config-realtime.ts
|
|
2
|
+
const REALTIME_CONFIG = {
|
|
3
|
+
signaling: {
|
|
4
|
+
connectTimeoutMs: 6e4,
|
|
5
|
+
handshakeTimeoutMs: 15e3,
|
|
6
|
+
requestTimeoutMs: 3e4
|
|
7
|
+
},
|
|
8
|
+
session: {
|
|
9
|
+
connectionTimeoutMs: 6e4 * 5,
|
|
10
|
+
retry: {
|
|
11
|
+
retries: 5,
|
|
12
|
+
factor: 2,
|
|
13
|
+
minTimeout: 1e3,
|
|
14
|
+
maxTimeout: 1e4
|
|
15
|
+
},
|
|
16
|
+
permanentErrorSubstrings: [
|
|
17
|
+
"permission denied",
|
|
18
|
+
"not allowed",
|
|
19
|
+
"invalid session",
|
|
20
|
+
"401",
|
|
21
|
+
"invalid api key",
|
|
22
|
+
"unauthorized"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
methods: {
|
|
26
|
+
promptTimeoutMs: 15e3,
|
|
27
|
+
updateTimeoutMs: 3e4
|
|
28
|
+
},
|
|
29
|
+
livekit: {
|
|
30
|
+
inferenceServerIdentityPrefix: "inference-server-",
|
|
31
|
+
roomOptions: {
|
|
32
|
+
adaptiveStream: false,
|
|
33
|
+
dynacast: false
|
|
34
|
+
},
|
|
35
|
+
defaultVideoCodec: "h264",
|
|
36
|
+
defaultMaxVideoBitrateBps: 35e5,
|
|
37
|
+
defaultPublishFps: 30
|
|
38
|
+
},
|
|
39
|
+
observability: {
|
|
40
|
+
stallFpsThreshold: .5,
|
|
41
|
+
statsDefaultIntervalMs: 1e3,
|
|
42
|
+
statsMinIntervalMs: 500,
|
|
43
|
+
telemetryReportIntervalMs: 1e4,
|
|
44
|
+
telemetryUrl: "https://platform.decart.ai/api/v1/telemetry",
|
|
45
|
+
telemetryMaxItemsPerReport: 120
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { REALTIME_CONFIG };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import mitt from "mitt";
|
|
2
|
-
|
|
3
2
|
//#region src/realtime/event-buffer.ts
|
|
4
3
|
function createEventBuffer() {
|
|
5
4
|
const emitter = mitt();
|
|
@@ -30,6 +29,5 @@ function createEventBuffer() {
|
|
|
30
29
|
stop
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
|
-
|
|
34
32
|
//#endregion
|
|
35
|
-
export { createEventBuffer };
|
|
33
|
+
export { createEventBuffer };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/realtime/initial-state-gate.ts
|
|
2
|
+
var InitialStateGate = class {
|
|
3
|
+
attemptId = 0;
|
|
4
|
+
startAttempt(initialState) {
|
|
5
|
+
const attemptId = ++this.attemptId;
|
|
6
|
+
const shouldWait = hasCallerProvidedInitialState(initialState);
|
|
7
|
+
return { waitForReadiness: async (initialStateAck) => {
|
|
8
|
+
if (shouldWait) await initialStateAck;
|
|
9
|
+
return this.attemptId === attemptId;
|
|
10
|
+
} };
|
|
11
|
+
}
|
|
12
|
+
reset() {
|
|
13
|
+
this.attemptId++;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function hasCallerProvidedInitialState(state) {
|
|
17
|
+
if (!state) return false;
|
|
18
|
+
return state.image !== void 0 && state.image !== null || state.prompt !== void 0 && state.prompt !== null;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { InitialStateGate };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createConsoleLogger } from "../utils/logger.js";
|
|
2
|
+
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
3
|
+
import mitt from "mitt";
|
|
4
|
+
import { Room, RoomEvent, Track, TrackEvent } from "livekit-client";
|
|
5
|
+
//#region src/realtime/media-channel.ts
|
|
6
|
+
function getDefaultVideoPublishOptions(videoCodec) {
|
|
7
|
+
const videoEncoding = {
|
|
8
|
+
maxBitrate: REALTIME_CONFIG.livekit.defaultMaxVideoBitrateBps,
|
|
9
|
+
maxFramerate: REALTIME_CONFIG.livekit.defaultPublishFps
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
source: Track.Source.Camera,
|
|
13
|
+
videoCodec: videoCodec ?? REALTIME_CONFIG.livekit.defaultVideoCodec,
|
|
14
|
+
simulcast: true,
|
|
15
|
+
videoEncoding
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var MediaChannel = class {
|
|
19
|
+
room = null;
|
|
20
|
+
remoteStream = null;
|
|
21
|
+
events = mitt();
|
|
22
|
+
logger;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = config.logger ?? createConsoleLogger("warn");
|
|
26
|
+
}
|
|
27
|
+
get localStream() {
|
|
28
|
+
return this.config.localStream;
|
|
29
|
+
}
|
|
30
|
+
on(event, handler) {
|
|
31
|
+
this.events.on(event, handler);
|
|
32
|
+
}
|
|
33
|
+
off(event, handler) {
|
|
34
|
+
this.events.off(event, handler);
|
|
35
|
+
}
|
|
36
|
+
async connect(opts) {
|
|
37
|
+
this.room ??= new Room(REALTIME_CONFIG.livekit.roomOptions);
|
|
38
|
+
const room = this.room;
|
|
39
|
+
room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
|
|
40
|
+
if (!participant.identity.startsWith(REALTIME_CONFIG.livekit.inferenceServerIdentityPrefix)) return;
|
|
41
|
+
if (track.kind !== Track.Kind.Video && track.kind !== Track.Kind.Audio) return;
|
|
42
|
+
track.attach();
|
|
43
|
+
const mediaStreamTrack = track.mediaStreamTrack;
|
|
44
|
+
if (mediaStreamTrack) {
|
|
45
|
+
this.remoteStream ??= new MediaStream();
|
|
46
|
+
if (!this.remoteStream.getTracks().includes(mediaStreamTrack)) this.remoteStream.addTrack(mediaStreamTrack);
|
|
47
|
+
this.events.emit("remoteStream", this.remoteStream);
|
|
48
|
+
}
|
|
49
|
+
track.on(TrackEvent.VideoPlaybackStarted, () => {
|
|
50
|
+
this.events.emit("firstFrame");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
room.on(RoomEvent.Disconnected, (reason) => {
|
|
54
|
+
this.logger.warn("livekit: room disconnected", { reason });
|
|
55
|
+
this.events.emit("disconnected", { reason });
|
|
56
|
+
});
|
|
57
|
+
this.config.observability?.startPhase("webrtc-handshake");
|
|
58
|
+
await room.connect(opts.url, opts.token);
|
|
59
|
+
this.config.observability?.endPhase("webrtc-handshake", { success: true });
|
|
60
|
+
this.config.observability?.setLiveKitRoom(room);
|
|
61
|
+
}
|
|
62
|
+
async publishLocalTracks() {
|
|
63
|
+
if (!this.config.localStream) return;
|
|
64
|
+
this.config.observability?.startPhase("publish-local-track");
|
|
65
|
+
await this.publishTracks(this.config.localStream);
|
|
66
|
+
this.config.observability?.endPhase("publish-local-track", { success: true });
|
|
67
|
+
}
|
|
68
|
+
disconnect() {
|
|
69
|
+
const room = this.room;
|
|
70
|
+
this.room = null;
|
|
71
|
+
this.remoteStream = null;
|
|
72
|
+
this.config.observability?.setLiveKitRoom(null);
|
|
73
|
+
if (room) room.disconnect().catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
async publishTracks(stream) {
|
|
76
|
+
if (!this.room) return;
|
|
77
|
+
for (const track of stream.getTracks()) if (track.kind === "video") await this.room.localParticipant.publishTrack(track, getDefaultVideoPublishOptions(this.config.videoCodec));
|
|
78
|
+
else await this.room.localParticipant.publishTrack(track);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { MediaChannel };
|
package/dist/realtime/methods.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
|
|
3
3
|
//#region src/realtime/methods.ts
|
|
4
|
-
const PROMPT_TIMEOUT_MS = 15 * 1e3;
|
|
5
|
-
const UPDATE_TIMEOUT_MS = 30 * 1e3;
|
|
6
4
|
const setInputSchema = z.object({
|
|
7
5
|
prompt: z.string().min(1).optional(),
|
|
8
6
|
enhance: z.boolean().optional().default(true),
|
|
@@ -17,61 +15,33 @@ const setPromptInputSchema = z.object({
|
|
|
17
15
|
prompt: z.string().min(1),
|
|
18
16
|
enhance: z.boolean().optional().default(true)
|
|
19
17
|
});
|
|
20
|
-
const realtimeMethods = (
|
|
21
|
-
const assertConnected = () => {
|
|
22
|
-
const state = webrtcManager.getConnectionState();
|
|
23
|
-
if (state !== "connected" && state !== "generating") throw new Error(`Cannot send message: connection is ${state}`);
|
|
24
|
-
};
|
|
18
|
+
const realtimeMethods = (session, imageToBase64) => {
|
|
25
19
|
const set = async (input) => {
|
|
26
|
-
assertConnected();
|
|
27
20
|
const parsed = setInputSchema.safeParse(input);
|
|
28
21
|
if (!parsed.success) throw parsed.error;
|
|
29
22
|
const { prompt, enhance, image } = parsed.data;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await webrtcManager.setImage(imageBase64, {
|
|
23
|
+
const imageBase64 = image !== void 0 && image !== null ? await imageToBase64(image) : null;
|
|
24
|
+
await session.setImage(imageBase64, {
|
|
33
25
|
prompt,
|
|
34
26
|
enhance,
|
|
35
|
-
timeout:
|
|
27
|
+
timeout: REALTIME_CONFIG.methods.updateTimeoutMs
|
|
36
28
|
});
|
|
37
29
|
};
|
|
38
30
|
const setPrompt = async (prompt, { enhance } = {}) => {
|
|
39
|
-
|
|
40
|
-
const parsedInput = setPromptInputSchema.safeParse({
|
|
31
|
+
const parsed = setPromptInputSchema.safeParse({
|
|
41
32
|
prompt,
|
|
42
33
|
enhance
|
|
43
34
|
});
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const ackPromise = new Promise((resolve, reject) => {
|
|
50
|
-
promptAckListener = (promptAckMessage) => {
|
|
51
|
-
if (promptAckMessage.prompt === parsedInput.data.prompt) if (promptAckMessage.success) resolve();
|
|
52
|
-
else reject(new Error(promptAckMessage.error ?? "Failed to send prompt"));
|
|
53
|
-
};
|
|
54
|
-
emitter.on("promptAck", promptAckListener);
|
|
55
|
-
});
|
|
56
|
-
if (!webrtcManager.sendMessage({
|
|
57
|
-
type: "prompt",
|
|
58
|
-
prompt: parsedInput.data.prompt,
|
|
59
|
-
enhance_prompt: parsedInput.data.enhance
|
|
60
|
-
})) throw new Error("WebSocket is not open");
|
|
61
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
62
|
-
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("Prompt timed out")), PROMPT_TIMEOUT_MS);
|
|
63
|
-
});
|
|
64
|
-
await Promise.race([ackPromise, timeoutPromise]);
|
|
65
|
-
} finally {
|
|
66
|
-
if (promptAckListener) emitter.off("promptAck", promptAckListener);
|
|
67
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
68
|
-
}
|
|
35
|
+
if (!parsed.success) throw parsed.error;
|
|
36
|
+
await session.sendPrompt(parsed.data.prompt, {
|
|
37
|
+
enhance: parsed.data.enhance,
|
|
38
|
+
timeout: REALTIME_CONFIG.methods.promptTimeoutMs
|
|
39
|
+
});
|
|
69
40
|
};
|
|
70
41
|
return {
|
|
71
42
|
set,
|
|
72
43
|
setPrompt
|
|
73
44
|
};
|
|
74
45
|
};
|
|
75
|
-
|
|
76
46
|
//#endregion
|
|
77
|
-
export { realtimeMethods };
|
|
47
|
+
export { realtimeMethods };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/realtime/observability/diagnostics.d.ts
|
|
2
|
+
type ClientSessionConnectionBreakdownPhase = {
|
|
3
|
+
phase: string;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
success: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
};
|
|
8
|
+
type ClientSessionConnectionBreakdownEvent = {
|
|
9
|
+
attempt: number;
|
|
10
|
+
success: boolean;
|
|
11
|
+
totalDurationMs: number;
|
|
12
|
+
initialImageSizeKb: number | null;
|
|
13
|
+
phases: ClientSessionConnectionBreakdownPhase[];
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
type ReconnectEvent = {
|
|
17
|
+
attempt: number;
|
|
18
|
+
maxAttempts: number;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
success: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
};
|
|
23
|
+
type VideoStallEvent = {
|
|
24
|
+
stalled: boolean;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
};
|
|
27
|
+
type DiagnosticEvents = {
|
|
28
|
+
"client-session-connection-breakdown": ClientSessionConnectionBreakdownEvent;
|
|
29
|
+
reconnect: ReconnectEvent;
|
|
30
|
+
videoStall: VideoStallEvent;
|
|
31
|
+
};
|
|
32
|
+
type DiagnosticEventName = keyof DiagnosticEvents;
|
|
33
|
+
type DiagnosticEventForName<K extends DiagnosticEventName> = {
|
|
34
|
+
name: K;
|
|
35
|
+
data: DiagnosticEvents[K];
|
|
36
|
+
};
|
|
37
|
+
type DiagnosticEvent = { [K in DiagnosticEventName]: DiagnosticEventForName<K> }[DiagnosticEventName];
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent };
|