@decartai/sdk 0.0.68 → 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 +49 -9
- package/dist/index.d.ts +6 -4
- 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 +17 -11
- package/dist/realtime/client.js +71 -155
- 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 +14 -53
- package/dist/realtime/observability/livekit-stats-provider.js +25 -0
- package/dist/realtime/observability/realtime-observability.js +70 -6
- package/dist/realtime/observability/telemetry-reporter.js +9 -28
- package/dist/realtime/observability/webrtc-stats.d.ts +5 -4
- package/dist/realtime/observability/webrtc-stats.js +3 -5
- package/dist/realtime/signaling-channel.js +286 -0
- package/dist/realtime/stream-session.js +252 -0
- package/dist/realtime/subscribe-client.d.ts +2 -3
- 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/webrtc-connection.js +0 -500
- package/dist/realtime/webrtc-manager.js +0 -210
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/realtime/config-realtime.ts
|
|
2
|
+
const REALTIME_CONFIG = {
|
|
3
|
+
signaling: {
|
|
4
|
+
connectTimeoutMs: 6e4,
|
|
5
|
+
handshakeTimeoutMs: 15e3,
|
|
6
|
+
requestTimeoutMs: 3e4
|
|
7
|
+
},
|
|
8
|
+
session: {
|
|
9
|
+
connectionTimeoutMs: 6e4 * 5,
|
|
10
|
+
retry: {
|
|
11
|
+
retries: 5,
|
|
12
|
+
factor: 2,
|
|
13
|
+
minTimeout: 1e3,
|
|
14
|
+
maxTimeout: 1e4
|
|
15
|
+
},
|
|
16
|
+
permanentErrorSubstrings: [
|
|
17
|
+
"permission denied",
|
|
18
|
+
"not allowed",
|
|
19
|
+
"invalid session",
|
|
20
|
+
"401",
|
|
21
|
+
"invalid api key",
|
|
22
|
+
"unauthorized"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
methods: {
|
|
26
|
+
promptTimeoutMs: 15e3,
|
|
27
|
+
updateTimeoutMs: 3e4
|
|
28
|
+
},
|
|
29
|
+
livekit: {
|
|
30
|
+
inferenceServerIdentityPrefix: "inference-server-",
|
|
31
|
+
roomOptions: {
|
|
32
|
+
adaptiveStream: false,
|
|
33
|
+
dynacast: false
|
|
34
|
+
},
|
|
35
|
+
defaultVideoCodec: "h264",
|
|
36
|
+
defaultMaxVideoBitrateBps: 35e5,
|
|
37
|
+
defaultPublishFps: 30
|
|
38
|
+
},
|
|
39
|
+
observability: {
|
|
40
|
+
stallFpsThreshold: .5,
|
|
41
|
+
statsDefaultIntervalMs: 1e3,
|
|
42
|
+
statsMinIntervalMs: 500,
|
|
43
|
+
telemetryReportIntervalMs: 1e4,
|
|
44
|
+
telemetryUrl: "https://platform.decart.ai/api/v1/telemetry",
|
|
45
|
+
telemetryMaxItemsPerReport: 120
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { REALTIME_CONFIG };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import mitt from "mitt";
|
|
2
|
-
|
|
3
2
|
//#region src/realtime/event-buffer.ts
|
|
4
3
|
function createEventBuffer() {
|
|
5
4
|
const emitter = mitt();
|
|
@@ -30,6 +29,5 @@ function createEventBuffer() {
|
|
|
30
29
|
stop
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
|
-
|
|
34
32
|
//#endregion
|
|
35
|
-
export { createEventBuffer };
|
|
33
|
+
export { createEventBuffer };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/realtime/initial-state-gate.ts
|
|
2
|
+
var InitialStateGate = class {
|
|
3
|
+
attemptId = 0;
|
|
4
|
+
startAttempt(initialState) {
|
|
5
|
+
const attemptId = ++this.attemptId;
|
|
6
|
+
const shouldWait = hasCallerProvidedInitialState(initialState);
|
|
7
|
+
return { waitForReadiness: async (initialStateAck) => {
|
|
8
|
+
if (shouldWait) await initialStateAck;
|
|
9
|
+
return this.attemptId === attemptId;
|
|
10
|
+
} };
|
|
11
|
+
}
|
|
12
|
+
reset() {
|
|
13
|
+
this.attemptId++;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function hasCallerProvidedInitialState(state) {
|
|
17
|
+
if (!state) return false;
|
|
18
|
+
return state.image !== void 0 && state.image !== null || state.prompt !== void 0 && state.prompt !== null;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { InitialStateGate };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createConsoleLogger } from "../utils/logger.js";
|
|
2
|
+
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
3
|
+
import mitt from "mitt";
|
|
4
|
+
import { Room, RoomEvent, Track, TrackEvent } from "livekit-client";
|
|
5
|
+
//#region src/realtime/media-channel.ts
|
|
6
|
+
function getDefaultVideoPublishOptions(videoCodec) {
|
|
7
|
+
const videoEncoding = {
|
|
8
|
+
maxBitrate: REALTIME_CONFIG.livekit.defaultMaxVideoBitrateBps,
|
|
9
|
+
maxFramerate: REALTIME_CONFIG.livekit.defaultPublishFps
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
source: Track.Source.Camera,
|
|
13
|
+
videoCodec: videoCodec ?? REALTIME_CONFIG.livekit.defaultVideoCodec,
|
|
14
|
+
simulcast: true,
|
|
15
|
+
videoEncoding
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var MediaChannel = class {
|
|
19
|
+
room = null;
|
|
20
|
+
remoteStream = null;
|
|
21
|
+
events = mitt();
|
|
22
|
+
logger;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = config.logger ?? createConsoleLogger("warn");
|
|
26
|
+
}
|
|
27
|
+
get localStream() {
|
|
28
|
+
return this.config.localStream;
|
|
29
|
+
}
|
|
30
|
+
on(event, handler) {
|
|
31
|
+
this.events.on(event, handler);
|
|
32
|
+
}
|
|
33
|
+
off(event, handler) {
|
|
34
|
+
this.events.off(event, handler);
|
|
35
|
+
}
|
|
36
|
+
async connect(opts) {
|
|
37
|
+
this.room ??= new Room(REALTIME_CONFIG.livekit.roomOptions);
|
|
38
|
+
const room = this.room;
|
|
39
|
+
room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
|
|
40
|
+
if (!participant.identity.startsWith(REALTIME_CONFIG.livekit.inferenceServerIdentityPrefix)) return;
|
|
41
|
+
if (track.kind !== Track.Kind.Video && track.kind !== Track.Kind.Audio) return;
|
|
42
|
+
track.attach();
|
|
43
|
+
const mediaStreamTrack = track.mediaStreamTrack;
|
|
44
|
+
if (mediaStreamTrack) {
|
|
45
|
+
this.remoteStream ??= new MediaStream();
|
|
46
|
+
if (!this.remoteStream.getTracks().includes(mediaStreamTrack)) this.remoteStream.addTrack(mediaStreamTrack);
|
|
47
|
+
this.events.emit("remoteStream", this.remoteStream);
|
|
48
|
+
}
|
|
49
|
+
track.on(TrackEvent.VideoPlaybackStarted, () => {
|
|
50
|
+
this.events.emit("firstFrame");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
room.on(RoomEvent.Disconnected, (reason) => {
|
|
54
|
+
this.logger.warn("livekit: room disconnected", { reason });
|
|
55
|
+
this.events.emit("disconnected", { reason });
|
|
56
|
+
});
|
|
57
|
+
this.config.observability?.startPhase("webrtc-handshake");
|
|
58
|
+
await room.connect(opts.url, opts.token);
|
|
59
|
+
this.config.observability?.endPhase("webrtc-handshake", { success: true });
|
|
60
|
+
this.config.observability?.setLiveKitRoom(room);
|
|
61
|
+
}
|
|
62
|
+
async publishLocalTracks() {
|
|
63
|
+
if (!this.config.localStream) return;
|
|
64
|
+
this.config.observability?.startPhase("publish-local-track");
|
|
65
|
+
await this.publishTracks(this.config.localStream);
|
|
66
|
+
this.config.observability?.endPhase("publish-local-track", { success: true });
|
|
67
|
+
}
|
|
68
|
+
disconnect() {
|
|
69
|
+
const room = this.room;
|
|
70
|
+
this.room = null;
|
|
71
|
+
this.remoteStream = null;
|
|
72
|
+
this.config.observability?.setLiveKitRoom(null);
|
|
73
|
+
if (room) room.disconnect().catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
async publishTracks(stream) {
|
|
76
|
+
if (!this.room) return;
|
|
77
|
+
for (const track of stream.getTracks()) if (track.kind === "video") await this.room.localParticipant.publishTrack(track, getDefaultVideoPublishOptions(this.config.videoCodec));
|
|
78
|
+
else await this.room.localParticipant.publishTrack(track);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { MediaChannel };
|
package/dist/realtime/methods.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
|
|
3
3
|
//#region src/realtime/methods.ts
|
|
4
|
-
const PROMPT_TIMEOUT_MS = 15 * 1e3;
|
|
5
|
-
const UPDATE_TIMEOUT_MS = 30 * 1e3;
|
|
6
4
|
const setInputSchema = z.object({
|
|
7
5
|
prompt: z.string().min(1).optional(),
|
|
8
6
|
enhance: z.boolean().optional().default(true),
|
|
@@ -17,61 +15,33 @@ const setPromptInputSchema = z.object({
|
|
|
17
15
|
prompt: z.string().min(1),
|
|
18
16
|
enhance: z.boolean().optional().default(true)
|
|
19
17
|
});
|
|
20
|
-
const realtimeMethods = (
|
|
21
|
-
const assertConnected = () => {
|
|
22
|
-
const state = webrtcManager.getConnectionState();
|
|
23
|
-
if (state !== "connected" && state !== "generating") throw new Error(`Cannot send message: connection is ${state}`);
|
|
24
|
-
};
|
|
18
|
+
const realtimeMethods = (session, imageToBase64) => {
|
|
25
19
|
const set = async (input) => {
|
|
26
|
-
assertConnected();
|
|
27
20
|
const parsed = setInputSchema.safeParse(input);
|
|
28
21
|
if (!parsed.success) throw parsed.error;
|
|
29
22
|
const { prompt, enhance, image } = parsed.data;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await webrtcManager.setImage(imageBase64, {
|
|
23
|
+
const imageBase64 = image !== void 0 && image !== null ? await imageToBase64(image) : null;
|
|
24
|
+
await session.setImage(imageBase64, {
|
|
33
25
|
prompt,
|
|
34
26
|
enhance,
|
|
35
|
-
timeout:
|
|
27
|
+
timeout: REALTIME_CONFIG.methods.updateTimeoutMs
|
|
36
28
|
});
|
|
37
29
|
};
|
|
38
30
|
const setPrompt = async (prompt, { enhance } = {}) => {
|
|
39
|
-
|
|
40
|
-
const parsedInput = setPromptInputSchema.safeParse({
|
|
31
|
+
const parsed = setPromptInputSchema.safeParse({
|
|
41
32
|
prompt,
|
|
42
33
|
enhance
|
|
43
34
|
});
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const ackPromise = new Promise((resolve, reject) => {
|
|
50
|
-
promptAckListener = (promptAckMessage) => {
|
|
51
|
-
if (promptAckMessage.prompt === parsedInput.data.prompt) if (promptAckMessage.success) resolve();
|
|
52
|
-
else reject(new Error(promptAckMessage.error ?? "Failed to send prompt"));
|
|
53
|
-
};
|
|
54
|
-
emitter.on("promptAck", promptAckListener);
|
|
55
|
-
});
|
|
56
|
-
if (!webrtcManager.sendMessage({
|
|
57
|
-
type: "prompt",
|
|
58
|
-
prompt: parsedInput.data.prompt,
|
|
59
|
-
enhance_prompt: parsedInput.data.enhance
|
|
60
|
-
})) throw new Error("WebSocket is not open");
|
|
61
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
62
|
-
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("Prompt timed out")), PROMPT_TIMEOUT_MS);
|
|
63
|
-
});
|
|
64
|
-
await Promise.race([ackPromise, timeoutPromise]);
|
|
65
|
-
} finally {
|
|
66
|
-
if (promptAckListener) emitter.off("promptAck", promptAckListener);
|
|
67
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
68
|
-
}
|
|
35
|
+
if (!parsed.success) throw parsed.error;
|
|
36
|
+
await session.sendPrompt(parsed.data.prompt, {
|
|
37
|
+
enhance: parsed.data.enhance,
|
|
38
|
+
timeout: REALTIME_CONFIG.methods.promptTimeoutMs
|
|
39
|
+
});
|
|
69
40
|
};
|
|
70
41
|
return {
|
|
71
42
|
set,
|
|
72
43
|
setPrompt
|
|
73
44
|
};
|
|
74
45
|
};
|
|
75
|
-
|
|
76
46
|
//#endregion
|
|
77
|
-
export { realtimeMethods };
|
|
47
|
+
export { realtimeMethods };
|
|
@@ -1,47 +1,17 @@
|
|
|
1
1
|
//#region src/realtime/observability/diagnostics.d.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type PhaseTimingEvent = {
|
|
5
|
-
phase: ConnectionPhase;
|
|
2
|
+
type ClientSessionConnectionBreakdownPhase = {
|
|
3
|
+
phase: string;
|
|
6
4
|
durationMs: number;
|
|
7
5
|
success: boolean;
|
|
8
6
|
error?: string;
|
|
9
7
|
};
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type IceStateEvent = {
|
|
18
|
-
state: string;
|
|
19
|
-
previousState: string;
|
|
20
|
-
timestampMs: number;
|
|
21
|
-
};
|
|
22
|
-
type PeerConnectionStateEvent = {
|
|
23
|
-
state: string;
|
|
24
|
-
previousState: string;
|
|
25
|
-
timestampMs: number;
|
|
26
|
-
};
|
|
27
|
-
type SignalingStateEvent = {
|
|
28
|
-
state: string;
|
|
29
|
-
previousState: string;
|
|
30
|
-
timestampMs: number;
|
|
31
|
-
};
|
|
32
|
-
type SelectedCandidatePairEvent = {
|
|
33
|
-
local: {
|
|
34
|
-
candidateType: string;
|
|
35
|
-
protocol: string;
|
|
36
|
-
address?: string;
|
|
37
|
-
port?: number;
|
|
38
|
-
};
|
|
39
|
-
remote: {
|
|
40
|
-
candidateType: string;
|
|
41
|
-
protocol: string;
|
|
42
|
-
address?: string;
|
|
43
|
-
port?: number;
|
|
44
|
-
};
|
|
8
|
+
type ClientSessionConnectionBreakdownEvent = {
|
|
9
|
+
attempt: number;
|
|
10
|
+
success: boolean;
|
|
11
|
+
totalDurationMs: number;
|
|
12
|
+
initialImageSizeKb: number | null;
|
|
13
|
+
phases: ClientSessionConnectionBreakdownPhase[];
|
|
14
|
+
error?: string;
|
|
45
15
|
};
|
|
46
16
|
type ReconnectEvent = {
|
|
47
17
|
attempt: number;
|
|
@@ -51,28 +21,19 @@ type ReconnectEvent = {
|
|
|
51
21
|
error?: string;
|
|
52
22
|
};
|
|
53
23
|
type VideoStallEvent = {
|
|
54
|
-
/** True when a stall is detected, false when recovered. */
|
|
55
24
|
stalled: boolean;
|
|
56
|
-
/** Duration of the stall in ms (0 when stall first detected, actual duration on recovery). */
|
|
57
25
|
durationMs: number;
|
|
58
26
|
};
|
|
59
|
-
/** All diagnostic event types keyed by name. */
|
|
60
27
|
type DiagnosticEvents = {
|
|
61
|
-
|
|
62
|
-
iceCandidate: IceCandidateEvent;
|
|
63
|
-
iceStateChange: IceStateEvent;
|
|
64
|
-
peerConnectionStateChange: PeerConnectionStateEvent;
|
|
65
|
-
signalingStateChange: SignalingStateEvent;
|
|
66
|
-
selectedCandidatePair: SelectedCandidatePairEvent;
|
|
28
|
+
"client-session-connection-breakdown": ClientSessionConnectionBreakdownEvent;
|
|
67
29
|
reconnect: ReconnectEvent;
|
|
68
30
|
videoStall: VideoStallEvent;
|
|
69
31
|
};
|
|
70
32
|
type DiagnosticEventName = keyof DiagnosticEvents;
|
|
71
|
-
|
|
72
|
-
type DiagnosticEvent = { [K in DiagnosticEventName]: {
|
|
33
|
+
type DiagnosticEventForName<K extends DiagnosticEventName> = {
|
|
73
34
|
name: K;
|
|
74
35
|
data: DiagnosticEvents[K];
|
|
75
|
-
}
|
|
76
|
-
|
|
36
|
+
};
|
|
37
|
+
type DiagnosticEvent = { [K in DiagnosticEventName]: DiagnosticEventForName<K> }[DiagnosticEventName];
|
|
77
38
|
//#endregion
|
|
78
|
-
export {
|
|
39
|
+
export { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent };
|
|
@@ -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 };
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
2
|
+
import { createLiveKitStatsProvider } from "./livekit-stats-provider.js";
|
|
1
3
|
import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
|
|
2
4
|
import { WebRTCStatsCollector } from "./webrtc-stats.js";
|
|
3
|
-
|
|
4
5
|
//#region src/realtime/observability/realtime-observability.ts
|
|
5
|
-
const STALL_FPS_THRESHOLD = .5;
|
|
6
6
|
var RealtimeObservability = class {
|
|
7
7
|
telemetryReporter = new NullTelemetryReporter();
|
|
8
8
|
telemetryReporterReady = false;
|
|
9
9
|
pendingTelemetryDiagnostics = [];
|
|
10
10
|
statsCollector = null;
|
|
11
11
|
statsCollectorSource = null;
|
|
12
|
+
liveKitRoom = null;
|
|
12
13
|
videoStalled = false;
|
|
13
14
|
stallStartMs = 0;
|
|
15
|
+
connectionBreakdown = null;
|
|
14
16
|
constructor(options) {
|
|
15
17
|
this.options = options;
|
|
16
18
|
}
|
|
@@ -22,6 +24,56 @@ var RealtimeObservability = class {
|
|
|
22
24
|
});
|
|
23
25
|
this.addTelemetryDiagnostic(name, data, timestamp);
|
|
24
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
|
+
}
|
|
25
77
|
sessionStarted(sessionId) {
|
|
26
78
|
if (!this.options.telemetryEnabled) return;
|
|
27
79
|
if (this.telemetryReporterReady) this.telemetryReporter.stop();
|
|
@@ -45,15 +97,27 @@ var RealtimeObservability = class {
|
|
|
45
97
|
}
|
|
46
98
|
if (source === this.statsCollectorSource) return;
|
|
47
99
|
this.stopStats();
|
|
100
|
+
this.resetStallDetection();
|
|
48
101
|
this.statsCollectorSource = source;
|
|
49
102
|
if (!this.options.telemetryEnabled && !this.options.onStats) return;
|
|
50
103
|
this.statsCollector = new WebRTCStatsCollector();
|
|
51
104
|
this.statsCollector.start(source, (stats) => this.handleStats(stats));
|
|
52
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
|
+
}
|
|
53
116
|
stopStats() {
|
|
54
117
|
this.statsCollector?.stop();
|
|
55
118
|
this.statsCollector = null;
|
|
56
119
|
this.statsCollectorSource = null;
|
|
120
|
+
this.liveKitRoom = null;
|
|
57
121
|
this.resetStallDetection();
|
|
58
122
|
}
|
|
59
123
|
stop() {
|
|
@@ -62,6 +126,7 @@ var RealtimeObservability = class {
|
|
|
62
126
|
this.telemetryReporter = new NullTelemetryReporter();
|
|
63
127
|
this.telemetryReporterReady = false;
|
|
64
128
|
this.pendingTelemetryDiagnostics.length = 0;
|
|
129
|
+
this.connectionBreakdown = null;
|
|
65
130
|
}
|
|
66
131
|
handleStats(stats) {
|
|
67
132
|
this.options.onStats?.(stats);
|
|
@@ -70,14 +135,14 @@ var RealtimeObservability = class {
|
|
|
70
135
|
}
|
|
71
136
|
detectVideoStall(stats) {
|
|
72
137
|
const fps = stats.video?.framesPerSecond ?? 0;
|
|
73
|
-
if (!this.videoStalled && stats.video && fps <
|
|
138
|
+
if (!this.videoStalled && stats.video && fps < REALTIME_CONFIG.observability.stallFpsThreshold) {
|
|
74
139
|
this.videoStalled = true;
|
|
75
140
|
this.stallStartMs = Date.now();
|
|
76
141
|
this.diagnostic("videoStall", {
|
|
77
142
|
stalled: true,
|
|
78
143
|
durationMs: 0
|
|
79
144
|
}, this.stallStartMs);
|
|
80
|
-
} else if (this.videoStalled && fps >=
|
|
145
|
+
} else if (this.videoStalled && fps >= REALTIME_CONFIG.observability.stallFpsThreshold) {
|
|
81
146
|
const durationMs = Date.now() - this.stallStartMs;
|
|
82
147
|
this.videoStalled = false;
|
|
83
148
|
this.diagnostic("videoStall", {
|
|
@@ -104,6 +169,5 @@ var RealtimeObservability = class {
|
|
|
104
169
|
this.stallStartMs = 0;
|
|
105
170
|
}
|
|
106
171
|
};
|
|
107
|
-
|
|
108
172
|
//#endregion
|
|
109
|
-
export { RealtimeObservability };
|
|
173
|
+
export { RealtimeObservability };
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { VERSION } from "../../version.js";
|
|
2
2
|
import { buildAuthHeaders } from "../../shared/request.js";
|
|
3
|
-
|
|
3
|
+
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
4
4
|
//#region src/realtime/observability/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;
|
|
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.
|
|
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
|
|
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,
|
|
83
|
-
diagnostics: this.diagnosticsBuffer.splice(0,
|
|
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(
|
|
89
|
+
fetch(REALTIME_CONFIG.observability.telemetryUrl, {
|
|
99
90
|
method: "POST",
|
|
100
91
|
headers: commonHeaders,
|
|
101
92
|
body: JSON.stringify(chunk)
|
|
102
|
-
}).
|
|
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
|
|
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 };
|
|
@@ -127,10 +127,7 @@ type WebRTCStats = {
|
|
|
127
127
|
* jitter and failure modes, so this is essential signal for
|
|
128
128
|
* benchmarking and incident triage.
|
|
129
129
|
*/
|
|
130
|
-
selectedCandidatePairs:
|
|
131
|
-
local: IceCandidateInfo;
|
|
132
|
-
remote: IceCandidateInfo;
|
|
133
|
-
}>;
|
|
130
|
+
selectedCandidatePairs: IceCandidatePair[];
|
|
134
131
|
};
|
|
135
132
|
};
|
|
136
133
|
/** One side of an ICE candidate pair (sender or receiver). */
|
|
@@ -143,5 +140,9 @@ type IceCandidateInfo = {
|
|
|
143
140
|
/** "udp" | "tcp" */
|
|
144
141
|
protocol: string;
|
|
145
142
|
};
|
|
143
|
+
type IceCandidatePair = {
|
|
144
|
+
local: IceCandidateInfo;
|
|
145
|
+
remote: IceCandidateInfo;
|
|
146
|
+
};
|
|
146
147
|
//#endregion
|
|
147
148
|
export { WebRTCStats };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
1
2
|
//#region src/realtime/observability/webrtc-stats.ts
|
|
2
|
-
const DEFAULT_INTERVAL_MS = 1e3;
|
|
3
|
-
const MIN_INTERVAL_MS = 500;
|
|
4
3
|
var WebRTCStatsCollector = class {
|
|
5
4
|
source = null;
|
|
6
5
|
intervalId = null;
|
|
@@ -17,7 +16,7 @@ var WebRTCStatsCollector = class {
|
|
|
17
16
|
onStats = null;
|
|
18
17
|
intervalMs;
|
|
19
18
|
constructor(options = {}) {
|
|
20
|
-
this.intervalMs = Math.max(options.intervalMs ??
|
|
19
|
+
this.intervalMs = Math.max(options.intervalMs ?? REALTIME_CONFIG.observability.statsDefaultIntervalMs, REALTIME_CONFIG.observability.statsMinIntervalMs);
|
|
21
20
|
}
|
|
22
21
|
/** Attach to a stats provider and start polling. */
|
|
23
22
|
start(source, onStats) {
|
|
@@ -273,6 +272,5 @@ var WebRTCStatsCollector = class {
|
|
|
273
272
|
};
|
|
274
273
|
}
|
|
275
274
|
};
|
|
276
|
-
|
|
277
275
|
//#endregion
|
|
278
|
-
export { WebRTCStatsCollector };
|
|
276
|
+
export { WebRTCStatsCollector };
|