@decartai/sdk 0.1.13 → 0.1.15

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 (53) hide show
  1. package/README.md +109 -10
  2. package/dist/create-client.d.ts +46 -0
  3. package/dist/create-client.js +208 -0
  4. package/dist/files/types.js +4 -1
  5. package/dist/index.d.ts +8 -150
  6. package/dist/index.js +6 -116
  7. package/dist/index.react-native.d.ts +36 -0
  8. package/dist/index.react-native.js +9 -0
  9. package/dist/package.js +4 -0
  10. package/dist/process/types.d.ts +2 -2
  11. package/dist/realtime/browser/frame-metadata-diagnostics.js +90 -0
  12. package/dist/realtime/browser/frame-metadata-worker.js +2 -0
  13. package/dist/realtime/browser/index.js +32 -0
  14. package/dist/realtime/{mirror-stream.js → browser/mirror-stream.js} +2 -2
  15. package/dist/realtime/{preflight.js → browser/preflight.js} +20 -115
  16. package/dist/realtime/browser/prepare-connection.js +57 -0
  17. package/dist/realtime/client.d.ts +11 -4
  18. package/dist/realtime/client.js +42 -37
  19. package/dist/realtime/config-realtime.js +51 -0
  20. package/dist/realtime/livekit.js +26 -0
  21. package/dist/realtime/media-channel.js +40 -11
  22. package/dist/realtime/methods.d.ts +1 -1
  23. package/dist/realtime/methods.js +7 -2
  24. package/dist/realtime/observability/connection-quality.d.ts +3 -3
  25. package/dist/realtime/observability/connection-quality.js +3 -1
  26. package/dist/realtime/observability/g2g.d.ts +14 -0
  27. package/dist/realtime/observability/realtime-observability.js +8 -32
  28. package/dist/realtime/observability/webrtc-stats.d.ts +4 -4
  29. package/dist/realtime/preflight-connectivity.js +102 -0
  30. package/dist/realtime/preflight-types.d.ts +30 -0
  31. package/dist/realtime/react-native/index.js +36 -0
  32. package/dist/realtime/react-native/preflight.js +10 -0
  33. package/dist/realtime/react-native/prepare-connection.js +26 -0
  34. package/dist/realtime/signaling-channel.js +3 -1
  35. package/dist/realtime/stream-session.js +5 -4
  36. package/dist/realtime/subscribe-client.js +11 -10
  37. package/dist/shared/model.d.ts +56 -37
  38. package/dist/shared/model.js +61 -18
  39. package/dist/shared/request.js +5 -4
  40. package/dist/shared/types.d.ts +2 -2
  41. package/dist/shared/types.js +9 -2
  42. package/dist/utils/errors.d.ts +3 -0
  43. package/dist/utils/errors.js +16 -1
  44. package/dist/utils/media.js +1 -1
  45. package/dist/utils/platform.js +17 -1
  46. package/dist/utils/runtime.js +13 -0
  47. package/dist/utils/user-agent.js +1 -1
  48. package/dist/version.js +9 -4
  49. package/package.json +10 -3
  50. package/dist/realtime/observability/glass-to-glass.d.ts +0 -41
  51. package/dist/realtime/observability/glass-to-glass.js +0 -229
  52. package/dist/realtime/observability/pixel-marker.js +0 -144
  53. package/dist/realtime/preflight.d.ts +0 -59
