@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
@@ -0,0 +1,25 @@
1
+ //#region src/realtime/observability/livekit-stats-provider.ts
2
+ function createLiveKitStatsProvider(room) {
3
+ let uid = 0;
4
+ const collectFromTrack = async (track, entries) => {
5
+ if (!track) return;
6
+ let report;
7
+ try {
8
+ report = await track.getRTCStatsReport();
9
+ } catch {
10
+ return;
11
+ }
12
+ if (!report) return;
13
+ report.forEach((stat, id) => {
14
+ entries.push([`${id}#${uid++}`, stat]);
15
+ });
16
+ };
17
+ return { async getStats() {
18
+ const entries = [];
19
+ for (const pub of room.localParticipant.trackPublications.values()) await collectFromTrack(pub.track, entries);
20
+ for (const participant of room.remoteParticipants.values()) for (const pub of participant.trackPublications.values()) await collectFromTrack(pub.track, entries);
21
+ return new Map(entries);
22
+ } };
23
+ }
24
+ //#endregion
25
+ export { createLiveKitStatsProvider };
@@ -0,0 +1,173 @@
1
+ import { REALTIME_CONFIG } from "../config-realtime.js";
2
+ import { createLiveKitStatsProvider } from "./livekit-stats-provider.js";
3
+ import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
4
+ import { WebRTCStatsCollector } from "./webrtc-stats.js";
5
+ //#region src/realtime/observability/realtime-observability.ts
6
+ var RealtimeObservability = class {
7
+ telemetryReporter = new NullTelemetryReporter();
8
+ telemetryReporterReady = false;
9
+ pendingTelemetryDiagnostics = [];
10
+ statsCollector = null;
11
+ statsCollectorSource = null;
12
+ liveKitRoom = null;
13
+ videoStalled = false;
14
+ stallStartMs = 0;
15
+ connectionBreakdown = null;
16
+ constructor(options) {
17
+ this.options = options;
18
+ }
19
+ diagnostic(name, data, timestamp = Date.now()) {
20
+ this.options.logger.debug(name, data);
21
+ this.options.onDiagnostic?.({
22
+ name,
23
+ data
24
+ });
25
+ this.addTelemetryDiagnostic(name, data, timestamp);
26
+ }
27
+ beginConnectionBreakdown(attempt, initialImageSizeKb) {
28
+ this.connectionBreakdown = {
29
+ attempt,
30
+ connectStartedAt: Date.now(),
31
+ initialImageSizeKb,
32
+ phases: /* @__PURE__ */ new Map()
33
+ };
34
+ }
35
+ startPhase(name) {
36
+ if (!this.connectionBreakdown) return;
37
+ this.connectionBreakdown.phases.set(name, { startedAt: Date.now() });
38
+ }
39
+ endPhase(name, opts) {
40
+ if (!this.connectionBreakdown) return;
41
+ const entry = this.connectionBreakdown.phases.get(name);
42
+ if (!entry) {
43
+ this.options.logger.warn("observability: endPhase called for unknown phase", { phase: name });
44
+ return;
45
+ }
46
+ entry.endedAt = Date.now();
47
+ entry.success = opts.success;
48
+ if (opts.error !== void 0) entry.error = opts.error;
49
+ }
50
+ finishConnectionBreakdown(opts) {
51
+ const buffer = this.connectionBreakdown;
52
+ if (!buffer) return;
53
+ this.connectionBreakdown = null;
54
+ const now = Date.now();
55
+ const phases = [];
56
+ for (const [phase, entry] of buffer.phases) {
57
+ const unfinished = entry.endedAt === void 0;
58
+ const endedAt = entry.endedAt ?? now;
59
+ const success = entry.success ?? false;
60
+ const error = entry.error ?? (unfinished && !opts.success ? opts.error : void 0);
61
+ phases.push({
62
+ phase,
63
+ durationMs: endedAt - entry.startedAt,
64
+ success,
65
+ ...error !== void 0 ? { error } : {}
66
+ });
67
+ }
68
+ this.diagnostic("client-session-connection-breakdown", {
69
+ attempt: buffer.attempt,
70
+ success: opts.success,
71
+ totalDurationMs: now - buffer.connectStartedAt,
72
+ initialImageSizeKb: buffer.initialImageSizeKb,
73
+ phases,
74
+ ...opts.error !== void 0 ? { error: opts.error } : {}
75
+ }, now);
76
+ }
77
+ sessionStarted(sessionId) {
78
+ if (!this.options.telemetryEnabled) return;
79
+ if (this.telemetryReporterReady) this.telemetryReporter.stop();
80
+ const reporter = new TelemetryReporter({
81
+ apiKey: this.options.apiKey,
82
+ sessionId,
83
+ model: this.options.model,
84
+ integration: this.options.integration,
85
+ logger: this.options.logger
86
+ });
87
+ reporter.start();
88
+ this.telemetryReporter = reporter;
89
+ this.telemetryReporterReady = true;
90
+ for (const diagnostic of this.pendingTelemetryDiagnostics) this.telemetryReporter.addDiagnostic(diagnostic);
91
+ this.pendingTelemetryDiagnostics.length = 0;
92
+ }
93
+ setStatsProvider(source) {
94
+ if (!source) {
95
+ this.stopStats();
96
+ return;
97
+ }
98
+ if (source === this.statsCollectorSource) return;
99
+ this.stopStats();
100
+ this.resetStallDetection();
101
+ this.statsCollectorSource = source;
102
+ if (!this.options.telemetryEnabled && !this.options.onStats) return;
103
+ this.statsCollector = new WebRTCStatsCollector();
104
+ this.statsCollector.start(source, (stats) => this.handleStats(stats));
105
+ }
106
+ setLiveKitRoom(room) {
107
+ if (!room) {
108
+ this.liveKitRoom = null;
109
+ this.setStatsProvider(null);
110
+ return;
111
+ }
112
+ if (room === this.liveKitRoom) return;
113
+ this.setStatsProvider(createLiveKitStatsProvider(room));
114
+ this.liveKitRoom = room;
115
+ }
116
+ stopStats() {
117
+ this.statsCollector?.stop();
118
+ this.statsCollector = null;
119
+ this.statsCollectorSource = null;
120
+ this.liveKitRoom = null;
121
+ this.resetStallDetection();
122
+ }
123
+ stop() {
124
+ this.stopStats();
125
+ this.telemetryReporter.stop();
126
+ this.telemetryReporter = new NullTelemetryReporter();
127
+ this.telemetryReporterReady = false;
128
+ this.pendingTelemetryDiagnostics.length = 0;
129
+ this.connectionBreakdown = null;
130
+ }
131
+ handleStats(stats) {
132
+ this.options.onStats?.(stats);
133
+ this.telemetryReporter.addStats(stats);
134
+ this.detectVideoStall(stats);
135
+ }
136
+ detectVideoStall(stats) {
137
+ const fps = stats.video?.framesPerSecond ?? 0;
138
+ if (!this.videoStalled && stats.video && fps < REALTIME_CONFIG.observability.stallFpsThreshold) {
139
+ this.videoStalled = true;
140
+ this.stallStartMs = Date.now();
141
+ this.diagnostic("videoStall", {
142
+ stalled: true,
143
+ durationMs: 0
144
+ }, this.stallStartMs);
145
+ } else if (this.videoStalled && fps >= REALTIME_CONFIG.observability.stallFpsThreshold) {
146
+ const durationMs = Date.now() - this.stallStartMs;
147
+ this.videoStalled = false;
148
+ this.diagnostic("videoStall", {
149
+ stalled: false,
150
+ durationMs
151
+ });
152
+ }
153
+ }
154
+ addTelemetryDiagnostic(name, data, timestamp) {
155
+ if (!this.options.telemetryEnabled) return;
156
+ const diagnostic = {
157
+ name,
158
+ data,
159
+ timestamp
160
+ };
161
+ if (!this.telemetryReporterReady) {
162
+ this.pendingTelemetryDiagnostics.push(diagnostic);
163
+ return;
164
+ }
165
+ this.telemetryReporter.addDiagnostic(diagnostic);
166
+ }
167
+ resetStallDetection() {
168
+ this.videoStalled = false;
169
+ this.stallStartMs = 0;
170
+ }
171
+ };
172
+ //#endregion
173
+ export { RealtimeObservability };
@@ -1,14 +1,7 @@
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;
1
+ import { VERSION } from "../../version.js";
2
+ import { buildAuthHeaders } from "../../shared/request.js";
3
+ import { REALTIME_CONFIG } from "../config-realtime.js";
4
+ //#region src/realtime/observability/telemetry-reporter.ts
12
5
  /** No-op reporter that silently discards all data. Used when telemetry is disabled. */
