@decartai/sdk 0.0.67 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +43 -28
- package/dist/process/client.js +1 -3
- package/dist/process/request.js +1 -3
- package/dist/queue/client.js +1 -3
- package/dist/queue/polling.js +1 -2
- package/dist/queue/request.js +1 -3
- package/dist/realtime/client.d.ts +23 -13
- package/dist/realtime/client.js +74 -244
- package/dist/realtime/config-realtime.js +49 -0
- package/dist/realtime/event-buffer.js +1 -3
- package/dist/realtime/initial-state-gate.js +21 -0
- package/dist/realtime/media-channel.js +82 -0
- package/dist/realtime/methods.js +12 -42
- package/dist/realtime/mirror-stream.js +1 -2
- package/dist/realtime/observability/diagnostics.d.ts +39 -0
- package/dist/realtime/observability/livekit-stats-provider.js +25 -0
- package/dist/realtime/observability/realtime-observability.js +173 -0
- package/dist/realtime/{telemetry-reporter.js → observability/telemetry-reporter.js} +12 -31
- package/dist/realtime/observability/webrtc-stats.d.ts +148 -0
- package/dist/realtime/observability/webrtc-stats.js +276 -0
- package/dist/realtime/signaling-channel.js +286 -0
- package/dist/realtime/stream-session.js +252 -0
- package/dist/realtime/subscribe-client.d.ts +2 -1
- package/dist/realtime/subscribe-client.js +115 -11
- package/dist/realtime/types.d.ts +25 -1
- package/dist/shared/model.d.ts +11 -1
- package/dist/shared/model.js +51 -14
- package/dist/shared/request.js +1 -3
- package/dist/shared/types.js +1 -3
- package/dist/tokens/client.js +1 -3
- package/dist/utils/env.js +1 -2
- package/dist/utils/errors.js +1 -2
- package/dist/utils/logger.js +1 -2
- package/dist/utils/media.js +43 -0
- package/dist/utils/platform.js +13 -0
- package/dist/utils/user-agent.js +1 -3
- package/dist/version.js +1 -2
- package/package.json +2 -1
- package/dist/realtime/diagnostics.d.ts +0 -78
- package/dist/realtime/webrtc-connection.js +0 -501
- package/dist/realtime/webrtc-manager.js +0 -189
- package/dist/realtime/webrtc-stats.d.ts +0 -59
- package/dist/realtime/webrtc-stats.js +0 -154
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
2
|
+
//#region src/realtime/observability/webrtc-stats.ts
|
|
3
|
+
var WebRTCStatsCollector = class {
|
|
4
|
+
source = null;
|
|
5
|
+
intervalId = null;
|
|
6
|
+
prevBytesVideo = 0;
|
|
7
|
+
prevBytesAudio = 0;
|
|
8
|
+
prevBytesSentVideo = 0;
|
|
9
|
+
prevTimestamp = 0;
|
|
10
|
+
prevPacketsLostVideo = 0;
|
|
11
|
+
prevFramesDropped = 0;
|
|
12
|
+
prevFreezeCount = 0;
|
|
13
|
+
prevFreezeDuration = 0;
|
|
14
|
+
prevPacketsLostAudio = 0;
|
|
15
|
+
prevNackCountInbound = 0;
|
|
16
|
+
onStats = null;
|
|
17
|
+
intervalMs;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.intervalMs = Math.max(options.intervalMs ?? REALTIME_CONFIG.observability.statsDefaultIntervalMs, REALTIME_CONFIG.observability.statsMinIntervalMs);
|
|
20
|
+
}
|
|
21
|
+
/** Attach to a stats provider and start polling. */
|
|
22
|
+
start(source, onStats) {
|
|
23
|
+
this.stop();
|
|
24
|
+
this.source = source;
|
|
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.prevNackCountInbound = 0;
|
|
36
|
+
this.intervalId = setInterval(() => this.collect(), this.intervalMs);
|
|
37
|
+
}
|
|
38
|
+
/** Stop polling and release resources. */
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.intervalId !== null) {
|
|
41
|
+
clearInterval(this.intervalId);
|
|
42
|
+
this.intervalId = null;
|
|
43
|
+
}
|
|
44
|
+
this.source = null;
|
|
45
|
+
this.onStats = null;
|
|
46
|
+
}
|
|
47
|
+
isRunning() {
|
|
48
|
+
return this.intervalId !== null;
|
|
49
|
+
}
|
|
50
|
+
async collect() {
|
|
51
|
+
if (!this.source || !this.onStats) return;
|
|
52
|
+
try {
|
|
53
|
+
const rawStats = await this.source.getStats();
|
|
54
|
+
const stats = this.parse(rawStats);
|
|
55
|
+
this.onStats(stats);
|
|
56
|
+
} catch {
|
|
57
|
+
this.stop();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
parse(rawStats) {
|
|
61
|
+
const now = performance.now();
|
|
62
|
+
const elapsed = this.prevTimestamp > 0 ? (now - this.prevTimestamp) / 1e3 : 0;
|
|
63
|
+
let video = null;
|
|
64
|
+
let audio = null;
|
|
65
|
+
let outboundVideo = null;
|
|
66
|
+
let remoteInbound = null;
|
|
67
|
+
const connection = {
|
|
68
|
+
currentRoundTripTime: null,
|
|
69
|
+
availableOutgoingBitrate: null,
|
|
70
|
+
selectedCandidatePairs: []
|
|
71
|
+
};
|
|
72
|
+
const succeededPairs = [];
|
|
73
|
+
rawStats.forEach((report) => {
|
|
74
|
+
if (report.type === "inbound-rtp" && report.kind === "video") {
|
|
75
|
+
const bytesReceived = report.bytesReceived ?? 0;
|
|
76
|
+
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesVideo) * 8 / elapsed : 0;
|
|
77
|
+
this.prevBytesVideo = bytesReceived;
|
|
78
|
+
const r = report;
|
|
79
|
+
const packetsLost = r.packetsLost ?? 0;
|
|
80
|
+
const framesDropped = r.framesDropped ?? 0;
|
|
81
|
+
const freezeCount = r.freezeCount ?? 0;
|
|
82
|
+
const freezeDuration = r.totalFreezesDuration ?? 0;
|
|
83
|
+
const framesDecoded = r.framesDecoded ?? 0;
|
|
84
|
+
const nackCount = r.nackCount ?? 0;
|
|
85
|
+
const jbEmitted = r.jitterBufferEmittedCount ?? 0;
|
|
86
|
+
const totalDecodeTime = r.totalDecodeTime ?? 0;
|
|
87
|
+
const totalProcessingDelay = r.totalProcessingDelay ?? 0;
|
|
88
|
+
const totalInterFrameDelay = r.totalInterFrameDelay ?? 0;
|
|
89
|
+
const totalSquaredInterFrameDelay = r.totalSquaredInterFrameDelay ?? 0;
|
|
90
|
+
const jitterBufferDelay = r.jitterBufferDelay ?? 0;
|
|
91
|
+
const jitterBufferTargetDelay = r.jitterBufferTargetDelay ?? 0;
|
|
92
|
+
const jitterBufferMinimumDelay = r.jitterBufferMinimumDelay ?? 0;
|
|
93
|
+
const avgDecodeTimeMs = framesDecoded > 0 ? totalDecodeTime / framesDecoded * 1e3 : null;
|
|
94
|
+
const avgProcessingDelayMs = framesDecoded > 0 ? totalProcessingDelay / framesDecoded * 1e3 : null;
|
|
95
|
+
const avgInterFrameDelayMs = framesDecoded > 0 ? totalInterFrameDelay / framesDecoded * 1e3 : null;
|
|
96
|
+
const interFrameDelayStdDevMs = framesDecoded > 0 ? Math.sqrt(Math.max(0, totalSquaredInterFrameDelay / framesDecoded - (totalInterFrameDelay / framesDecoded) ** 2)) * 1e3 : null;
|
|
97
|
+
const avgJitterBufferMs = jbEmitted > 0 ? jitterBufferDelay / jbEmitted * 1e3 : null;
|
|
98
|
+
const jitterBufferTargetDelayMs = jbEmitted > 0 ? jitterBufferTargetDelay / jbEmitted * 1e3 : null;
|
|
99
|
+
const jitterBufferMinimumDelayMs = jbEmitted > 0 ? jitterBufferMinimumDelay / jbEmitted * 1e3 : null;
|
|
100
|
+
video = {
|
|
101
|
+
framesDecoded,
|
|
102
|
+
framesDropped,
|
|
103
|
+
framesReceived: r.framesReceived ?? 0,
|
|
104
|
+
keyFramesDecoded: r.keyFramesDecoded ?? 0,
|
|
105
|
+
framesPerSecond: r.framesPerSecond ?? 0,
|
|
106
|
+
frameWidth: r.frameWidth ?? 0,
|
|
107
|
+
frameHeight: r.frameHeight ?? 0,
|
|
108
|
+
bytesReceived,
|
|
109
|
+
packetsReceived: r.packetsReceived ?? 0,
|
|
110
|
+
packetsLost,
|
|
111
|
+
jitter: r.jitter ?? 0,
|
|
112
|
+
bitrate: Math.round(bitrate),
|
|
113
|
+
freezeCount,
|
|
114
|
+
totalFreezesDuration: freezeDuration,
|
|
115
|
+
packetsLostDelta: Math.max(0, packetsLost - this.prevPacketsLostVideo),
|
|
116
|
+
framesDroppedDelta: Math.max(0, framesDropped - this.prevFramesDropped),
|
|
117
|
+
freezeCountDelta: Math.max(0, freezeCount - this.prevFreezeCount),
|
|
118
|
+
freezeDurationDelta: Math.max(0, freezeDuration - this.prevFreezeDuration),
|
|
119
|
+
nackCount,
|
|
120
|
+
nackCountDelta: Math.max(0, nackCount - this.prevNackCountInbound),
|
|
121
|
+
pliCount: r.pliCount ?? 0,
|
|
122
|
+
firCount: r.firCount ?? 0,
|
|
123
|
+
avgDecodeTimeMs,
|
|
124
|
+
avgJitterBufferMs,
|
|
125
|
+
avgProcessingDelayMs,
|
|
126
|
+
avgInterFrameDelayMs,
|
|
127
|
+
interFrameDelayStdDevMs,
|
|
128
|
+
jitterBufferTargetDelayMs,
|
|
129
|
+
jitterBufferMinimumDelayMs,
|
|
130
|
+
decoderImplementation: r.decoderImplementation ?? ""
|
|
131
|
+
};
|
|
132
|
+
this.prevPacketsLostVideo = packetsLost;
|
|
133
|
+
this.prevFramesDropped = framesDropped;
|
|
134
|
+
this.prevFreezeCount = freezeCount;
|
|
135
|
+
this.prevFreezeDuration = freezeDuration;
|
|
136
|
+
this.prevNackCountInbound = nackCount;
|
|
137
|
+
}
|
|
138
|
+
if (report.type === "outbound-rtp" && report.kind === "video") {
|
|
139
|
+
const r = report;
|
|
140
|
+
const bytesSent = r.bytesSent ?? 0;
|
|
141
|
+
const packetsSent = r.packetsSent ?? 0;
|
|
142
|
+
const frameWidth = r.frameWidth ?? 0;
|
|
143
|
+
const frameHeight = r.frameHeight ?? 0;
|
|
144
|
+
const pixels = frameWidth * frameHeight;
|
|
145
|
+
const framesEncoded = r.framesEncoded ?? 0;
|
|
146
|
+
const totalEncodeTime = r.totalEncodeTime ?? 0;
|
|
147
|
+
const totalPacketSendDelay = r.totalPacketSendDelay ?? 0;
|
|
148
|
+
const qpSum = r.qpSum ?? 0;
|
|
149
|
+
const nackCount = r.nackCount ?? 0;
|
|
150
|
+
const pliCount = r.pliCount ?? 0;
|
|
151
|
+
const firCount = r.firCount ?? 0;
|
|
152
|
+
const retransmittedBytesSent = r.retransmittedBytesSent ?? 0;
|
|
153
|
+
const retransmittedPacketsSent = r.retransmittedPacketsSent ?? 0;
|
|
154
|
+
const targetBitrate = r.targetBitrate ?? null;
|
|
155
|
+
const avgEncodeTimeMs = framesEncoded > 0 ? totalEncodeTime / framesEncoded * 1e3 : null;
|
|
156
|
+
const avgPacketSendDelayMs = packetsSent > 0 ? totalPacketSendDelay / packetsSent * 1e3 : null;
|
|
157
|
+
const avgQp = framesEncoded > 0 ? qpSum / framesEncoded : null;
|
|
158
|
+
if (outboundVideo === null) outboundVideo = {
|
|
159
|
+
qualityLimitationReason: r.qualityLimitationReason ?? "none",
|
|
160
|
+
qualityLimitationDurations: r.qualityLimitationDurations ?? {},
|
|
161
|
+
bytesSent,
|
|
162
|
+
packetsSent,
|
|
163
|
+
framesPerSecond: r.framesPerSecond ?? 0,
|
|
164
|
+
frameWidth,
|
|
165
|
+
frameHeight,
|
|
166
|
+
bitrate: 0,
|
|
167
|
+
targetBitrateKbps: targetBitrate != null ? Math.round(targetBitrate / 1e3) : null,
|
|
168
|
+
avgEncodeTimeMs,
|
|
169
|
+
avgPacketSendDelayMs,
|
|
170
|
+
avgQp,
|
|
171
|
+
nackCount,
|
|
172
|
+
pliCount,
|
|
173
|
+
firCount,
|
|
174
|
+
retransmittedBytesSent,
|
|
175
|
+
retransmittedPacketsSent,
|
|
176
|
+
encoderImplementation: r.encoderImplementation ?? ""
|
|
177
|
+
};
|
|
178
|
+
else {
|
|
179
|
+
outboundVideo.bytesSent += bytesSent;
|
|
180
|
+
outboundVideo.packetsSent += packetsSent;
|
|
181
|
+
outboundVideo.nackCount += nackCount;
|
|
182
|
+
outboundVideo.pliCount += pliCount;
|
|
183
|
+
outboundVideo.firCount += firCount;
|
|
184
|
+
outboundVideo.retransmittedBytesSent += retransmittedBytesSent;
|
|
185
|
+
outboundVideo.retransmittedPacketsSent += retransmittedPacketsSent;
|
|
186
|
+
if (pixels > outboundVideo.frameWidth * outboundVideo.frameHeight) {
|
|
187
|
+
outboundVideo.frameWidth = frameWidth;
|
|
188
|
+
outboundVideo.frameHeight = frameHeight;
|
|
189
|
+
outboundVideo.framesPerSecond = r.framesPerSecond ?? 0;
|
|
190
|
+
outboundVideo.qualityLimitationReason = r.qualityLimitationReason ?? "none";
|
|
191
|
+
outboundVideo.qualityLimitationDurations = r.qualityLimitationDurations ?? {};
|
|
192
|
+
outboundVideo.targetBitrateKbps = targetBitrate != null ? Math.round(targetBitrate / 1e3) : null;
|
|
193
|
+
outboundVideo.avgEncodeTimeMs = avgEncodeTimeMs;
|
|
194
|
+
outboundVideo.avgPacketSendDelayMs = avgPacketSendDelayMs;
|
|
195
|
+
outboundVideo.avgQp = avgQp;
|
|
196
|
+
outboundVideo.encoderImplementation = r.encoderImplementation ?? "";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (report.type === "remote-inbound-rtp" && report.kind === "video") {
|
|
201
|
+
const r = report;
|
|
202
|
+
remoteInbound = {
|
|
203
|
+
fractionLost: r.fractionLost ?? null,
|
|
204
|
+
jitter: r.jitter ?? null,
|
|
205
|
+
roundTripTime: r.roundTripTime ?? null
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (report.type === "inbound-rtp" && report.kind === "audio") {
|
|
209
|
+
const bytesReceived = report.bytesReceived ?? 0;
|
|
210
|
+
const bitrate = elapsed > 0 ? (bytesReceived - this.prevBytesAudio) * 8 / elapsed : 0;
|
|
211
|
+
this.prevBytesAudio = bytesReceived;
|
|
212
|
+
const r = report;
|
|
213
|
+
const audioPacketsLost = r.packetsLost ?? 0;
|
|
214
|
+
audio = {
|
|
215
|
+
bytesReceived,
|
|
216
|
+
packetsReceived: r.packetsReceived ?? 0,
|
|
217
|
+
packetsLost: audioPacketsLost,
|
|
218
|
+
jitter: r.jitter ?? 0,
|
|
219
|
+
bitrate: Math.round(bitrate),
|
|
220
|
+
packetsLostDelta: Math.max(0, audioPacketsLost - this.prevPacketsLostAudio)
|
|
221
|
+
};
|
|
222
|
+
this.prevPacketsLostAudio = audioPacketsLost;
|
|
223
|
+
}
|
|
224
|
+
if (report.type === "candidate-pair") {
|
|
225
|
+
const r = report;
|
|
226
|
+
if (r.state === "succeeded") {
|
|
227
|
+
connection.currentRoundTripTime = r.currentRoundTripTime ?? null;
|
|
228
|
+
connection.availableOutgoingBitrate = r.availableOutgoingBitrate ?? null;
|
|
229
|
+
const localId = r.localCandidateId;
|
|
230
|
+
const remoteId = r.remoteCandidateId;
|
|
231
|
+
if (localId && remoteId) succeededPairs.push({
|
|
232
|
+
localId,
|
|
233
|
+
remoteId
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
if (succeededPairs.length > 0) {
|
|
239
|
+
const toInfo = (id) => {
|
|
240
|
+
const c = rawStats.get(id);
|
|
241
|
+
if (!c) return null;
|
|
242
|
+
return {
|
|
243
|
+
candidateType: c.candidateType ?? "",
|
|
244
|
+
address: c.address ?? c.ip ?? "",
|
|
245
|
+
port: c.port ?? 0,
|
|
246
|
+
protocol: c.protocol ?? ""
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
for (const { localId, remoteId } of succeededPairs) {
|
|
250
|
+
const local = toInfo(localId);
|
|
251
|
+
const remote = toInfo(remoteId);
|
|
252
|
+
if (local && remote) connection.selectedCandidatePairs.push({
|
|
253
|
+
local,
|
|
254
|
+
remote
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const ov = outboundVideo;
|
|
259
|
+
if (ov !== null) {
|
|
260
|
+
const outBitrate = elapsed > 0 ? (ov.bytesSent - this.prevBytesSentVideo) * 8 / elapsed : 0;
|
|
261
|
+
ov.bitrate = Math.max(0, Math.round(outBitrate));
|
|
262
|
+
this.prevBytesSentVideo = ov.bytesSent;
|
|
263
|
+
}
|
|
264
|
+
this.prevTimestamp = now;
|
|
265
|
+
return {
|
|
266
|
+
timestamp: Date.now(),
|
|
267
|
+
video,
|
|
268
|
+
audio,
|
|
269
|
+
outboundVideo,
|
|
270
|
+
connection,
|
|
271
|
+
remoteInbound
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
276
|
+
export { WebRTCStatsCollector };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { buildUserAgent } from "../utils/user-agent.js";
|
|
2
|
+
import { createConsoleLogger } from "../utils/logger.js";
|
|
3
|
+
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
4
|
+
import mitt from "mitt";
|
|
5
|
+
//#region src/realtime/signaling-channel.ts
|
|
6
|
+
var SignalingChannel = class {
|
|
7
|
+
ws = null;
|
|
8
|
+
events = mitt();
|
|
9
|
+
pendingAcks = [];
|
|
10
|
+
pendingRoomInfo = null;
|
|
11
|
+
connected = false;
|
|
12
|
+
closing = false;
|
|
13
|
+
logger;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.logger = config.logger ?? createConsoleLogger("warn");
|
|
17
|
+
}
|
|
18
|
+
on(event, handler) {
|
|
19
|
+
this.events.on(event, handler);
|
|
20
|
+
}
|
|
21
|
+
off(event, handler) {
|
|
22
|
+
this.events.off(event, handler);
|
|
23
|
+
}
|
|
24
|
+
async openAndJoin(opts = {}) {
|
|
25
|
+
const connectTimeout = opts.connectTimeout ?? REALTIME_CONFIG.signaling.connectTimeoutMs;
|
|
26
|
+
const handshakeTimeout = opts.handshakeTimeout ?? REALTIME_CONFIG.signaling.handshakeTimeoutMs;
|
|
27
|
+
this.config.observability?.startPhase("websocket-open");
|
|
28
|
+
await this.openSocket(connectTimeout);
|
|
29
|
+
this.config.observability?.endPhase("websocket-open", { success: true });
|
|
30
|
+
this.config.observability?.startPhase("room-join");
|
|
31
|
+
const roomInfoWait = this.waitForRoomInfo(handshakeTimeout);
|
|
32
|
+
if (!this.writeMessage({ type: "livekit_join" })) {
|
|
33
|
+
roomInfoWait.cancel();
|
|
34
|
+
throw new Error("WebSocket is not open");
|
|
35
|
+
}
|
|
36
|
+
let roomInfo;
|
|
37
|
+
try {
|
|
38
|
+
roomInfo = await roomInfoWait.promise;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.rejectAllPending(error instanceof Error ? error : new Error(String(error)));
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
this.config.observability?.endPhase("room-join", { success: true });
|
|
44
|
+
this.connected = true;
|
|
45
|
+
const initialStateAck = this.sendInitialStateTracked(opts.initialState);
|
|
46
|
+
initialStateAck.catch(() => {});
|
|
47
|
+
return {
|
|
48
|
+
roomInfo,
|
|
49
|
+
initialStateAck
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async sendInitialStateTracked(initialState) {
|
|
53
|
+
if (!initialState) return;
|
|
54
|
+
this.config.observability?.startPhase("initial-state-handshake");
|
|
55
|
+
await this.sendInitialState(initialState);
|
|
56
|
+
this.config.observability?.endPhase("initial-state-handshake", { success: true });
|
|
57
|
+
}
|
|
58
|
+
close() {
|
|
59
|
+
this.closing = true;
|
|
60
|
+
this.connected = false;
|
|
61
|
+
const ws = this.ws;
|
|
62
|
+
this.ws = null;
|
|
63
|
+
if (ws) try {
|
|
64
|
+
ws.close();
|
|
65
|
+
} catch {}
|
|
66
|
+
this.rejectPendingRoomInfo(/* @__PURE__ */ new Error("Control channel closed"));
|
|
67
|
+
this.rejectAllPending(/* @__PURE__ */ new Error("Control channel closed"));
|
|
68
|
+
}
|
|
69
|
+
async sendPrompt(text, opts = {}) {
|
|
70
|
+
const ack = await this.request({
|
|
71
|
+
message: {
|
|
72
|
+
type: "prompt",
|
|
73
|
+
prompt: text,
|
|
74
|
+
enhance_prompt: opts.enhance ?? true
|
|
75
|
+
},
|
|
76
|
+
matchAck: (msg) => msg.type === "prompt_ack" && msg.prompt === text,
|
|
77
|
+
timeoutMs: opts.timeout ?? REALTIME_CONFIG.signaling.requestTimeoutMs,
|
|
78
|
+
label: "Prompt send"
|
|
79
|
+
});
|
|
80
|
+
if (!ack.success) throw new Error(ack.error ?? "Failed to send prompt");
|
|
81
|
+
}
|
|
82
|
+
async setImage(image, opts = {}) {
|
|
83
|
+
const message = {
|
|
84
|
+
type: "set_image",
|
|
85
|
+
image_data: image
|
|
86
|
+
};
|
|
87
|
+
if (opts.prompt !== void 0) message.prompt = opts.prompt;
|
|
88
|
+
if (opts.enhance !== void 0) message.enhance_prompt = opts.enhance;
|
|
89
|
+
const ack = await this.request({
|
|
90
|
+
message,
|
|
91
|
+
matchAck: (msg) => msg.type === "set_image_ack",
|
|
92
|
+
timeoutMs: opts.timeout ?? REALTIME_CONFIG.signaling.requestTimeoutMs,
|
|
93
|
+
label: "Image send"
|
|
94
|
+
});
|
|
95
|
+
if (!ack.success) throw new Error(ack.error ?? "Failed to send image");
|
|
96
|
+
}
|
|
97
|
+
async openSocket(timeout) {
|
|
98
|
+
const userAgent = encodeURIComponent(buildUserAgent(this.config.integration));
|
|
99
|
+
const separator = this.config.url.includes("?") ? "&" : "?";
|
|
100
|
+
const wsUrl = `${this.config.url}${separator}user_agent=${userAgent}`;
|
|
101
|
+
this.closing = false;
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`WebSocket open timeout (${timeout}ms)`)), timeout);
|
|
104
|
+
const ws = new WebSocket(wsUrl);
|
|
105
|
+
this.ws = ws;
|
|
106
|
+
ws.onopen = () => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve();
|
|
109
|
+
};
|
|
110
|
+
ws.onclose = (e) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
const wasConnected = this.connected;
|
|
113
|
+
const pendingCount = this.pendingAcks.length;
|
|
114
|
+
this.connected = false;
|
|
115
|
+
this.ws = null;
|
|
116
|
+
this.logger.warn("signaling: websocket closed", {
|
|
117
|
+
code: e.code,
|
|
118
|
+
reason: e.reason,
|
|
119
|
+
wasConnected,
|
|
120
|
+
closing: this.closing,
|
|
121
|
+
pendingAcks: pendingCount
|
|
122
|
+
});
|
|
123
|
+
const error = /* @__PURE__ */ new Error(`WebSocket closed: ${e.code} ${e.reason}`);
|
|
124
|
+
this.rejectPendingRoomInfo(error);
|
|
125
|
+
this.rejectAllPending(error);
|
|
126
|
+
if (wasConnected || this.closing) this.events.emit("closed", {
|
|
127
|
+
code: e.code,
|
|
128
|
+
reason: e.reason
|
|
129
|
+
});
|
|
130
|
+
else reject(error);
|
|
131
|
+
};
|
|
132
|
+
ws.onerror = () => {};
|
|
133
|
+
ws.onmessage = (e) => {
|
|
134
|
+
try {
|
|
135
|
+
this.handleMessage(JSON.parse(e.data));
|
|
136
|
+
} catch {}
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
waitForRoomInfo(timeoutMs) {
|
|
141
|
+
let cleanup = () => {};
|
|
142
|
+
return {
|
|
143
|
+
promise: new Promise((resolve, reject) => {
|
|
144
|
+
let timer = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
this.logger.warn("signaling: livekit_room_info timeout", { timeoutMs });
|
|
147
|
+
reject(/* @__PURE__ */ new Error(`livekit_room_info timeout (${timeoutMs}ms)`));
|
|
148
|
+
}, timeoutMs);
|
|
149
|
+
const pendingRoomInfo = {
|
|
150
|
+
resolve: (info) => {
|
|
151
|
+
cleanup();
|
|
152
|
+
resolve(info);
|
|
153
|
+
},
|
|
154
|
+
reject: (err) => {
|
|
155
|
+
cleanup();
|
|
156
|
+
reject(err);
|
|
157
|
+
},
|
|
158
|
+
cancel: () => {
|
|
159
|
+
cleanup();
|
|
160
|
+
},
|
|
161
|
+
pauseTimeout: () => {
|
|
162
|
+
if (timer) {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
timer = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
cleanup = () => {
|
|
169
|
+
if (timer) {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
timer = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.pendingRoomInfo === pendingRoomInfo) this.pendingRoomInfo = null;
|
|
174
|
+
};
|
|
175
|
+
this.pendingRoomInfo = pendingRoomInfo;
|
|
176
|
+
}),
|
|
177
|
+
cancel: cleanup
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async sendInitialState(initialState) {
|
|
181
|
+
if (!initialState) return;
|
|
182
|
+
if (initialState.image !== void 0) {
|
|
183
|
+
await this.setImage(initialState.image, {
|
|
184
|
+
prompt: initialState.prompt,
|
|
185
|
+
enhance: initialState.enhance
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (initialState.prompt !== void 0 && initialState.prompt !== null) await this.sendPrompt(initialState.prompt, { enhance: initialState.enhance });
|
|
190
|
+
}
|
|
191
|
+
async request({ message, matchAck, timeoutMs, label }) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
cleanup();
|
|
195
|
+
this.logger.warn("signaling: ack timed out", {
|
|
196
|
+
label,
|
|
197
|
+
timeoutMs
|
|
198
|
+
});
|
|
199
|
+
reject(/* @__PURE__ */ new Error(`${label} timed out`));
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
const entry = {
|
|
202
|
+
matches: matchAck,
|
|
203
|
+
onMatch: (msg) => {
|
|
204
|
+
cleanup();
|
|
205
|
+
resolve(msg);
|
|
206
|
+
},
|
|
207
|
+
reject: (err) => {
|
|
208
|
+
cleanup();
|
|
209
|
+
reject(err);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const cleanup = () => {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
this.pendingAcks = this.pendingAcks.filter((e) => e !== entry);
|
|
215
|
+
};
|
|
216
|
+
this.pendingAcks.push(entry);
|
|
217
|
+
if (!this.writeMessage(message)) {
|
|
218
|
+
cleanup();
|
|
219
|
+
reject(/* @__PURE__ */ new Error("WebSocket is not open"));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
writeMessage(message) {
|
|
224
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return false;
|
|
225
|
+
this.ws.send(JSON.stringify(message));
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
handleMessage(msg) {
|
|
229
|
+
for (const ack of [...this.pendingAcks]) if (ack.matches(msg)) {
|
|
230
|
+
ack.onMatch(msg);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
switch (msg.type) {
|
|
234
|
+
case "livekit_room_info":
|
|
235
|
+
this.resolvePendingRoomInfo({
|
|
236
|
+
livekitUrl: msg.livekit_url,
|
|
237
|
+
token: msg.token,
|
|
238
|
+
roomName: msg.room_name,
|
|
239
|
+
sessionId: msg.session_id
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
242
|
+
case "queue_position":
|
|
243
|
+
this.pendingRoomInfo?.pauseTimeout();
|
|
244
|
+
this.events.emit("queuePosition", {
|
|
245
|
+
position: msg.position,
|
|
246
|
+
queueSize: msg.queue_size
|
|
247
|
+
});
|
|
248
|
+
break;
|
|
249
|
+
case "generation_tick":
|
|
250
|
+
this.events.emit("generationTick", { seconds: msg.seconds });
|
|
251
|
+
break;
|
|
252
|
+
case "generation_ended":
|
|
253
|
+
this.events.emit("generationEnded", {
|
|
254
|
+
seconds: msg.seconds,
|
|
255
|
+
reason: msg.reason
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
case "error": {
|
|
259
|
+
const error = new Error(msg.error);
|
|
260
|
+
error.source = "server";
|
|
261
|
+
this.logger.error("signaling: server error received", { error: msg.error });
|
|
262
|
+
this.events.emit("serverError", error);
|
|
263
|
+
this.rejectPendingRoomInfo(error);
|
|
264
|
+
this.rejectAllPending(error);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
resolvePendingRoomInfo(info) {
|
|
270
|
+
const pending = this.pendingRoomInfo;
|
|
271
|
+
if (!pending) return;
|
|
272
|
+
pending.resolve(info);
|
|
273
|
+
}
|
|
274
|
+
rejectPendingRoomInfo(error) {
|
|
275
|
+
const pending = this.pendingRoomInfo;
|
|
276
|
+
if (!pending) return;
|
|
277
|
+
pending.reject(error);
|
|
278
|
+
}
|
|
279
|
+
rejectAllPending(error) {
|
|
280
|
+
const pending = this.pendingAcks;
|
|
281
|
+
this.pendingAcks = [];
|
|
282
|
+
for (const entry of pending) entry.reject(error);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
export { SignalingChannel };
|