@@ -1,109 +1,8 @@
1
- import { resolveFpsNumber } from "../shared/model.js";
2
- import { REALTIME_CONFIG } from "./config-realtime.js";
3
- import { extractSignals, scoreLowerBetter, worst } from "./observability/connection-quality.js";
4
- //#region src/realtime/preflight.ts
5
- /** Extract the candidate type ("host" | "srflx" | "prflx" | "relay") from an ICE candidate. */
6
- function candidateType(candidate) {
7
- if (candidate.type) return candidate.type;
8
- return /\btyp (\w+)/.exec(candidate.candidate)?.[1] ?? "";
9
- }
10
- async function gatherIceCandidates(iceServers, timeoutMs, signal, logger) {
11
- if (signal?.aborted) return {
12
- transport: "failed",
13
- rttMs: null
14
- };
15
- const PC = globalThis?.RTCPeerConnection;
16
- if (typeof PC !== "function") {
17
- logger.warn("preflight: RTCPeerConnection unavailable in this environment");
18
- return {
19
- transport: "failed",
20
- rttMs: null
21
- };
22
- }
23
- let pc = null;
24
- try {
25
- pc = new PC({ iceServers });
26
- pc.createDataChannel("decart-preflight");
27
- let sawSrflx = false;
28
- let sawOtherCandidate = false;
29
- let firstSrflxAt = null;
30
- const start = performance.now();
31
- await new Promise((resolve) => {
32
- let settled = false;
33
- const finish = () => {
34
- if (settled) return;
35
- settled = true;
36
- clearTimeout(timer);
37
- resolve();
38
- };
39
- const timer = setTimeout(finish, timeoutMs);
40
- signal?.addEventListener("abort", finish, { once: true });
41
- const peer = pc;
42
- peer.onicecandidate = (event) => {
43
- if (!event.candidate || event.candidate.candidate === "") {
44
- finish();
45
- return;
46
- }
47
- if (candidateType(event.candidate) === "srflx") {
48
- sawSrflx = true;
49
- if (firstSrflxAt === null) firstSrflxAt = performance.now();
50
- } else sawOtherCandidate = true;
51
- };
52
- peer.onicegatheringstatechange = () => {
53
- if (peer.iceGatheringState === "complete") finish();
54
- };
55
- peer.createOffer().then((offer) => peer.setLocalDescription(offer)).catch((error) => {
56
- logger.warn("preflight: failed to create offer", { error: error instanceof Error ? error.message : String(error) });
57
- finish();
58
- });
59
- });
60
- const rttMs = firstSrflxAt !== null ? Math.round(firstSrflxAt - start) : null;
61
- let transport;
62
- if (sawSrflx) transport = "udp";
63
- else if (sawOtherCandidate) transport = "relay";
64
- else transport = "failed";
65
- return {
66
- transport,
67
- rttMs
68
- };
69
- } catch (error) {
70
- logger.warn("preflight: connectivity probe threw", { error: error instanceof Error ? error.message : String(error) });
71
- return {
72
- transport: "failed",
73
- rttMs: null
74
- };
75
- } finally {
76
- try {
77
- pc?.close();
78
- } catch {}
79
- }
80
- }
81
- /** Map probe metrics to a connectivity quality verdict. Pure. */
82
- function classifyConnectivity(metrics, thresholds) {
83
- const reasons = [];
84
- let quality;
85
- if (metrics.transport === "failed") {
86
- quality = "critical";
87
- reasons.push("Could not establish any WebRTC connectivity (no ICE candidates gathered). Real-time streaming is unlikely to work on this network.");
88
- } else if (metrics.transport === "relay") {
89
- quality = "poor";
90
- reasons.push("Direct UDP connectivity could not be confirmed; the session will need a TURN relay, which adds latency and can't be verified without starting a session.");
91
- } else if (metrics.rttMs != null && metrics.rttMs > thresholds.marginalMs) {
92
- quality = "poor";
93
- reasons.push(`Network round-trip time is high (~${metrics.rttMs}ms > ${thresholds.marginalMs}ms); the real-time experience may feel laggy.`);
94
- } else if (metrics.rttMs != null && metrics.rttMs > thresholds.goodMs) {
95
- quality = "fair";
96
- reasons.push(`Network round-trip time is elevated (~${metrics.rttMs}ms > ${thresholds.goodMs}ms).`);
97
- } else quality = "good";
98
- return {
99
- quality,
100
- metrics: {
101
- transport: metrics.transport,
102
- rttMs: metrics.rttMs
103
- },
104
- reasons
105
- };
106
- }
1
+ import { resolveFpsNumber } from "../../shared/model.js";
2
+ import { REALTIME_CONFIG } from "../config-realtime.js";
3
+ import { extractSignals, scoreLowerBetter, worst } from "../observability/connection-quality.js";
4
+ import { classifyConnectivity, gatherIceCandidates } from "../preflight-connectivity.js";
5
+ //#region src/realtime/browser/preflight.ts
107
6
  /**
108
7
  * Classify an active-probe result. Judges startup (TTFF) and steady-state
109
8
  * (mid-stream glass-to-glass) latency separately — both are real experienced
@@ -131,9 +30,9 @@ function classifyActiveProbe(metrics, thresholds) {
131
30
  if (q !== "good") reasons.push(`Mid-stream glass-to-glass latency is ~${metrics.g2gMs}ms (good ≤ ${g.goodMs}ms); the real-time experience may feel laggy.`);
132
31
  }
133
32
  if (metrics.ttffMs == null && metrics.g2gMs == null) if (metrics.rttMs != null) {
134
- reasons.push("Could not measure glass-to-glass latency during the probe (no marker round-trip); using network RTT instead.");
33
+ reasons.push("Could not measure glass-to-glass latency during the probe (no frame metadata); using network RTT instead.");
135
34
  dims.push(scoreLowerBetter(metrics.rttMs, thresholds.rtt.goodMs, thresholds.rtt.fairMs, thresholds.rtt.poorMs));
136
- } else reasons.push("The probe connected but could not measure latency (no marker round-trip and no RTT sample).");
35
+ } else reasons.push("The probe connected but could not measure latency (no frame metadata and no RTT sample).");
137
36
  if (metrics.g2gDropRatio != null) {
138
37
  const d = thresholds.g2gDrop;
139
38
  const q = scoreLowerBetter(metrics.g2gDropRatio, d.good, d.fair, d.poor);
@@ -182,10 +81,9 @@ function activeMetricsFromStats(stats) {
182
81
  };
183
82
  }
184
83
  /**
185
- * Animated synthetic video source — no camera permission needed; content is
186
- * irrelevant to the marker. Sized to the model's exact input dimensions so the
187
- * server doesn't resize/crop the frame, which would move the bottom-left marker
188
- * out of where the server reads it (breaking the round trip).
84
+ * Animated synthetic video source — no camera permission needed. It uses the
85
+ * model's input dimensions to exercise the same encode/inference path as a real
86
+ * session without unnecessary server-side resizing.
189
87
  */