13
6
  var NullTelemetryReporter = class {
14
7
  start() {}
@@ -22,7 +15,6 @@ var TelemetryReporter = class {
22
15
  sessionId;
23
16
  model;
24
17
  integration;
25
- logger;
26
18
  reportIntervalMs;
27
19
  intervalId = null;
28
20
  statsBuffer = [];
@@ -32,8 +24,7 @@ var TelemetryReporter = class {
32
24
  this.sessionId = options.sessionId;
33
25
  this.model = options.model;
34
26
  this.integration = options.integration;
35
- this.logger = options.logger;
36
- this.reportIntervalMs = options.reportIntervalMs ?? DEFAULT_REPORT_INTERVAL_MS;
27
+ this.reportIntervalMs = options.reportIntervalMs ?? REALTIME_CONFIG.observability.telemetryReportIntervalMs;
37
28
  }
38
29
  /** Start the periodic reporting timer. */
39
30
  start() {
@@ -62,7 +53,7 @@ var TelemetryReporter = class {
62
53
  this.diagnosticsBuffer = [];
63
54
  }
64
55
  /**
65
- * Build a single chunk from the front of the buffers, respecting MAX_ITEMS_PER_REPORT.
56
+ * Build a single chunk from the front of the buffers, respecting the configured report item cap.
66
57
  * Returns null when both buffers are empty.
67
58
  */
68
59
  createReportChunk() {
@@ -79,8 +70,8 @@ var TelemetryReporter = class {
79
70
  sdkVersion: VERSION,
80
71
  ...this.model ? { model: this.model } : {},
81
72
  tags,
82
- stats: this.statsBuffer.splice(0, MAX_ITEMS_PER_REPORT),
83
- diagnostics: this.diagnosticsBuffer.splice(0, MAX_ITEMS_PER_REPORT)
73
+ stats: this.statsBuffer.splice(0, REALTIME_CONFIG.observability.telemetryMaxItemsPerReport),
74
+ diagnostics: this.diagnosticsBuffer.splice(0, REALTIME_CONFIG.observability.telemetryMaxItemsPerReport)
84
75
  };
85
76
  }
86
77
  sendReport() {
@@ -95,25 +86,15 @@ var TelemetryReporter = class {
95
86
  };
96
87
  let chunk = this.createReportChunk();
97
88
  while (chunk !== null) {
98
- fetch(TELEMETRY_URL, {
89
+ fetch(REALTIME_CONFIG.observability.telemetryUrl, {
99
90
  method: "POST",
100
91
  headers: commonHeaders,
101
92
  body: JSON.stringify(chunk)
102
- }).then((response) => {
103
- if (!response.ok) this.logger.warn("Telemetry report rejected", {
104
- status: response.status,
105
- statusText: response.statusText
106
- });
107
- }).catch((error) => {
108
- this.logger.debug("Telemetry report failed", { error: String(error) });
109
- });
93
+ }).catch(() => {});
110
94
  chunk = this.createReportChunk();
111
95
  }
112
- } catch (error) {
113
- this.logger.debug("Telemetry report failed", { error: String(error) });
114
- }
96
+ } catch {}
115
97
  }
116
98
  };
