@decartai/sdk 0.0.66 → 0.0.68
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 +27 -0
- package/dist/index.d.ts +2 -2
- package/dist/realtime/client.d.ts +7 -2
- package/dist/realtime/client.js +48 -119
- package/dist/realtime/mirror-stream.js +116 -0
- package/dist/realtime/{diagnostics.d.ts → observability/diagnostics.d.ts} +1 -1
- package/dist/realtime/observability/realtime-observability.js +109 -0
- package/dist/realtime/{telemetry-reporter.js → observability/telemetry-reporter.js} +3 -3
- package/dist/realtime/observability/webrtc-stats.d.ts +147 -0
- package/dist/realtime/observability/webrtc-stats.js +278 -0
- package/dist/realtime/subscribe-client.d.ts +3 -1
- package/dist/realtime/webrtc-connection.js +18 -19
- package/dist/realtime/webrtc-manager.js +24 -3
- package/package.json +1 -1
- package/dist/realtime/webrtc-stats.d.ts +0 -59
- package/dist/realtime/webrtc-stats.js +0 -154
|
@@ -21,6 +21,7 @@ var WebRTCManager = class {
|
|
|
21
21
|
connection;
|
|
22
22
|
config;
|
|
23
23
|
logger;
|
|
24
|
+
observability;
|
|
24
25
|
localStream = null;
|
|
25
26
|
subscribeMode = false;
|
|
26
27
|
managerState = "disconnected";
|
|
@@ -28,6 +29,7 @@ var WebRTCManager = class {
|
|
|
28
29
|
isReconnecting = false;
|
|
29
30
|
intentionalDisconnect = false;
|
|
30
31
|
reconnectGeneration = 0;
|
|
32
|
+
statsProviderConnection = null;
|
|
31
33
|
constructor(config) {
|
|
32
34
|
this.config = config;
|
|
33
35
|
this.logger = config.logger ?? {
|
|
@@ -36,6 +38,7 @@ var WebRTCManager = class {
|
|
|
36
38
|
warn() {},
|
|
37
39
|
error() {}
|
|
38
40
|
};
|
|
41
|
+
this.observability = config.observability;
|
|
39
42
|
this.connection = new WebRTCConnection({
|
|
40
43
|
onRemoteStream: config.onRemoteStream,
|
|
41
44
|
onStateChange: (state) => this.handleConnectionStateChange(state),
|
|
@@ -46,7 +49,7 @@ var WebRTCManager = class {
|
|
|
46
49
|
initialImage: config.initialImage,
|
|
47
50
|
initialPrompt: config.initialPrompt,
|
|
48
51
|
logger: this.logger,
|
|
49
|
-
|
|
52
|
+
observability: this.observability
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
emitState(state) {
|
|
@@ -56,15 +59,28 @@ var WebRTCManager = class {
|
|
|
56
59
|
this.config.onConnectionStateChange?.(state);
|
|
57
60
|
}
|
|
58
61
|
}
|
|
62
|
+
syncStatsProvider() {
|
|
63
|
+
const pc = this.getPeerConnection();
|
|
64
|
+
const isLive = this.managerState === "connected" || this.managerState === "generating";
|
|
65
|
+
if (isLive && pc && pc !== this.statsProviderConnection) {
|
|
66
|
+
this.statsProviderConnection = pc;
|
|
67
|
+
this.observability.setStatsProvider(pc);
|
|
68
|
+
} else if (!isLive && this.statsProviderConnection) {
|
|
69
|
+
this.statsProviderConnection = null;
|
|
70
|
+
this.observability.setStatsProvider(null);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
handleConnectionStateChange(state) {
|
|
60
74
|
if (this.intentionalDisconnect) {
|
|
61
75
|
this.emitState("disconnected");
|
|
76
|
+
this.syncStatsProvider();
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
79
|
if (this.isReconnecting) {
|
|
65
80
|
if (state === "connected" || state === "generating") {
|
|
66
81
|
this.isReconnecting = false;
|
|
67
82
|
this.emitState(state);
|
|
83
|
+
this.syncStatsProvider();
|
|
68
84
|
}
|
|
69
85
|
return;
|
|
70
86
|
}
|
|
@@ -73,6 +89,7 @@ var WebRTCManager = class {
|
|
|
73
89
|
return;
|
|
74
90
|
}
|
|
75
91
|
this.emitState(state);
|
|
92
|
+
this.syncStatsProvider();
|
|
76
93
|
}
|
|
77
94
|
async reconnect() {
|
|
78
95
|
if (this.isReconnecting || this.intentionalDisconnect) return;
|
|
@@ -80,6 +97,8 @@ var WebRTCManager = class {
|
|
|
80
97
|
const reconnectGeneration = ++this.reconnectGeneration;
|
|
81
98
|
this.isReconnecting = true;
|
|
82
99
|
this.emitState("reconnecting");
|
|
100
|
+
this.observability.setStatsProvider(null);
|
|
101
|
+
this.statsProviderConnection = null;
|
|
83
102
|
const reconnectStart = performance.now();
|
|
84
103
|
try {
|
|
85
104
|
let attemptCount = 0;
|
|
@@ -101,7 +120,7 @@ var WebRTCManager = class {
|
|
|
101
120
|
error: error.message,
|
|
102
121
|
attempt: error.attemptNumber
|
|
103
122
|
});
|
|
104
|
-
this.
|
|
123
|
+
this.observability.diagnostic("reconnect", {
|
|
105
124
|
attempt: error.attemptNumber,
|
|
106
125
|
maxAttempts: RETRY_OPTIONS.retries + 1,
|
|
107
126
|
durationMs: performance.now() - reconnectStart,
|
|
@@ -116,7 +135,7 @@ var WebRTCManager = class {
|
|
|
116
135
|
return !PERMANENT_ERRORS.some((err) => msg.includes(err));
|
|
117
136
|
}
|
|
118
137
|
});
|
|
119
|
-
this.
|
|
138
|
+
this.observability.diagnostic("reconnect", {
|
|
120
139
|
attempt: attemptCount,
|
|
121
140
|
maxAttempts: RETRY_OPTIONS.retries + 1,
|
|
122
141
|
durationMs: performance.now() - reconnectStart,
|
|
@@ -166,6 +185,8 @@ var WebRTCManager = class {
|
|
|
166
185
|
this.reconnectGeneration += 1;
|
|
167
186
|
this.connection.cleanup();
|
|
168
187
|
this.localStream = null;
|
|
188
|
+
this.statsProviderConnection = null;
|
|
189
|
+
this.observability.setStatsProvider(null);
|
|
169
190
|
this.emitState("disconnected");
|
|
170
191
|
}
|
|
171
192
|
isConnected() {
|
package/package.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
//#region src/realtime/webrtc-stats.d.ts
|
|
2
|
-
type WebRTCStats = {
|
|
3
|
-
timestamp: number;
|
|
4
|
-
video: {
|
|
5
|
-
framesDecoded: number;
|
|
6
|
-
framesDropped: number;
|
|
7
|
-
framesPerSecond: number;
|
|
8
|
-
frameWidth: number;
|
|
9
|
-
frameHeight: number;
|
|
10
|
-
bytesReceived: number;
|
|
11
|
-
packetsReceived: number;
|
|
12
|
-
packetsLost: number;
|
|
13
|
-
jitter: number;
|
|
14
|
-
/** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
|
|
15
|
-
bitrate: number;
|
|
16
|
-
freezeCount: number;
|
|
17
|
-
totalFreezesDuration: number;
|
|
18
|
-
/** Delta: packets lost since previous sample. */
|
|
19
|
-
packetsLostDelta: number;
|
|
20
|
-
/** Delta: frames dropped since previous sample. */
|
|
21
|
-
framesDroppedDelta: number;
|
|
22
|
-
/** Delta: freeze count since previous sample. */
|
|
23
|
-
freezeCountDelta: number;
|
|
24
|
-
/** Delta: freeze duration (seconds) since previous sample. */
|
|
25
|
-
freezeDurationDelta: number;
|
|
26
|
-
} | null;
|
|
27
|
-
audio: {
|
|
28
|
-
bytesReceived: number;
|
|
29
|
-
packetsReceived: number;
|
|
30
|
-
packetsLost: number;
|
|
31
|
-
jitter: number;
|
|
32
|
-
/** Estimated inbound bitrate in bits/sec, computed from bytesReceived delta. */
|
|
33
|
-
bitrate: number;
|
|
34
|
-
/** Delta: packets lost since previous sample. */
|
|
35
|
-
packetsLostDelta: number;
|
|
36
|
-
} | null;
|
|
37
|
-
/** Outbound video track stats (from the local camera/screen share being sent). */
|
|
38
|
-
outboundVideo: {
|
|
39
|
-
/** Why the encoder is limiting quality: "none", "bandwidth", "cpu", or "other". */
|
|
40
|
-
qualityLimitationReason: string;
|
|
41
|
-
/** Cumulative time (seconds) spent in each quality limitation state. */
|
|
42
|
-
qualityLimitationDurations: Record<string, number>;
|
|
43
|
-
bytesSent: number;
|
|
44
|
-
packetsSent: number;
|
|
45
|
-
framesPerSecond: number;
|
|
46
|
-
frameWidth: number;
|
|
47
|
-
frameHeight: number;
|
|
48
|
-
/** Estimated outbound bitrate in bits/sec, computed from bytesSent delta. */
|
|
49
|
-
bitrate: number;
|
|
50
|
-
} | null;
|
|
51
|
-
connection: {
|
|
52
|
-
/** Current round-trip time in seconds, or null if unavailable. */
|
|
53
|
-
currentRoundTripTime: number | null;
|
|
54
|
-
/** Available outgoing bitrate estimate in bits/sec, or null if unavailable. */
|
|
55
|
-
availableOutgoingBitrate: number | null;
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
//#endregion
|
|
59
|
-
export { WebRTCStats };
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
//#region src/realtime/webrtc-stats.ts
|
|
2
|
-
const DEFAULT_INTERVAL_MS = 1e3;
|
|
3
|
-
const MIN_INTERVAL_MS = 500;
|
|
4
|
-
var WebRTCStatsCollector = class {
|
|
5
|
-
pc = null;
|
|
6
|
-
intervalId = null;
|
|
7
|
-
prevBytesVideo = 0;
|
|
8
|
-
prevBytesAudio = 0;
|
|
9
|
-
prevBytesSentVideo = 0;
|
|
10
|
-
prevTimestamp = 0;
|
|
11
|
-
prevPacketsLostVideo = 0;
|
|
12
|
-
prevFramesDropped = 0;
|
|
13
|
-
prevFreezeCount = 0;
|
|
14
|
-
prevFreezeDuration = 0;
|
|
15
|
-
prevPacketsLostAudio = 0;
|
|
16
|
-
onStats = null;
|
|
17
|
-
intervalMs;
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
|
|
20
|
-
}
|
|
21
|
-
/** Attach to a peer connection and start polling. */
|
|
22
|
-
start(pc, onStats) {
|
|
23
|
-
this.stop();
|
|
24
|
-
this.pc = pc;
|
|
25
|
-
this.onStats = onStats;
|
|
26
|
-
this.prevBytesVideo = 0;
|
|
27
|
-
this.prevBytesAudio = 0;
|
|
28
|
-
this.prevBytesSentVideo = 0;
|
|
29
|
-
this.prevTimestamp = 0;
|
|
30
|
-
this.prevPacketsLostVideo = 0;
|
|
31
|
-
this.prevFramesDropped = 0;
|
|
32
|
-
this.prevFreezeCount = 0;
|
|
33
|
-
this.prevFreezeDuration = 0;
|
|
34
|
-
this.prevPacketsLostAudio = 0;
|
|
35
|
-
this.intervalId = setInterval(() => this.collect(), this.intervalMs);
|
|
36
|
-
}
|
|
37
|
-
/** Stop polling and release resources. */
|
|
38
|
-
stop() {
|
|
39
|
-
if (this.intervalId !== null) {
|
|
40
|
-
clearInterval(this.intervalId);
|
|
41
|
-
this.intervalId = null;
|
|
42
|
-
}
|
|
43
|
-
this.pc = null;
|
|
44
|
-
this.onStats = null;
|
|
45
|
-
}
|
|
46
|
-
isRunning() {
|
|
47
|
-
return this.intervalId !== null;
|
|
48
|
-
}
|
|
49
|
-
async collect() {
|
|
50
|
-
if (!this.pc || !this.onStats) return;
|
|
51
|
-
try {
|
|
52
|
-
const rawStats = await this.pc.getStats();
|
|
53
|
-
const stats = this.parse(rawStats);
|
|
54
|
-
this.onStats(stats);
|
|
55
|
-
} catch {
|
|
56
|
-
this.stop();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
parse(rawStats) {
|
|
60
|
-
const now = performance.now();
|
|
61
|
-
const elapsed = this.prevTimestamp > 0 ? (now - this.prevTimestamp) / 1e3 : 0;
|
|
62
|
-
let video = null;
|
|
63
|
-
let audio = null;
|
|
64
|
-
let outboundVideo = null;
|
|
65
|
-
const connection = {
|
|
66
|
-
currentRoundTripTime: null,
|
|
67
|
-
availableOutgoingBitrate: null
|
|
68
|
-
};
|
|
69
|
-
rawStats.forEach((report) => {
|
|
70
|
-
if (report.type === "inbound-rtp" && report.kind === "video") {
|
|
71
|
-
const bytesReceived = report.bytesReceived ?? 0;
|
|
72
|
-
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesVideo) * 8 / elapsed : 0;
|
|
73
|
-
this.prevBytesVideo = bytesReceived;
|
|
74
|
-
const r = report;
|
|
75
|
-
const packetsLost = r.packetsLost ?? 0;
|
|
76
|
-
const framesDropped = r.framesDropped ?? 0;
|
|
77
|
-
const freezeCount = r.freezeCount ?? 0;
|
|
78
|
-
const freezeDuration = r.totalFreezesDuration ?? 0;
|
|
79
|
-
video = {
|
|
80
|
-
framesDecoded: r.framesDecoded ?? 0,
|
|
81
|
-
framesDropped,
|
|
82
|
-
framesPerSecond: r.framesPerSecond ?? 0,
|
|
83
|
-
frameWidth: r.frameWidth ?? 0,
|
|
84
|
-
frameHeight: r.frameHeight ?? 0,
|
|
85
|
-
bytesReceived,
|
|
86
|
-
packetsReceived: r.packetsReceived ?? 0,
|
|
87
|
-
packetsLost,
|
|
88
|
-
jitter: r.jitter ?? 0,
|
|
89
|
-
bitrate: Math.round(bitrate),
|
|
90
|
-
freezeCount,
|
|
91
|
-
totalFreezesDuration: freezeDuration,
|
|
92
|
-
packetsLostDelta: Math.max(0, packetsLost - this.prevPacketsLostVideo),
|
|
93
|
-
framesDroppedDelta: Math.max(0, framesDropped - this.prevFramesDropped),
|
|
94
|
-
freezeCountDelta: Math.max(0, freezeCount - this.prevFreezeCount),
|
|
95
|
-
freezeDurationDelta: Math.max(0, freezeDuration - this.prevFreezeDuration)
|
|
96
|
-
};
|
|
97
|
-
this.prevPacketsLostVideo = packetsLost;
|
|
98
|
-
this.prevFramesDropped = framesDropped;
|
|
99
|
-
this.prevFreezeCount = freezeCount;
|
|
100
|
-
this.prevFreezeDuration = freezeDuration;
|
|
101
|
-
}
|
|
102
|
-
if (report.type === "outbound-rtp" && report.kind === "video") {
|
|
103
|
-
const r = report;
|
|
104
|
-
const bytesSent = r.bytesSent ?? 0;
|
|
105
|
-
const outBitrate = elapsed > 0 ? (bytesSent - this.prevBytesSentVideo) * 8 / elapsed : 0;
|
|
106
|
-
this.prevBytesSentVideo = bytesSent;
|
|
107
|
-
outboundVideo = {
|
|
108
|
-
qualityLimitationReason: r.qualityLimitationReason ?? "none",
|
|
109
|
-
qualityLimitationDurations: r.qualityLimitationDurations ?? {},
|
|
110
|
-
bytesSent,
|
|
111
|
-
packetsSent: r.packetsSent ?? 0,
|
|
112
|
-
framesPerSecond: r.framesPerSecond ?? 0,
|
|
113
|
-
frameWidth: r.frameWidth ?? 0,
|
|
114
|
-
frameHeight: r.frameHeight ?? 0,
|
|
115
|
-
bitrate: Math.round(outBitrate)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (report.type === "inbound-rtp" && report.kind === "audio") {
|
|
119
|
-
const bytesReceived = report.bytesReceived ?? 0;
|
|
120
|
-
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesAudio) * 8 / elapsed : 0;
|
|
121
|
-
this.prevBytesAudio = bytesReceived;
|
|
122
|
-
const r = report;
|
|
123
|
-
const audioPacketsLost = r.packetsLost ?? 0;
|
|
124
|
-
audio = {
|
|
125
|
-
bytesReceived,
|
|
126
|
-
packetsReceived: r.packetsReceived ?? 0,
|
|
127
|
-
packetsLost: audioPacketsLost,
|
|
128
|
-
jitter: r.jitter ?? 0,
|
|
129
|
-
bitrate: Math.round(bitrate),
|
|
130
|
-
packetsLostDelta: Math.max(0, audioPacketsLost - this.prevPacketsLostAudio)
|
|
131
|
-
};
|
|
132
|
-
this.prevPacketsLostAudio = audioPacketsLost;
|
|
133
|
-
}
|
|
134
|
-
if (report.type === "candidate-pair") {
|
|
135
|
-
const r = report;
|
|
136
|
-
if (r.state === "succeeded") {
|
|
137
|
-
connection.currentRoundTripTime = r.currentRoundTripTime ?? null;
|
|
138
|
-
connection.availableOutgoingBitrate = r.availableOutgoingBitrate ?? null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
this.prevTimestamp = now;
|
|
143
|
-
return {
|
|
144
|
-
timestamp: Date.now(),
|
|
145
|
-
video,
|
|
146
|
-
audio,
|
|
147
|
-
outboundVideo,
|
|
148
|
-
connection
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
//#endregion
|
|
154
|
-
export { WebRTCStatsCollector };
|