190
88
  function createSyntheticSource(width, height, fps) {
191
89
  if (typeof document === "undefined") throw new Error("deep connectivity probe requires a DOM environment (document is undefined)");
@@ -232,11 +130,18 @@ const ABORTED_DEEP_PROBE = {
232
130
  /** Unblock `promise` early when `signal` aborts. */
233
131
  function raceAbort(promise, signal) {
234
132
  if (!signal) return promise;
235
- if (signal.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
133
+ if (signal.aborted) throw signal.reason ?? createAbortError();
236
134
  return Promise.race([promise, new Promise((_, reject) => {
237
- signal.addEventListener("abort", () => reject(signal.reason ?? new DOMException("Aborted", "AbortError")), { once: true });
135
+ signal.addEventListener("abort", () => reject(signal.reason ?? createAbortError()), { once: true });
238
136
  })]);
239
137
  }
138
+ function createAbortError() {
139
+ const DOMExceptionConstructor = globalThis.DOMException;
140
+ if (typeof DOMExceptionConstructor === "function") return new DOMExceptionConstructor("Aborted", "AbortError");
141
+ const error = /* @__PURE__ */ new Error("Aborted");
142
+ error.name = "AbortError";
143
+ return error;
144
+ }
240
145
  /** Resolve once enough g2g samples exist or the probe window elapses (never rejects). */
241
146
  function waitForProbe(getLatest, minSamples, durationMs, signal) {
242
147
  return new Promise((resolve) => {
@@ -308,4 +213,4 @@ const createPreflight = ({ logger, connect }) => {
308
213
  return { checkConnectivity };
309
214
  };
310
215
  //#endregion
311
- export { createPreflight };
216
+ export { classifyActiveProbe, createPreflight };
@@ -0,0 +1,57 @@
1
+ import { RealtimeObservability } from "../observability/realtime-observability.js";
2
+ import { isDesktopSafari } from "../../utils/platform.js";
3
+ import { createLiveKitMediaChannel } from "../media-channel.js";
4
+ import { createBrowserFrameMetadataDiagnostics, createFrameMetadataWorker } from "./frame-metadata-diagnostics.js";
5
+ import { createMirroredStream, shouldMirrorTrack } from "./mirror-stream.js";
6
+ //#region src/realtime/browser/prepare-connection.ts
7
+ const prepareBrowserConnection = ({ stream, mirror, debugQuality, preferredVideoCodec, fps, logger, observability: observabilityOptions }) => {
8
+ let inputStream = stream ?? new MediaStream();
9
+ let disposeMirroring = () => {};
10
+ if (mirror !== false) try {
11
+ const videoTrack = inputStream.getVideoTracks?.()[0];
12
+ if (videoTrack && (mirror === true || shouldMirrorTrack(videoTrack))) {
13
+ const mirrored = createMirroredStream(inputStream, { fps });
14
+ inputStream = mirrored.stream;
15
+ disposeMirroring = mirrored.dispose;
16
+ } else if (mirror === true && !videoTrack) logger.warn("mirror: true requested but no video track was found on the input stream");
17
+ } catch (error) {
18
+ logger.warn("Failed to mirror input stream; falling back to un-mirrored input", { error: error instanceof Error ? error.message : String(error) });
19
+ }
20
+ let pendingWorker;
21
+ if (debugQuality) try {
22
+ pendingWorker = createFrameMetadataWorker();
23
+ } catch (error) {
24
+ logger.warn("Frame-metadata worker unavailable; glass-to-glass latency measurement disabled", { error: error instanceof Error ? error.message : String(error) });
25
+ }
26
+ const frameTiming = pendingWorker !== void 0;
27
+ const takeFrameMetadataWorker = () => {
28
+ if (pendingWorker) {
29
+ const worker = pendingWorker;
30
+ pendingWorker = void 0;
31
+ return worker;
32
+ }
33
+ return createFrameMetadataWorker();
34
+ };
35
+ const observability = new RealtimeObservability({
36
+ ...observabilityOptions,
37
+ glassToGlass: frameTiming ? createBrowserFrameMetadataDiagnostics() : void 0
38
+ });
39
+ const safariCodec = isDesktopSafari() ? "vp8" : void 0;
40
+ return {
41
+ stream: inputStream,
42
+ observability,
43
+ frameTiming,
44
+ videoCodec: safariCodec ?? preferredVideoCodec,
45
+ queryParams: { ...safariCodec ? { livekit_server_codec: safariCodec } : {} },
46
+ createMediaChannel: (config) => createLiveKitMediaChannel({
47
+ ...config,
48
+ createFrameMetadataWorker: frameTiming ? takeFrameMetadataWorker : void 0
49
+ }),
50
+ dispose: () => {
51
+ disposeMirroring();
52
+ pendingWorker?.terminate();
53
+ }
54
+ };
55
+ };
56
+ //#endregion
57
+ export { prepareBrowserConnection };
@@ -14,7 +14,7 @@ declare const realTimeClientInitialStateSchema: z.ZodObject<{
14
14
  text: z.ZodString;
15
15
  enhance: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
16
16
  }, z.core.$strip>>;
17
- image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString]>>;
17
+ image: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<Blob, unknown, z.core.$ZodTypeInternals<Blob, unknown>>, z.ZodType<File, unknown, z.core.$ZodTypeInternals<File, unknown>>, z.ZodString]>>;
18
18
  passthrough: z.ZodOptional<z.ZodBoolean>;
19
19
  }, z.core.$strip>;
20
20
  type OnRemoteStreamFn = (stream: MediaStream) => void;
@@ -46,7 +46,7 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
46
46
  text: z.ZodString;
47
47
  enhance: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
48
48
  }, z.core.$strip>>;
49
- image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString]>>;
49
+ image: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<Blob, unknown, z.core.$ZodTypeInternals<Blob, unknown>>, z.ZodType<File, unknown, z.core.$ZodTypeInternals<File, unknown>>, z.ZodString]>>;
50
50
  passthrough: z.ZodOptional<z.ZodBoolean>;