117
-
118
99
  //#endregion
119
- export { NullTelemetryReporter, TelemetryReporter };
100
+ export { NullTelemetryReporter, TelemetryReporter };
@@ -0,0 +1,148 @@
1
+ //#region src/realtime/observability/webrtc-stats.d.ts
2
+ type WebRTCStats = {
3
+ timestamp: number;
4
+ video: {
5
+ framesDecoded: number;
6
+ framesDropped: number;
7
+ framesReceived: number;
8
+ keyFramesDecoded: number;
9
+ framesPerSecond: number;
10
+ frameWidth: number;
11
+ frameHeight: number;
12
+ bytesReceived: number;
13
+ packetsReceived: number;
14
+ packetsLost: number;
15
+ jitter: number;
16
+ /** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
17
+ bitrate: number;
18
+ freezeCount: number;
19
+ totalFreezesDuration: number;
20
+ /** Delta: packets lost since previous sample. */
21
+ packetsLostDelta: number;
22
+ /** Delta: frames dropped since previous sample. */
23
+ framesDroppedDelta: number;
24
+ /** Delta: freeze count since previous sample. */
25
+ freezeCountDelta: number;
26
+ /** Delta: freeze duration (seconds) since previous sample. */
27
+ freezeDurationDelta: number;
28
+ /** NACKs sent to the sender (requesting packet retransmission). */
29
+ nackCount: number;
30
+ nackCountDelta: number;
31
+ /** PLIs sent to the sender (full frame retransmission request). */
32
+ pliCount: number;
33
+ /** FIRs sent to the sender (forced intra-refresh request). */
34
+ firCount: number;
35
+ /**
36
+ * Average decode time (ms/frame), cumulative since stream start.
37
+ * Derived from totalDecodeTime/framesDecoded. `null` if the browser
38
+ * hasn't produced the underlying counters yet.
39
+ */
40
+ avgDecodeTimeMs: number | null;
41
+ /** Average jitter-buffer time (ms/frame emitted). Cumulative. */
42
+ avgJitterBufferMs: number | null;
43
+ /**
44
+ * Average total processing delay (ms/frame decoded) — from network
45
+ * receive to decoder output. Cumulative.
46
+ */
47
+ avgProcessingDelayMs: number | null;
48
+ /** Average inter-frame delay at the decoder (ms). */
49
+ avgInterFrameDelayMs: number | null;
50
+ /**
51
+ * Std-dev of inter-frame delay (ms), computed from
52
+ * totalInterFrameDelay + totalSquaredInterFrameDelay.
53
+ */
54
+ interFrameDelayStdDevMs: number | null;
55
+ /** Current target delay of the jitter buffer (ms). */
56
+ jitterBufferTargetDelayMs: number | null;
57
+ /** Current minimum delay of the jitter buffer (ms). */
58
+ jitterBufferMinimumDelayMs: number | null;
59
+ /** Which decoder the browser picked (e.g. "libvpx", "ExternalDecoder"). */
60
+ decoderImplementation: string;
61
+ } | null;
62
+ audio: {
63
+ bytesReceived: number;
64
+ packetsReceived: number;
65
+ packetsLost: number;
66
+ jitter: number;
67
+ /** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
68
+ bitrate: number;
69
+ /** Delta: packets lost since previous sample. */
70
+ packetsLostDelta: number;
71
+ } | null;
72
+ /** Outbound video track stats (from the local camera/screen share being sent). */
73
+ outboundVideo: {
74
+ /** Why the encoder is limiting quality: "none", "bandwidth", "cpu", or "other". */
75
+ qualityLimitationReason: string;
76
+ /** Cumulative time (seconds) spent in each quality limitation state. */
77
+ qualityLimitationDurations: Record<string, number>;
78
+ bytesSent: number;
79
+ packetsSent: number;
80
+ framesPerSecond: number;
81
+ frameWidth: number;
82
+ frameHeight: number;
83
+ /** Estimated outbound bitrate in bits/sec, computed from bytesSent delta. */
84
+ bitrate: number;
85
+ /** Encoder's current target bitrate in kbps (BWE output). */
86
+ targetBitrateKbps: number | null;
87
+ /** Average encode time per frame (ms), cumulative. */
88
+ avgEncodeTimeMs: number | null;
89
+ /** Average packet send delay (ms), cumulative. */
90
+ avgPacketSendDelayMs: number | null;
91
+ /** Average quantization parameter across encoded frames (lower is better). */
92
+ avgQp: number | null;
93
+ /** NACKs received from receiver (retransmission requests). */
94
+ nackCount: number;
95
+ /** PLIs received from receiver. */
96
+ pliCount: number;
97
+ /** FIRs received from receiver. */
98
+ firCount: number;
99
+ retransmittedBytesSent: number;
100
+ retransmittedPacketsSent: number;
101
+ /** Which encoder the browser picked (e.g. "libvpx", "SimulcastEncoderAdapter"). */
102
+ encoderImplementation: string;
103
+ } | null;
104
+ /**
105
+ * Remote-inbound stats — what the far end reports *about its reception
106
+ * of our outbound stream*. Answers "does the server think we're lossy?"
107
+ * independently of what we see locally. Populated from
108
+ * `remote-inbound-rtp` reports.
109
+ */
110
+ remoteInbound: {
111
+ fractionLost: number | null;
112
+ /** In seconds. */
113
+ jitter: number | null;
114
+ /** In seconds. Often more accurate than connection.currentRoundTripTime. */
115
+ roundTripTime: number | null;
116
+ } | null;
117
+ connection: {
118
+ /** Current round-trip time in seconds, or null if unavailable. */
119
+ currentRoundTripTime: number | null;
120
+ /** Available outgoing bitrate estimate in bits/sec, or null if unavailable. */
121
+ availableOutgoingBitrate: number | null;
122
+ /**
123
+ * Selected ICE candidate pairs (usually one per PC). Populated from
124
+ * the `candidate-pair` report with state="succeeded" plus the matching
125
+ * `local-candidate` / `remote-candidate` lookups. Lets diagnostic tools
126
+ * tell direct-UDP sessions from TURN-relayed ones — the path affects
127
+ * jitter and failure modes, so this is essential signal for
128
+ * benchmarking and incident triage.
129
+ */
130
+ selectedCandidatePairs: IceCandidatePair[];
131
+ };
132
+ };
133
+ /** One side of an ICE candidate pair (sender or receiver). */
134
+ type IceCandidateInfo = {
135
+ /** "host" | "srflx" | "prflx" | "relay" */
136
+ candidateType: string;
137
+ /** IP (v4 or v6). May be `""` for mDNS-obfuscated host candidates. */
138
+ address: string;
139
+ port: number;
140
+ /** "udp" | "tcp" */
141
+ protocol: string;
142
+ };
143
+ type IceCandidatePair = {
144
+ local: IceCandidateInfo;
145
+ remote: IceCandidateInfo;
146
+ };
147
+ //#endregion
148
+ export { WebRTCStats };