@decartai/sdk 0.1.12 → 0.1.14
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 +85 -1
- package/dist/create-client.d.ts +46 -0
- package/dist/create-client.js +106 -0
- package/dist/index.d.ts +8 -150
- package/dist/index.js +6 -116
- package/dist/index.react-native.d.ts +36 -0
- package/dist/index.react-native.js +9 -0
- package/dist/realtime/browser/glass-to-glass-diagnostics.js +27 -0
- package/dist/realtime/{observability → browser}/glass-to-glass.js +2 -19
- package/dist/realtime/browser/index.js +32 -0
- package/dist/realtime/{mirror-stream.js → browser/mirror-stream.js} +1 -1
- package/dist/realtime/{observability → browser}/pixel-marker.js +1 -1
- package/dist/realtime/{preflight.js → browser/preflight.js} +14 -108
- package/dist/realtime/browser/prepare-connection.js +39 -0
- package/dist/realtime/client.d.ts +11 -4
- package/dist/realtime/client.js +33 -37
- package/dist/realtime/livekit.js +26 -0
- package/dist/realtime/media-channel.js +17 -10
- package/dist/realtime/methods.d.ts +1 -1
- package/dist/realtime/methods.js +3 -2
- package/dist/realtime/observability/g2g.d.ts +14 -0
- package/dist/realtime/observability/realtime-observability.js +8 -18
- package/dist/realtime/observability/webrtc-stats.d.ts +1 -1
- package/dist/realtime/preflight-connectivity.js +102 -0
- package/dist/realtime/preflight-types.d.ts +30 -0
- package/dist/realtime/react-native/index.js +36 -0
- package/dist/realtime/react-native/preflight.js +10 -0
- package/dist/realtime/react-native/prepare-connection.js +26 -0
- package/dist/realtime/stream-session.js +1 -2
- package/dist/realtime/subscribe-client.js +10 -9
- package/dist/shared/model.d.ts +36 -36
- package/dist/shared/model.js +13 -22
- package/dist/shared/request.js +5 -4
- package/dist/shared/types.d.ts +1 -1
- package/dist/shared/types.js +3 -2
- package/dist/utils/errors.d.ts +3 -0
- package/dist/utils/errors.js +16 -1
- package/dist/utils/platform.js +17 -1
- package/dist/utils/runtime.js +13 -0
- package/package.json +10 -3
- package/dist/realtime/observability/glass-to-glass.d.ts +0 -41
- package/dist/realtime/preflight.d.ts +0 -59
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
import { createFrameTransformPump } from "
|
|
1
|
+
import { createFrameTransformPump } from "./mirror-stream.js";
|
|
2
2
|
import { MAX_MARKER_HEIGHT, MIN_MARKER_WIDTH, read, stamp } from "./pixel-marker.js";
|
|
3
|
-
//#region src/realtime/
|
|
4
|
-
/**
|
|
5
|
-
* True glass-to-glass latency measurement for the realtime pipeline.
|
|
6
|
-
*
|
|
7
|
-
* Opt-in (the marker is visible in the output and pixel work has a cost). When
|
|
8
|
-
* enabled, the SDK stamps a monotonic sequence number into the bottom-left of
|
|
9
|
-
* every outgoing frame ({@link createStampPump}) and reads it back off the
|
|
10
|
-
* rendered remote frames ({@link createMarkerReader}); the server re-stamps the
|
|
11
|
-
* seq from input to matching output (its `pixel_latency` mode). The
|
|
12
|
-
* {@link SeqTracker} matches stamp time to render time to compute the real
|
|
13
|
-
* camera→display latency through the model, and infers end-to-end frame drops
|
|
14
|
-
* from seqs that are stamped but never rendered.
|
|
15
|
-
*
|
|
16
|
-
* The stamp pump is built on the shared `createFrameTransformPump` (Insertable
|
|
17
|
-
* Streams where available, canvas `captureStream` fallback). The reader is a
|
|
18
|
-
* passive offscreen-`<video>` tap so it never consumes or re-encodes the track
|
|
19
|
-
* the consumer displays.
|
|
20
|
-
*/
|
|
3
|
+
//#region src/realtime/browser/glass-to-glass.ts
|
|
21
4
|
/** Bound on in-flight seqs; a seq that ages out unmatched is an end-to-end drop. Cf. server `_MAX_PENDING`. */
|
|
22
5
|
const MAX_PENDING = 256;
|
|
23
6
|
/** Rolling window for the latency percentiles (≈10s at 30fps). */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRealTimeClient } from "../client.js";
|
|
2
|
+
import { createRealTimeSubscribeClient } from "../subscribe-client.js";
|
|
3
|
+
import { createPreflight } from "./preflight.js";
|
|
4
|
+
import { prepareBrowserConnection } from "./prepare-connection.js";
|
|
5
|
+
//#region src/realtime/browser/index.ts
|
|
6
|
+
const createBrowserRealtime = (options) => {
|
|
7
|
+
const publish = createRealTimeClient({
|
|
8
|
+
baseUrl: options.publishBaseUrl,
|
|
9
|
+
apiKey: options.apiKey,
|
|
10
|
+
integration: options.integration,
|
|
11
|
+
logger: options.logger,
|
|
12
|
+
telemetryEnabled: options.telemetryEnabled,
|
|
13
|
+
prepareConnection: prepareBrowserConnection
|
|
14
|
+
});
|
|
15
|
+
const subscribe = createRealTimeSubscribeClient({
|
|
16
|
+
baseUrl: options.subscribeBaseUrl,
|
|
17
|
+
apiKey: options.apiKey,
|
|
18
|
+
integration: options.integration,
|
|
19
|
+
logger: options.logger
|
|
20
|
+
});
|
|
21
|
+
const preflight = createPreflight({
|
|
22
|
+
logger: options.logger,
|
|
23
|
+
connect: publish.connect
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
connect: (stream, connectOptions) => publish.connect(stream, connectOptions),
|
|
27
|
+
subscribe: subscribe.subscribe,
|
|
28
|
+
checkConnectivity: preflight.checkConnectivity
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { createBrowserRealtime };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/realtime/mirror-stream.ts
|
|
1
|
+
//#region src/realtime/browser/mirror-stream.ts
|
|
2
2
|
function isMediaStreamTrackProcessorSupported() {
|
|
3
3
|
return typeof globalThis !== "undefined" && typeof globalThis.MediaStreamTrackProcessor === "function" && typeof globalThis.MediaStreamTrackGenerator === "function";
|
|
4
4
|
}
|
|
@@ -1,109 +1,8 @@
|
|
|
1
|
-
import { resolveFpsNumber } from "
|
|
2
|
-
import { REALTIME_CONFIG } from "
|
|
3
|
-
import { extractSignals, scoreLowerBetter, worst } from "
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
@@ -232,11 +131,18 @@ const ABORTED_DEEP_PROBE = {
|
|
|
232
131
|
/** Unblock `promise` early when `signal` aborts. */
|
|
233
132
|
function raceAbort(promise, signal) {
|
|
234
133
|
if (!signal) return promise;
|
|
235
|
-
if (signal.aborted) throw signal.reason ??
|
|
134
|
+
if (signal.aborted) throw signal.reason ?? createAbortError();
|
|
236
135
|
return Promise.race([promise, new Promise((_, reject) => {
|
|
237
|
-
signal.addEventListener("abort", () => reject(signal.reason ??
|
|
136
|
+
signal.addEventListener("abort", () => reject(signal.reason ?? createAbortError()), { once: true });
|
|
238
137
|
})]);
|
|
239
138
|
}
|
|
139
|
+
function createAbortError() {
|
|
140
|
+
const DOMExceptionConstructor = globalThis.DOMException;
|
|
141
|
+
if (typeof DOMExceptionConstructor === "function") return new DOMExceptionConstructor("Aborted", "AbortError");
|
|
142
|
+
const error = /* @__PURE__ */ new Error("Aborted");
|
|
143
|
+
error.name = "AbortError";
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
240
146
|
/** Resolve once enough g2g samples exist or the probe window elapses (never rejects). */
|
|
241
147
|
function waitForProbe(getLatest, minSamples, durationMs, signal) {
|
|
242
148
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { RealtimeObservability } from "../observability/realtime-observability.js";
|
|
2
|
+
import { isDesktopSafari } from "../../utils/platform.js";
|
|
3
|
+
import { createLiveKitMediaChannel } from "../media-channel.js";
|
|
4
|
+
import { createMirroredStream, shouldMirrorTrack } from "./mirror-stream.js";
|
|
5
|
+
import { createBrowserGlassToGlassDiagnostics } from "./glass-to-glass-diagnostics.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
|
+
const observability = new RealtimeObservability({
|
|
21
|
+
...observabilityOptions,
|
|
22
|
+
glassToGlass: debugQuality ? createBrowserGlassToGlassDiagnostics() : void 0
|
|
23
|
+
});
|
|
24
|
+
if (debugQuality) inputStream = observability.attachOutgoingStream(inputStream, fps);
|
|
25
|
+
const safariCodec = isDesktopSafari() ? "vp8" : void 0;
|
|
26
|
+
return {
|
|
27
|
+
stream: inputStream,
|
|
28
|
+
observability,
|
|
29
|
+
videoCodec: safariCodec ?? preferredVideoCodec,
|
|
30
|
+
queryParams: {
|
|
31
|
+
...safariCodec ? { livekit_server_codec: safariCodec } : {},
|
|
32
|
+
...debugQuality ? { pixel_latency: "1" } : {}
|
|
33
|
+
},
|
|
34
|
+
createMediaChannel: createLiveKitMediaChannel,
|
|
35
|
+
dispose: disposeMirroring
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
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.
|
|
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.
|
|
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
|
|
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 };
|
package/dist/realtime/client.js
CHANGED
|
@@ -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,11 @@ 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([
|
|
23
|
+
preferredVideoCodec: z.enum([
|
|
24
|
+
"h264",
|
|
25
|
+
"vp8",
|
|
26
|
+
"vp9"
|
|
27
|
+
]).optional(),
|
|
27
28
|
debugQuality: z.boolean().optional()
|
|
28
29
|
});
|
|
29
30
|
const createRealTimeClient = (opts) => {
|
|
@@ -34,20 +35,10 @@ const createRealTimeClient = (opts) => {
|
|
|
34
35
|
if (!parsedOptions.success) throw parsedOptions.error;
|
|
35
36
|
const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, preferredVideoCodec } = parsedOptions.data;
|
|
36
37
|
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
38
|
const debugQuality = parsedOptions.data.debugQuality ?? false;
|
|
49
39
|
let session;
|
|
50
40
|
let observability;
|
|
41
|
+
let preparedConnection;
|
|
51
42
|
try {
|
|
52
43
|
const initialImageRef = isFileRefId(initialState?.image) ? initialState.image : void 0;
|
|
53
44
|
const initialImage = initialImageRef === void 0 && initialState?.image ? await imageToBase64(initialState.image) : void 0;
|
|
@@ -57,41 +48,46 @@ const createRealTimeClient = (opts) => {
|
|
|
57
48
|
} : void 0;
|
|
58
49
|
const url = `${baseUrl}${options.model.urlPath}`;
|
|
59
50
|
const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
preparedConnection = opts.prepareConnection({
|
|
52
|
+
stream,
|
|
53
|
+
mirror,
|
|
54
|
+
debugQuality,
|
|
55
|
+
preferredVideoCodec,
|
|
56
|
+
fps: resolveFpsNumber(options.model.fps),
|
|
65
57
|
logger,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
observability: {
|
|
59
|
+
telemetryEnabled: opts.telemetryEnabled,
|
|
60
|
+
apiKey,
|
|
61
|
+
model: options.model.name,
|
|
62
|
+
integration,
|
|
63
|
+
logger,
|
|
64
|
+
onDiagnostic: (event) => emitOrBuffer("diagnostic", event),
|
|
65
|
+
onStats: (stats) => emitOrBuffer("stats", stats),
|
|
66
|
+
onConnectionQuality: (report) => {
|
|
67
|
+
emitOrBuffer("connectionQuality", report);
|
|
68
|
+
onConnectionQuality?.(report);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
73
71
|
});
|
|
74
|
-
|
|
75
|
-
const safariCodec = isDesktopSafari() ? "vp8" : void 0;
|
|
76
|
-
const publishCodec = safariCodec ?? preferredVideoCodec;
|
|
72
|
+
observability = preparedConnection.observability;
|
|
77
73
|
session = new StreamSession({
|
|
78
74
|
url: `${url}?${new URLSearchParams({
|
|
79
|
-
...
|
|
75
|
+
...preparedConnection.queryParams ?? {},
|
|
80
76
|
...options.queryParams ?? {},
|
|
81
|
-
...debugQuality ? { pixel_latency: "1" } : {},
|
|
82
77
|
api_key: apiKey,
|
|
83
78
|
model: options.model.name,
|
|
84
79
|
...resolution ? { resolution } : {}
|
|
85
80
|
}).toString()}`,
|
|
86
81
|
integration,
|
|
87
82
|
observability,
|
|
88
|
-
localStream:
|
|
83
|
+
localStream: preparedConnection.stream,
|
|
89
84
|
initialImage,
|
|
90
85
|
initialImageRef,
|
|
91
86
|
initialPrompt,
|
|
92
87
|
initialPassthrough: initialState?.passthrough,
|
|
93
88
|
logger,
|
|
94
|
-
videoCodec:
|
|
89
|
+
videoCodec: preparedConnection.videoCodec,
|
|
90
|
+
createMediaChannel: preparedConnection.createMediaChannel
|
|
95
91
|
});
|
|
96
92
|
let sessionId = null;
|
|
97
93
|
let subscribeToken = null;
|
|
@@ -126,7 +122,7 @@ const createRealTimeClient = (opts) => {
|
|
|
126
122
|
observability?.stop();
|
|
127
123
|
stop();
|
|
128
124
|
activeSession.disconnect();
|
|
129
|
-
|
|
125
|
+
preparedConnection?.dispose();
|
|
130
126
|
},
|
|
131
127
|
on: eventEmitter.on,
|
|
132
128
|
off: eventEmitter.off,
|
|
@@ -158,7 +154,7 @@ const createRealTimeClient = (opts) => {
|
|
|
158
154
|
} catch (error) {
|
|
159
155
|
observability?.stop();
|
|
160
156
|
session?.disconnect();
|
|
161
|
-
|
|
157
|
+
preparedConnection?.dispose();
|
|
162
158
|
throw error;
|
|
163
159
|
}
|
|
164
160
|
};
|
|
@@ -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 };
|
|
@@ -1,13 +1,13 @@
|
|
|
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) {
|
|
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
|
|
10
|
+
source,
|
|
11
11
|
videoCodec: resolvedCodec,
|
|
12
12
|
simulcast: resolvedCodec !== "vp9",
|
|
13
13
|
videoEncoding: {
|
|
@@ -16,8 +16,9 @@ function getDefaultVideoPublishOptions(videoCodec) {
|
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
var
|
|
19
|
+
var LiveKitMediaChannel = class {
|
|
20
20
|
room = null;
|
|
21
|
+
cameraTrackSource = null;
|
|
21
22
|
remoteStream = null;
|
|
22
23
|
events = mitt();
|
|
23
24
|
logger;
|
|
@@ -35,14 +36,16 @@ var MediaChannel = class {
|
|
|
35
36
|
this.events.off(event, handler);
|
|
36
37
|
}
|
|
37
38
|
async connect(opts) {
|
|
38
|
-
|
|
39
|
+
const { Room: LiveKitRoom, RoomEvent, Track } = await loadLiveKitClient();
|
|
40
|
+
this.cameraTrackSource = Track.Source.Camera;
|
|
41
|
+
this.room ??= new LiveKitRoom(REALTIME_CONFIG.livekit.roomOptions);
|
|
39
42
|
const room = this.room;
|
|
40
43
|
room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
|
|
41
44
|
if (!participant.identity.startsWith(REALTIME_CONFIG.livekit.inferenceServerIdentityPrefix)) return;
|
|
42
|
-
if (track.kind !==
|
|
45
|
+
if (track.kind !== "video" && track.kind !== "audio") return;
|
|
43
46
|
const mediaStreamTrack = track.mediaStreamTrack;
|
|
44
47
|
if (mediaStreamTrack) {
|
|
45
|
-
if (track.kind ===
|
|
48
|
+
if (track.kind === "video") this.config.observability?.attachRemoteVideoTrack(mediaStreamTrack);
|
|
46
49
|
const tracks = this.remoteStream?.getTracks() ?? [];
|
|
47
50
|
if (!tracks.includes(mediaStreamTrack)) tracks.push(mediaStreamTrack);
|
|
48
51
|
this.remoteStream = new MediaStream(tracks);
|
|
@@ -74,15 +77,19 @@ var MediaChannel = class {
|
|
|
74
77
|
disconnect() {
|
|
75
78
|
const room = this.room;
|
|
76
79
|
this.room = null;
|
|
80
|
+
this.cameraTrackSource = null;
|
|
77
81
|
this.remoteStream = null;
|
|
78
82
|
this.config.observability?.setLiveKitRoom(null);
|
|
79
83
|
if (room) room.disconnect().catch(() => {});
|
|
80
84
|
}
|
|
81
85
|
async publishTracks(stream) {
|
|
82
86
|
if (!this.room) return;
|
|
83
|
-
for (const track of stream.getTracks()) if (track.kind === "video")
|
|
84
|
-
|
|
87
|
+
for (const track of stream.getTracks()) if (track.kind === "video") {
|
|
88
|
+
if (!this.cameraTrackSource) throw new Error("Cannot publish video track: media channel is not connected");
|
|
89
|
+
await this.room.localParticipant.publishTrack(track, getDefaultVideoPublishOptions(this.cameraTrackSource, this.config.videoCodec));
|
|
90
|
+
} else await this.room.localParticipant.publishTrack(track);
|
|
85
91
|
}
|
|
86
92
|
};
|
|
93
|
+
const createLiveKitMediaChannel = (config) => new LiveKitMediaChannel(config);
|
|
87
94
|
//#endregion
|
|
88
|
-
export {
|
|
95
|
+
export { createLiveKitMediaChannel };
|
|
@@ -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.
|
|
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
|
package/dist/realtime/methods.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globalInstanceSchema } from "../utils/runtime.js";
|
|
1
2
|
import { isFileRefId } from "../files/types.js";
|
|
2
3
|
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -6,8 +7,8 @@ const setInputSchema = z.object({
|
|
|
6
7
|
prompt: z.string().min(1).optional(),
|
|
7
8
|
enhance: z.boolean().optional().default(true),
|
|
8
9
|
image: z.union([
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
globalInstanceSchema("Blob"),
|
|
11
|
+
globalInstanceSchema("File"),
|
|
11
12
|
z.string(),
|
|
12
13
|
z.null()
|
|
13
14
|
]).optional()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/realtime/observability/g2g.d.ts
|
|
2
|
+
type G2GMetrics = {
|
|
3
|
+
/** Time from connection start to the first rendered output frame. */
|
|
4
|
+
ttffMs: number | null;
|
|
5
|
+
/** Median steady-state camera-to-display latency. */
|
|
6
|
+
medianMs: number | null;
|
|
7
|
+
/** 90th percentile steady-state camera-to-display latency. */
|
|
8
|
+
p90Ms: number | null;
|
|
9
|
+
sampleCount: number;
|
|
10
|
+
/** Recent stamped frames that never returned, from 0 to 1. */
|
|
11
|
+
dropRatio: number | null;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { G2GMetrics };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
2
2
|
import { ConnectionQualityEvaluator } from "./connection-quality.js";
|
|
3
|
-
import { SeqTracker, createMarkerReader, createStampPump } from "./glass-to-glass.js";
|
|
4
3
|
import { createLiveKitStatsProvider } from "./livekit-stats-provider.js";
|
|
5
4
|
import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
|
|
6
5
|
import { WebRTCStatsCollector } from "./webrtc-stats.js";
|
|
@@ -16,14 +15,10 @@ var RealtimeObservability = class {
|
|
|
16
15
|
stallStartMs = 0;
|
|
17
16
|
connectionBreakdown = null;
|
|
18
17
|
connectionQuality = new ConnectionQualityEvaluator();
|
|
19
|
-
|
|
20
|
-
seqTracker;
|
|
21
|
-
markerReader;
|
|
22
|
-
stampPump = null;
|
|
18
|
+
glassToGlass;
|
|
23
19
|
constructor(options) {
|
|
24
20
|
this.options = options;
|
|
25
|
-
this.
|
|
26
|
-
this.markerReader = this.seqTracker ? createMarkerReader(this.seqTracker) : null;
|
|
21
|
+
this.glassToGlass = options.glassToGlass;
|
|
27
22
|
}
|
|
28
23
|
/**
|
|
29
24
|
* Wrap the outgoing stream so each frame carries a marker, feeding the shared
|
|
@@ -32,13 +27,9 @@ var RealtimeObservability = class {
|
|
|
32
27
|
* writer and reader of the tracker share one lifecycle and survive reconnects.
|
|
33
28
|
*/
|
|
34
29
|
attachOutgoingStream(stream, fps) {
|
|
35
|
-
if (!this.
|
|
30
|
+
if (!this.glassToGlass) return stream;
|
|
36
31
|
try {
|
|
37
|
-
this.
|
|
38
|
-
tracker: this.seqTracker,
|
|
39
|
-
fps
|
|
40
|
-
});
|
|
41
|
-
return this.stampPump.stream;
|
|
32
|
+
return this.glassToGlass.attachOutgoingStream(stream, fps);
|
|
42
33
|
} catch (error) {
|
|
43
34
|
this.options.logger.warn("Failed to start glass-to-glass stamp pump; continuing without it", { error: error instanceof Error ? error.message : String(error) });
|
|
44
35
|
return stream;
|
|
@@ -46,7 +37,7 @@ var RealtimeObservability = class {
|
|
|
46
37
|
}
|
|
47
38
|
/** Feed the remote video track to the marker reader (called from the media channel). */
|
|
48
39
|
attachRemoteVideoTrack(track) {
|
|
49
|
-
this.
|
|
40
|
+
this.glassToGlass?.attachRemoteVideoTrack(track);
|
|
50
41
|
}
|
|
51
42
|
/**
|
|
52
43
|
* Mark the start of a connect attempt for glass-to-glass timing (resets the
|
|
@@ -55,7 +46,7 @@ var RealtimeObservability = class {
|
|
|
55
46
|
* measurement is on.
|
|
56
47
|
*/
|
|
57
48
|
markGlassToGlassStart() {
|
|
58
|
-
this.
|
|
49
|
+
this.glassToGlass?.markStart();
|
|
59
50
|
}
|
|
60
51
|
diagnostic(name, data, timestamp = Date.now()) {
|
|
61
52
|
this.options.logger.debug(name, data);
|
|
@@ -163,8 +154,7 @@ var RealtimeObservability = class {
|
|
|
163
154
|
}
|
|
164
155
|
stop() {
|
|
165
156
|
this.stopStats();
|
|
166
|
-
this.
|
|
167
|
-
this.markerReader?.dispose();
|
|
157
|
+
this.glassToGlass?.dispose();
|
|
168
158
|
this.telemetryReporter.stop();
|
|
169
159
|
this.telemetryReporter = new NullTelemetryReporter();
|
|
170
160
|
this.telemetryReporterReady = false;
|
|
@@ -175,7 +165,7 @@ var RealtimeObservability = class {
|
|
|
175
165
|
return this.connectionQuality.current();
|
|
176
166
|
}
|
|
177
167
|
handleStats(stats) {
|
|
178
|
-
if (this.
|
|
168
|
+
if (this.glassToGlass) stats.glassToGlass = this.glassToGlass.snapshot();
|
|
179
169
|
this.options.onStats?.(stats);
|
|
180
170
|
this.telemetryReporter.addStats(stats);
|
|
181
171
|
this.detectVideoStall(stats);
|