51
51
  }, z.core.$strip>>;
52
52
  queryParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -57,12 +57,19 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
57
57
  }>>;
58
58
  preferredVideoCodec: z.ZodOptional<z.ZodEnum<{
59
59
  h264: "h264";
60
+ vp8: "vp8";
60
61
  vp9: "vp9";
61
62
  }>>;
62
63
  debugQuality: z.ZodOptional<z.ZodBoolean>;
63
64
  }, z.core.$strip>;
64
- type RealTimeClientConnectOptions = Omit<z.infer<typeof realTimeClientConnectOptionsSchema>, "model"> & {
65
+ type RealtimeMediaStream = {
66
+ getTracks(): unknown[];
67
+ getAudioTracks(): unknown[];
68
+ getVideoTracks(): unknown[];
69
+ };
70
+ type RealTimeClientConnectOptions<TStream extends RealtimeMediaStream = MediaStream> = Omit<z.infer<typeof realTimeClientConnectOptionsSchema>, "model" | "onRemoteStream"> & {
65
71
  model: ModelDefinition | CustomModelDefinition;
72
+ onRemoteStream: (stream: TStream) => void;
66
73
  };
67
74
  type Events = {
68
75
  connectionChange: ConnectionState;
@@ -101,4 +108,4 @@ type RealTimeClient = {
101
108
  replaceVideoTrack: (track: MediaStreamTrack) => Promise<void>;
102
109
  };
103
110
  //#endregion
104
- export { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState };
111
+ export { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, RealtimeMediaStream };
@@ -1,14 +1,11 @@
1
1
  import { classifyWebrtcError } from "../utils/errors.js";
2
+ import { createConsoleLogger } from "../utils/logger.js";
2
3
  import { isFileRefId } from "../files/types.js";
3
4
  import { modelDefinitionSchema, resolveFpsNumber } from "../shared/model.js";
4
5
  import { modelStateSchema } from "../shared/types.js";
5
- import { createConsoleLogger } from "../utils/logger.js";
6
6
  import { imageToBase64 } from "../utils/media.js";
7
- import { isDesktopSafari } from "../utils/platform.js";
8
7
  import { createEventBuffer } from "./event-buffer.js";
9
8
  import { realtimeMethods } from "./methods.js";
10
- import { createMirroredStream, shouldMirrorTrack } from "./mirror-stream.js";
11
- import { RealtimeObservability } from "./observability/realtime-observability.js";
12
9
  import { StreamSession } from "./stream-session.js";
13
10
  import { z } from "zod";
14
11
  //#region src/realtime/client.ts
@@ -23,7 +20,19 @@ const realTimeClientConnectOptionsSchema = z.object({
23
20
  queryParams: z.record(z.string(), z.string()).optional(),
24
21
  mirror: z.union([z.literal("auto"), z.boolean()]).optional(),
25
22
  resolution: z.enum(["720p", "1080p"]).optional(),
26
- preferredVideoCodec: z.enum(["h264", "vp9"]).optional(),
23
+ /** Local track publish codec. Desktop Safari is always pinned to vp8 and ignores this value. */
24
+ preferredVideoCodec: z.enum([
25
+ "h264",
26
+ "vp8",
27
+ "vp9"
28
+ ]).optional(),
29
+ /**
30
+ * Opt-in quality measurement using LiveKit frame metadata. The capture
31
+ * timestamp is propagated through inference and matched to output playout to
32
+ * surface true glass-to-glass `g2gMs` / `ttffMs` on the `stats` and
33
+ * `connectionQuality` signals. Browser-only and experimental because it
34
+ * relies on LiveKit's frame-metadata worker and encoded transforms.
35
+ */
27
36
  debugQuality: z.boolean().optional()
28
37
  });
29
38
  const createRealTimeClient = (opts) => {
@@ -34,20 +43,10 @@ const createRealTimeClient = (opts) => {
34
43
  if (!parsedOptions.success) throw parsedOptions.error;
35
44
  const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, preferredVideoCodec } = parsedOptions.data;
36
45
  const mirror = parsedOptions.data.mirror ?? false;
37
- let inputStream = stream ?? new MediaStream();
38
- let mirroredStream;
39
- if (mirror !== false) try {
40
- const firstVideoTrack = inputStream.getVideoTracks?.()[0];
41
- if (firstVideoTrack && (mirror === true || shouldMirrorTrack(firstVideoTrack))) {
42
- mirroredStream = createMirroredStream(inputStream, { fps: resolveFpsNumber(options.model.fps) });
43
- inputStream = mirroredStream.stream;
44
- } else if (mirror === true && !firstVideoTrack) logger.warn("mirror: true requested but no video track was found on the input stream");
45
- } catch (error) {
46
- logger.warn("Failed to mirror input stream; falling back to un-mirrored input", { error: error instanceof Error ? error.message : String(error) });
47
- }
48
46
  const debugQuality = parsedOptions.data.debugQuality ?? false;
49
47
  let session;
50
48
  let observability;
49
+ let preparedConnection;
51
50
  try {
52
51
  const initialImageRef = isFileRefId(initialState?.image) ? initialState.image : void 0;
53
52
  const initialImage = initialImageRef === void 0 && initialState?.image ? await imageToBase64(initialState.image) : void 0;
@@ -57,41 +56,47 @@ const createRealTimeClient = (opts) => {
57
56
  } : void 0;
58
57
  const url = `${baseUrl}${options.model.urlPath}`;
59
58
  const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer();
60
- observability = new RealtimeObservability({
61
- telemetryEnabled: opts.telemetryEnabled,
62
- apiKey,
63
- model: options.model.name,
64
- integration,
59
+ preparedConnection = opts.prepareConnection({
60
+ stream,
61
+ mirror,
62
+ debugQuality,
63
+ preferredVideoCodec,
64
+ fps: resolveFpsNumber(options.model.fps),
65
65
  logger,
66
- onDiagnostic: (event) => emitOrBuffer("diagnostic", event),
67
- onStats: (stats) => emitOrBuffer("stats", stats),
68
- onConnectionQuality: (report) => {
69
- emitOrBuffer("connectionQuality", report);
70
- onConnectionQuality?.(report);
71
- },
72
- debugQuality
66
+ observability: {
67
+ telemetryEnabled: opts.telemetryEnabled,
68
+ apiKey,
69
+ model: options.model.name,
70
+ integration,
71
+ logger,
72
+ onDiagnostic: (event) => emitOrBuffer("diagnostic", event),
73
+ onStats: (stats) => emitOrBuffer("stats", stats),
74
+ onConnectionQuality: (report) => {
75
+ emitOrBuffer("connectionQuality", report);
76
+ onConnectionQuality?.(report);
77
+ }
78
+ }
73
79
  });
74
- if (debugQuality) inputStream = observability.attachOutgoingStream(inputStream, resolveFpsNumber(options.model.fps));
75
- const safariCodec = isDesktopSafari() ? "vp8" : void 0;
76
- const publishCodec = safariCodec ?? preferredVideoCodec;
80
+ observability = preparedConnection.observability;
77
81
  session = new StreamSession({
78
82
  url: `${url}?${new URLSearchParams({
79
- ...safariCodec ? { livekit_server_codec: safariCodec } : {},
83
+ ...preparedConnection.queryParams ?? {},
80
84
  ...options.queryParams ?? {},
81
- ...debugQuality ? { pixel_latency: "1" } : {},
82
85
  api_key: apiKey,
83
86
  model: options.model.name,
84
87
  ...resolution ? { resolution } : {}
85
88
  }).toString()}`,
86
89
  integration,
87
90
  observability,
88
- localStream: inputStream,
91
+ frameTiming: preparedConnection.frameTiming,
92
+ localStream: preparedConnection.stream,
89
93
  initialImage,
90
94
  initialImageRef,
91
95
  initialPrompt,
92
96
  initialPassthrough: initialState?.passthrough,
93
97
  logger,
94
- videoCodec: publishCodec
98
+ videoCodec: preparedConnection.videoCodec,
99
+ createMediaChannel: preparedConnection.createMediaChannel
95
100
  });
96
101
  let sessionId = null;
97
102
  let subscribeToken = null;
@@ -126,7 +131,7 @@ const createRealTimeClient = (opts) => {
126
131
  observability?.stop();
127
132
  stop();
128
133
  activeSession.disconnect();
129
- mirroredStream?.dispose();
134
+ preparedConnection?.dispose();
130
135
  },
131
136
  on: eventEmitter.on,
132
137
  off: eventEmitter.off,
@@ -158,7 +163,7 @@ const createRealTimeClient = (opts) => {
158
163
  } catch (error) {
159
164
  observability?.stop();
160
165
  session?.disconnect();
161
- mirroredStream?.dispose();
166
+ preparedConnection?.dispose();
162
167
  throw error;
163
168
  }
164
169
  };
@@ -44,43 +44,78 @@ const REALTIME_CONFIG = {
44
44
  telemetryReportIntervalMs: 1e4,
45
45
  telemetryUrl: "https://platform.decart.ai/api/v1/telemetry",
46
46
  telemetryMaxItemsPerReport: 120,
47
+ /**
48
+ * Thresholds for the derived in-session connection-quality signal
49
+ * (see observability/connection-quality.ts). Tuned for a camera-up
50
+ * real-time pipeline (~3–3.5 Mbps upstream, model fps ~25–30). All
51
+ * values are tunable here so behaviour can change without code edits.
52
+ */
47
53
  connectionQuality: {
54
+ /** Rolling-window size used to smooth raw per-sample metrics. */
48
55
  windowSamples: 5,
56
+ /**
57
+ * Samples to wait before the bitrate dimensions count — the encoder
58
+ * and BWE ramp for several seconds after connect, so early low
59
+ * bitrate is not a slow network. RTT/loss/stall start scoring sooner.
60
+ * `decart-try-on` waits 30 samples watching inbound bitrate alone.
61
+ */
49
62
  warmupSamples: 8,
63
+ /** Consecutive worse samples required before the level downgrades. */
50
64
  downgradeConsecutive: 5,
65
+ /** Consecutive better samples required before the level upgrades (recover slow). */
51
66
  upgradeConsecutive: 5,
67
+ /** Round-trip time bands (ms). Bands widen by relayExtraMs on TURN-relayed paths. */
52
68
  rtt: {
53
69
  goodMs: 150,
54
70
  fairMs: 300,
55
71
  poorMs: 500,
56
72
  relayExtraMs: 100
57
73
  },
74
+ /**
75
+ * Mid-stream (steady-state) true glass-to-glass latency bands (ms) — the
76
+ * real per-frame camera→display latency through the model, used for the
77
+ * latency dimension *instead of* RTT when frame-metadata measurement is on.
78
+ * Already includes both network legs, so relayExtraMs does not apply.
79
+ * Excludes startup (see `ttff`). Anchored to Datadog
80
+ * `rt.stream.pipeline_latency_ms` (server-side median ~285ms / p95 ~510ms)
81
+ * plus network + jitter-buffer + decode headroom. Tune with real data.
82
+ */
58
83
  glassToGlass: {
59
84
  goodMs: 500,
60
85
  fairMs: 900,
61
86
  poorMs: 1500
62
87
  },
88
+ /**
89
+ * Time-to-first-frame bands (ms) — startup latency from connect to the
90
+ * first rendered model frame. An order of magnitude larger than mid-stream
91
+ * and judged separately (a slow first frame is a different problem from a
92
+ * laggy steady state). ~4–5s is an acceptable ("fair") cold start today.
93
+ */
63
94
  ttff: {
64
95
  goodMs: 4e3,
65
96
  fairMs: 6e3,
66
97
  poorMs: 1e4
67
98
  },
99
+ /** Fraction of outbound packets the server reports lost (0..1). */
68
100
  loss: {
69
101
  good: .001,
70
102
  fair: .01,
71
103
  poor: .05
72
104
  },
105
+ /** End-to-end frame drop ratio (0..1), when a frame identity is available. */
73
106
  g2gDrop: {
74
107
  good: .001,
75
108
  fair: .01,
76
109
  poor: .05
77
110
  },
111
+ /** Upstream headroom = available BWE ÷ the intended publish bitrate (requiredUpstreamKbps). */
78
112
  upstream: {
79
113
  goodRatio: 1,
80
114
  fairRatio: .8,
81
115
  poorRatio: .5,
82
116
  requiredUpstreamKbps: 3500
83
117
  },
118
+ /** Rendered (inbound) frames-per-second. */
84
119
  stall: {
85
120
  goodFps: 20,
86
121
  fairFps: 12,
@@ -88,13 +123,29 @@ const REALTIME_CONFIG = {
88
123
  }
89
124
  }
90
125
  },
126
+ /**
127
+ * SDK-only preflight (see preflight.ts). Validates WebRTC reachability
128
+ * (does UDP egress work / will the path need TURN) and latency via a
129
+ * throwaway RTCPeerConnection — no backend session, no media server.
130
+ */
91
131
  preflight: {
132
+ /** Public STUN servers used to gather server-reflexive candidates. */
92
133
  defaultStunUrls: ["stun:stun.l.google.com:19302"],
134
+ /** Abort candidate gathering after this long. */
93
135
  iceGatherTimeoutMs: 5e3,
136
+ /** RTT bands (ms) for the preflight verdict. */
94
137
  rtt: {
95
138
  goodMs: 150,
96
139
  marginalMs: 300
97
140
  },
141
+ /**
142
+ * Deep probe (`checkConnectivity({ deep: true, model })`): briefly opens a
143
+ * real session with a synthetic source + frame-metadata measurement to get a
144
+ * true glass-to-glass verdict, then tears it down. Costs a short GPU session.
145
+ * The verdict reuses the in-session `connectionQuality` bands. Duration must
146
+ * cover TTFF (~4–5s) + mid-stream warm-up (~2s) before steady-state samples
147
+ * accrue; resolves early once `minSamples` exist.
148
+ */
98
149
  active: {
99
150
  durationMs: 12e3,
100
151
  minSamples: 5
@@ -0,0 +1,26 @@
1
+ import { createLiveKitInitializationError } from "../utils/errors.js";
2
+ //#region src/realtime/livekit.ts
3
+ let liveKitModulePromise;
4
+ function validateLiveKitModule(module) {
5
+ const invalid = [
6
+ typeof module.Room !== "function" && "Room",
7
+ (typeof module.RoomEvent !== "object" || module.RoomEvent === null) && "RoomEvent",
8
+ (module.Track === null || typeof module.Track !== "object" && typeof module.Track !== "function") && "Track",
9
+ module.Track?.Source?.Camera !== "camera" && "Track.Source.Camera",
10
+ (typeof module.ConnectionState !== "object" || module.ConnectionState === null) && "ConnectionState"
11
+ ].filter((name) => name !== false);
12
+ if (invalid.length > 0) throw createLiveKitInitializationError(`livekit-client is missing or has invalid required exports: ${invalid.join(", ")}`);
13
+ return module;
14
+ }
15
+ /** Load the browser-neutral LiveKit client only when realtime is actually used. */
16
+ function loadLiveKitClient() {
17
+ liveKitModulePromise ??= import("livekit-client").then(validateLiveKitModule).catch((error) => {
18
+ liveKitModulePromise = void 0;
19
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "LIVEKIT_INITIALIZATION_ERROR") throw error;
20
+ const cause = error instanceof Error ? error : new Error(String(error));
21
+ throw createLiveKitInitializationError(`Failed to initialize livekit-client: ${cause.message}`, cause);
22
+ });
23
+ return liveKitModulePromise;
24
+ }
25
+ //#endregion
26
+ export { loadLiveKitClient, validateLiveKitModule };
@@ -1,24 +1,28 @@
1
1
  import { createConsoleLogger } from "../utils/logger.js";
2
2
  import { REALTIME_CONFIG } from "./config-realtime.js";
3
+ import { loadLiveKitClient } from "./livekit.js";
3
4
  import mitt from "mitt";
4
- import { Room, RoomEvent, Track } from "livekit-client";
5
5
  //#region src/realtime/media-channel.ts
6
- function getDefaultVideoPublishOptions(videoCodec) {
6
+ function getDefaultVideoPublishOptions(source, videoCodec, frameMetadata = false) {
7
7
  const resolvedCodec = videoCodec ?? REALTIME_CONFIG.livekit.defaultVideoCodec;
8
8
  const maxBitrate = resolvedCodec === "vp9" ? REALTIME_CONFIG.livekit.vp9MaxVideoBitrateBps : REALTIME_CONFIG.livekit.defaultMaxVideoBitrateBps;
9
9
  return {
10
- source: Track.Source.Camera,
10
+ source,
11
11
  videoCodec: resolvedCodec,
12
12
  simulcast: resolvedCodec !== "vp9",
13
13
  videoEncoding: {
14
14
  maxBitrate,
15
15
  maxFramerate: REALTIME_CONFIG.livekit.defaultPublishFps
16
- }
16
+ },
17
+ ...frameMetadata ? { frameMetadata: { timestamp: true } } : {}
17
18
  };
18
19
  }
19
- var MediaChannel = class {
20
+ var LiveKitMediaChannel = class {
21
+ config;
20
22
  room = null;
23
+ cameraTrackSource = null;
21
24
  remoteStream = null;
25
+ frameMetadataEnabled = false;
22
26
  events = mitt();
23
27
  logger;
24
28
  constructor(config) {
@@ -35,14 +39,34 @@ var MediaChannel = class {
35
39
  this.events.off(event, handler);
36
40
  }
37
41
  async connect(opts) {
38
- this.room ??= new Room(REALTIME_CONFIG.livekit.roomOptions);
42
+ const { Room: LiveKitRoom, RoomEvent, Track } = await loadLiveKitClient();
43
+ this.cameraTrackSource = Track.Source.Camera;
44
+ if (!this.room) {
45
+ let worker;
46
+ if (this.config.createFrameMetadataWorker) try {
47
+ worker = this.config.createFrameMetadataWorker();
48
+ } catch (error) {
49
+ this.logger.warn("Failed to create LiveKit frame-metadata worker; continuing without latency metrics", { error: error instanceof Error ? error.message : String(error) });
50
+ }
51
+ this.frameMetadataEnabled = worker !== void 0;
52
+ try {
53
+ this.room = new LiveKitRoom({
54
+ ...REALTIME_CONFIG.livekit.roomOptions,
55
+ ...worker ? { frameMetadata: { worker } } : {}
56
+ });
57
+ } catch (error) {
58
+ worker?.terminate();
59
+ this.frameMetadataEnabled = false;
60
+ throw error;
61
+ }
62
+ }
39
63
  const room = this.room;
40
64
  room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
41
65
  if (!participant.identity.startsWith(REALTIME_CONFIG.livekit.inferenceServerIdentityPrefix)) return;
42
- if (track.kind !== Track.Kind.Video && track.kind !== Track.Kind.Audio) return;
66
+ if (track.kind !== "video" && track.kind !== "audio") return;
43
67
  const mediaStreamTrack = track.mediaStreamTrack;
44
68
  if (mediaStreamTrack) {
45
- if (track.kind === Track.Kind.Video) this.config.observability?.attachRemoteVideoTrack(mediaStreamTrack);
69
+ if (track.kind === "video") this.config.observability?.attachRemoteVideoTrack(track);
46
70
  const tracks = this.remoteStream?.getTracks() ?? [];
47
71
  if (!tracks.includes(mediaStreamTrack)) tracks.push(mediaStreamTrack);
48
72
  this.remoteStream = new MediaStream(tracks);
@@ -74,15 +98,20 @@ var MediaChannel = class {
74
98
  disconnect() {
75
99
  const room = this.room;
76
100
  this.room = null;
101
+ this.cameraTrackSource = null;
77
102
  this.remoteStream = null;
103
+ this.frameMetadataEnabled = false;
78
104
  this.config.observability?.setLiveKitRoom(null);
79
105
  if (room) room.disconnect().catch(() => {});
80
106
  }
81
107
  async publishTracks(stream) {
82
108
  if (!this.room) return;
83
- for (const track of stream.getTracks()) if (track.kind === "video") await this.room.localParticipant.publishTrack(track, getDefaultVideoPublishOptions(this.config.videoCodec));
84
- else await this.room.localParticipant.publishTrack(track);
109
+ for (const track of stream.getTracks()) if (track.kind === "video") {
110
+ if (!this.cameraTrackSource) throw new Error("Cannot publish video track: media channel is not connected");
111
+ await this.room.localParticipant.publishTrack(track, getDefaultVideoPublishOptions(this.cameraTrackSource, this.config.videoCodec, this.frameMetadataEnabled));
112
+ } else await this.room.localParticipant.publishTrack(track);
85
113
  }
86
114
  };
115
+ const createLiveKitMediaChannel = (config) => new LiveKitMediaChannel(config);
87
116
  //#endregion
88
- export { MediaChannel };
117
+ export { LiveKitMediaChannel, createLiveKitMediaChannel, getDefaultVideoPublishOptions };
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  declare const setInputSchema: z.ZodObject<{
5
5
  prompt: z.ZodOptional<z.ZodString>;
6
6
  enhance: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
7
- image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString, z.ZodNull]>>;
7
+ image: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<Blob, unknown, z.core.$ZodTypeInternals<Blob, unknown>>, z.ZodType<File, unknown, z.core.$ZodTypeInternals<File, unknown>>, z.ZodString, z.ZodNull]>>;
8
8
  }, z.core.$strip>;
9
9
  type SetInput = z.input<typeof setInputSchema>;
10
10
  //#endregion