@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.
Files changed (45) hide show
  1. package/README.md +55 -5
  2. package/dist/index.d.ts +7 -5
  3. package/dist/index.js +43 -28
  4. package/dist/process/client.js +1 -3
  5. package/dist/process/request.js +1 -3
  6. package/dist/queue/client.js +1 -3
  7. package/dist/queue/polling.js +1 -2
  8. package/dist/queue/request.js +1 -3
  9. package/dist/realtime/client.d.ts +23 -13
  10. package/dist/realtime/client.js +74 -244
  11. package/dist/realtime/config-realtime.js +49 -0
  12. package/dist/realtime/event-buffer.js +1 -3
  13. package/dist/realtime/initial-state-gate.js +21 -0
  14. package/dist/realtime/media-channel.js +82 -0
  15. package/dist/realtime/methods.js +12 -42
  16. package/dist/realtime/mirror-stream.js +1 -2
  17. package/dist/realtime/observability/diagnostics.d.ts +39 -0
  18. package/dist/realtime/observability/livekit-stats-provider.js +25 -0
  19. package/dist/realtime/observability/realtime-observability.js +173 -0
  20. package/dist/realtime/{telemetry-reporter.js → observability/telemetry-reporter.js} +12 -31
  21. package/dist/realtime/observability/webrtc-stats.d.ts +148 -0
  22. package/dist/realtime/observability/webrtc-stats.js +276 -0
  23. package/dist/realtime/signaling-channel.js +286 -0
  24. package/dist/realtime/stream-session.js +252 -0
  25. package/dist/realtime/subscribe-client.d.ts +2 -1
  26. package/dist/realtime/subscribe-client.js +115 -11
  27. package/dist/realtime/types.d.ts +25 -1
  28. package/dist/shared/model.d.ts +11 -1
  29. package/dist/shared/model.js +51 -14
  30. package/dist/shared/request.js +1 -3
  31. package/dist/shared/types.js +1 -3
  32. package/dist/tokens/client.js +1 -3
  33. package/dist/utils/env.js +1 -2
  34. package/dist/utils/errors.js +1 -2
  35. package/dist/utils/logger.js +1 -2
  36. package/dist/utils/media.js +43 -0
  37. package/dist/utils/platform.js +13 -0
  38. package/dist/utils/user-agent.js +1 -3
  39. package/dist/version.js +1 -2
  40. package/package.json +2 -1
  41. package/dist/realtime/diagnostics.d.ts +0 -78
  42. package/dist/realtime/webrtc-connection.js +0 -501
  43. package/dist/realtime/webrtc-manager.js +0 -189
  44. package/dist/realtime/webrtc-stats.d.ts +0 -59
  45. package/dist/realtime/webrtc-stats.js +0 -154
@@ -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 { decodeSubscribeToken, encodeSubscribeToken } from "./subscribe-client.js";
8
- import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
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
- customizeOffer: createAsyncFunctionSchema(z.function()).optional(),
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, logger } = opts;
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 webrtcManager;
82
- let telemetryReporter = new NullTelemetryReporter();
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
- webrtcManager = new WebRTCManager({
93
- webrtcUrl: `${url}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}`,
54
+ observability = new RealtimeObservability({
55
+ telemetryEnabled: opts.telemetryEnabled,
56
+ apiKey,
57
+ model: options.model.name,
94
58
  integration,
95
59
  logger,
96
- onDiagnostic: (name, data) => {
97
- emitOrBuffer("diagnostic", {
98
- name,
99
- data
100
- });
101
- addTelemetryDiagnostic(name, data);
102
- },
103
- onRemoteStream,
104
- onConnectionStateChange: (state) => {
105
- emitOrBuffer("connectionChange", state);
106
- handleConnectionStateChange?.(state);
107
- },
108
- onError: (error) => {
109
- logger.error("WebRTC error", { error: error.message });
110
- emitOrBuffer("error", classifyWebrtcError(error));
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
- const pendingTelemetryDiagnostics = [];
122
- let telemetryReporterReady = false;
123
- const addTelemetryDiagnostic = (name, data, timestamp = Date.now()) => {
124
- if (!opts.telemetryEnabled) return;
125
- if (!telemetryReporterReady) {
126
- pendingTelemetryDiagnostics.push({
127
- name,
128
- data,
129
- timestamp
130
- });
131
- return;
132
- }
133
- telemetryReporter.addDiagnostic({
134
- name,
135
- data,
136
- timestamp
137
- });
138
- };
139
- const sessionIdListener = (msg) => {
140
- subscribeToken = encodeSubscribeToken(msg.session_id, msg.server_ip, msg.server_port);
141
- sessionId = msg.session_id;
142
- if (opts.telemetryEnabled) {
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
- set: methods.set,
227
- setPrompt: methods.setPrompt,
228
- isConnected: () => manager.isConnected(),
229
- getConnectionState: () => manager.getConnectionState(),
105
+ ...realtimeMethods(activeSession, imageToBase64),
106
+ isConnected: () => activeSession.isConnected(),
107
+ getConnectionState: () => activeSession.getConnectionState(),
230
108
  disconnect: () => {
231
- statsCollector?.stop();
232
- telemetryReporter.stop();
109
+ observability?.stop();
233
110
  stop();
234
- manager.cleanup();
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
- setImage: async (image, options) => {
246
- if (image === null) return manager.setImage(null, options);
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 manager.setImage(base64, options);
126
+ return activeSession.setImage(base64, imgOptions);
249
127
  }
250
128
  };
251
129
  flush();
252
130
  return client;
253
131
  } catch (error) {
254
- telemetryReporter.stop();
255
- webrtcManager?.cleanup();
132
+ observability?.stop();
133
+ session?.disconnect();
256
134
  mirroredStream?.dispose();
257
135
  throw error;
258
136
  }
259
137
  };
260
- const subscribe = async (options) => {
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 };
@@ -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 = (webrtcManager, imageToBase64) => {
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
- let imageBase64 = null;
31
- if (image !== void 0 && image !== null) imageBase64 = await imageToBase64(image);
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: UPDATE_TIMEOUT_MS
27
+ timeout: REALTIME_CONFIG.methods.updateTimeoutMs
36
28
  });
37
29
  };
38
30
  const setPrompt = async (prompt, { enhance } = {}) => {
39
- assertConnected();
40
- const parsedInput = setPromptInputSchema.safeParse({
31
+ const parsed = setPromptInputSchema.safeParse({
41
32
  prompt,
42
33
  enhance
43
34
  });
44
- if (!parsedInput.success) throw parsedInput.error;
45
- const emitter = webrtcManager.getWebsocketMessageEmitter();
46
- let promptAckListener;
47
- let timeoutId;
48
- try {
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 };
@@ -111,6 +111,5 @@ function createWithCanvas(sourceVideo, audioTracks, fps) {
111
111
  }
112
112
  };
113
113
  }
114
-
115
114
  //#endregion
116
- export { createMirroredStream, shouldMirrorTrack };
115
+ export { createMirroredStream, shouldMirrorTrack };
@@ -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 };