@decartai/sdk 0.1.13 → 0.1.15
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 +109 -10
- package/dist/create-client.d.ts +46 -0
- package/dist/create-client.js +208 -0
- package/dist/files/types.js +4 -1
- 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/package.js +4 -0
- package/dist/process/types.d.ts +2 -2
- package/dist/realtime/browser/frame-metadata-diagnostics.js +90 -0
- package/dist/realtime/browser/frame-metadata-worker.js +2 -0
- package/dist/realtime/browser/index.js +32 -0
- package/dist/realtime/{mirror-stream.js → browser/mirror-stream.js} +2 -2
- package/dist/realtime/{preflight.js → browser/preflight.js} +20 -115
- package/dist/realtime/browser/prepare-connection.js +57 -0
- package/dist/realtime/client.d.ts +11 -4
- package/dist/realtime/client.js +42 -37
- package/dist/realtime/config-realtime.js +51 -0
- package/dist/realtime/livekit.js +26 -0
- package/dist/realtime/media-channel.js +40 -11
- package/dist/realtime/methods.d.ts +1 -1
- package/dist/realtime/methods.js +7 -2
- package/dist/realtime/observability/connection-quality.d.ts +3 -3
- package/dist/realtime/observability/connection-quality.js +3 -1
- package/dist/realtime/observability/g2g.d.ts +14 -0
- package/dist/realtime/observability/realtime-observability.js +8 -32
- package/dist/realtime/observability/webrtc-stats.d.ts +4 -4
- 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/signaling-channel.js +3 -1
- package/dist/realtime/stream-session.js +5 -4
- package/dist/realtime/subscribe-client.js +11 -10
- package/dist/shared/model.d.ts +56 -37
- package/dist/shared/model.js +61 -18
- package/dist/shared/request.js +5 -4
- package/dist/shared/types.d.ts +2 -2
- package/dist/shared/types.js +9 -2
- package/dist/utils/errors.d.ts +3 -0
- package/dist/utils/errors.js +16 -1
- package/dist/utils/media.js +1 -1
- package/dist/utils/platform.js +17 -1
- package/dist/utils/runtime.js +13 -0
- package/dist/utils/user-agent.js +1 -1
- package/dist/version.js +9 -4
- package/package.json +10 -3
- package/dist/realtime/observability/glass-to-glass.d.ts +0 -41
- package/dist/realtime/observability/glass-to-glass.js +0 -229
- package/dist/realtime/observability/pixel-marker.js +0 -144
- package/dist/realtime/preflight.d.ts +0 -59
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";
|
|
@@ -5,9 +6,13 @@ import { z } from "zod";
|
|
|
5
6
|
const setInputSchema = z.object({
|
|
6
7
|
prompt: z.string().min(1).optional(),
|
|
7
8
|
enhance: z.boolean().optional().default(true),
|
|
9
|
+
/**
|
|
10
|
+
* - `Blob`/`File`/data:/http(s):/base64 string: bytes traverse the wire as base64.
|
|
11
|
+
* - `"file_..."` id (from `client.files.upload(...).id`): sent as a server-side reference.
|
|
12
|
+
*/
|
|
8
13
|
image: z.union([
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
globalInstanceSchema("Blob"),
|
|
15
|
+
globalInstanceSchema("File"),
|
|
11
16
|
z.string(),
|
|
12
17
|
z.null()
|
|
13
18
|
]).optional()
|
|
@@ -17,7 +17,7 @@ type ConnectionQualityMetrics = {
|
|
|
17
17
|
/**
|
|
18
18
|
* Mid-stream (steady-state) glass-to-glass latency (ms) — the real per-frame
|
|
19
19
|
* camera→display latency through the model, excluding startup. Only populated
|
|
20
|
-
* when the opt-in
|
|
20
|
+
* when the opt-in frame-metadata measurement is on (`connect({ debugQuality: true })`)
|
|
21
21
|
* and past warm-up; null otherwise. When present it drives the latency verdict
|
|
22
22
|
* instead of `rttMs`.
|
|
23
23
|
*/
|
|
@@ -36,8 +36,8 @@ type ConnectionQualityMetrics = {
|
|
|
36
36
|
/** Server's view of upstream (client→server) jitter in ms, or null. Observational. */
|
|
37
37
|
upstreamJitterMs: number | null;
|
|
38
38
|
/**
|
|
39
|
-
* End-to-end frame drop ratio (0–1)
|
|
40
|
-
*
|
|
39
|
+
* End-to-end frame drop ratio (0–1). Null until frame identity is propagated
|
|
40
|
+
* end-to-end; latency measurement itself only requires the capture timestamp.
|
|
41
41
|
*/
|
|
42
42
|
g2gDropRatio: number | null;
|
|
43
43
|
/** Estimated available upstream bandwidth in kbps. Chromium-only — null on Safari/Firefox. */
|
|
@@ -76,6 +76,7 @@ function minOrNull(values) {
|
|
|
76
76
|
return values.length === 0 ? null : Math.min(...values);
|
|
77
77
|
}
|
|
78
78
|
var RingBuffer = class {
|
|
79
|
+
size;
|
|
79
80
|
values = [];
|
|
80
81
|
constructor(size) {
|
|
81
82
|
this.size = size;
|
|
@@ -101,6 +102,7 @@ var RingBuffer = class {
|
|
|
101
102
|
* warm-up state changes; `current()` returns the latest at any time.
|
|
102
103
|
*/
|
|
103
104
|
var ConnectionQualityEvaluator = class {
|
|
105
|
+
thresholds;
|
|
104
106
|
rtt;
|
|
105
107
|
glassToGlass;
|
|
106
108
|
loss;
|
|
@@ -216,4 +218,4 @@ var ConnectionQualityEvaluator = class {
|
|
|
216
218
|
}
|
|
217
219
|
};
|
|
218
220
|
//#endregion
|
|
219
|
-
export { ConnectionQualityEvaluator, extractSignals, scoreLowerBetter, worst };
|
|
221
|
+
export { ConnectionQualityEvaluator, RANK, extractSignals, scoreLowerBetter, scoreMetrics, worst };
|
|
@@ -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,11 +1,11 @@
|
|
|
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";
|
|
7
6
|
//#region src/realtime/observability/realtime-observability.ts
|
|
8
7
|
var RealtimeObservability = class {
|
|
8
|
+
options;
|
|
9
9
|
telemetryReporter = new NullTelemetryReporter();
|
|
10
10
|
telemetryReporterReady = false;
|
|
11
11
|
pendingTelemetryDiagnostics = [];
|
|
@@ -16,37 +16,14 @@ var RealtimeObservability = class {
|
|
|
16
16
|
stallStartMs = 0;
|
|
17
17
|
connectionBreakdown = null;
|
|
18
18
|
connectionQuality = new ConnectionQualityEvaluator();
|
|
19
|
-
|
|
20
|
-
seqTracker;
|
|
21
|
-
markerReader;
|
|
22
|
-
stampPump = null;
|
|
19
|
+
glassToGlass;
|
|
23
20
|
constructor(options) {
|
|
24
21
|
this.options = options;
|
|
25
|
-
this.
|
|
26
|
-
this.markerReader = this.seqTracker ? createMarkerReader(this.seqTracker) : null;
|
|
22
|
+
this.glassToGlass = options.glassToGlass;
|
|
27
23
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Wrap the outgoing stream so each frame carries a marker, feeding the shared
|
|
30
|
-
* tracker that the reader matches against. Returns the stream to publish
|
|
31
|
-
* (unchanged when g2g is off or stamping can't start). Owned here so the
|
|
32
|
-
* writer and reader of the tracker share one lifecycle and survive reconnects.
|
|
33
|
-
*/
|
|
34
|
-
attachOutgoingStream(stream, fps) {
|
|
35
|
-
if (!this.seqTracker) return stream;
|
|
36
|
-
try {
|
|
37
|
-
this.stampPump = createStampPump(stream, {
|
|
38
|
-
tracker: this.seqTracker,
|
|
39
|
-
fps
|
|
40
|
-
});
|
|
41
|
-
return this.stampPump.stream;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
this.options.logger.warn("Failed to start glass-to-glass stamp pump; continuing without it", { error: error instanceof Error ? error.message : String(error) });
|
|
44
|
-
return stream;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** Feed the remote video track to the marker reader (called from the media channel). */
|
|
24
|
+
/** Feed the LiveKit remote video track to the frame-metadata reader. */
|
|
48
25
|
attachRemoteVideoTrack(track) {
|
|
49
|
-
this.
|
|
26
|
+
this.glassToGlass?.attachRemoteVideoTrack(track);
|
|
50
27
|
}
|
|
51
28
|
/**
|
|
52
29
|
* Mark the start of a connect attempt for glass-to-glass timing (resets the
|
|
@@ -55,7 +32,7 @@ var RealtimeObservability = class {
|
|
|
55
32
|
* measurement is on.
|
|
56
33
|
*/
|
|
57
34
|
markGlassToGlassStart() {
|
|
58
|
-
this.
|
|
35
|
+
this.glassToGlass?.markStart();
|
|
59
36
|
}
|
|
60
37
|
diagnostic(name, data, timestamp = Date.now()) {
|
|
61
38
|
this.options.logger.debug(name, data);
|
|
@@ -163,8 +140,7 @@ var RealtimeObservability = class {
|
|
|
163
140
|
}
|
|
164
141
|
stop() {
|
|
165
142
|
this.stopStats();
|
|
166
|
-
this.
|
|
167
|
-
this.markerReader?.dispose();
|
|
143
|
+
this.glassToGlass?.dispose();
|
|
168
144
|
this.telemetryReporter.stop();
|
|
169
145
|
this.telemetryReporter = new NullTelemetryReporter();
|
|
170
146
|
this.telemetryReporterReady = false;
|
|
@@ -175,7 +151,7 @@ var RealtimeObservability = class {
|
|
|
175
151
|
return this.connectionQuality.current();
|
|
176
152
|
}
|
|
177
153
|
handleStats(stats) {
|
|
178
|
-
if (this.
|
|
154
|
+
if (this.glassToGlass) stats.glassToGlass = this.glassToGlass.snapshot();
|
|
179
155
|
this.options.onStats?.(stats);
|
|
180
156
|
this.telemetryReporter.addStats(stats);
|
|
181
157
|
this.detectVideoStall(stats);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G2GMetrics } from "./
|
|
1
|
+
import { G2GMetrics } from "./g2g.js";
|
|
2
2
|
|
|
3
3
|
//#region src/realtime/observability/webrtc-stats.d.ts
|
|
4
4
|
type WebRTCStats = {
|
|
@@ -133,8 +133,8 @@ type WebRTCStats = {
|
|
|
133
133
|
};
|
|
134
134
|
/**
|
|
135
135
|
* True glass-to-glass latency + end-to-end drop signal, merged in by
|
|
136
|
-
* `RealtimeObservability` when the opt-in
|
|
137
|
-
*
|
|
136
|
+
* `RealtimeObservability` when the opt-in frame-metadata measurement is active.
|
|
137
|
+
* Null otherwise — the stats collector does not
|
|
138
138
|
* populate it.
|
|
139
139
|
*/
|
|
140
140
|
glassToGlass: G2GMetrics | null;
|
|
@@ -154,4 +154,4 @@ type IceCandidatePair = {
|
|
|
154
154
|
remote: IceCandidateInfo;
|
|
155
155
|
};
|
|
156
156
|
//#endregion
|
|
157
|
-
export { WebRTCStats };
|
|
157
|
+
export { IceCandidateInfo, IceCandidatePair, WebRTCStats };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/realtime/preflight-connectivity.ts
|
|
2
|
+
function candidateType(candidate) {
|
|
3
|
+
if (candidate.type) return candidate.type;
|
|
4
|
+
return /\btyp (\w+)/.exec(candidate.candidate)?.[1] ?? "";
|
|
5
|
+
}
|
|
6
|
+
function nowMs() {
|
|
7
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
8
|
+
}
|
|
9
|
+
async function gatherIceCandidates(iceServers, timeoutMs, signal, logger) {
|
|
10
|
+
if (signal?.aborted) return {
|
|
11
|
+
transport: "failed",
|
|
12
|
+
rttMs: null
|
|
13
|
+
};
|
|
14
|
+
const PC = globalThis?.RTCPeerConnection;
|
|
15
|
+
if (typeof PC !== "function") {
|
|
16
|
+
logger.warn("preflight: RTCPeerConnection unavailable in this environment");
|
|
17
|
+
return {
|
|
18
|
+
transport: "failed",
|
|
19
|
+
rttMs: null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
let pc = null;
|
|
23
|
+
try {
|
|
24
|
+
pc = new PC({ iceServers });
|
|
25
|
+
pc.createDataChannel("decart-preflight");
|
|
26
|
+
let sawSrflx = false;
|
|
27
|
+
let sawOtherCandidate = false;
|
|
28
|
+
let firstSrflxAt = null;
|
|
29
|
+
const start = nowMs();
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
let settled = false;
|
|
32
|
+
let timer;
|
|
33
|
+
const finish = () => {
|
|
34
|
+
if (settled) return;
|
|
35
|
+
settled = true;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
signal?.removeEventListener("abort", finish);
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
timer = setTimeout(finish, timeoutMs);
|
|
41
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
42
|
+
if (signal?.aborted) finish();
|
|
43
|
+
const peer = pc;
|
|
44
|
+
peer.onicecandidate = (event) => {
|
|
45
|
+
if (!event.candidate || event.candidate.candidate === "") return finish();
|
|
46
|
+
if (candidateType(event.candidate) === "srflx") {
|
|
47
|
+
sawSrflx = true;
|
|
48
|
+
if (firstSrflxAt === null) firstSrflxAt = nowMs();
|
|
49
|
+
} else sawOtherCandidate = true;
|
|
50
|
+
};
|
|
51
|
+
peer.onicegatheringstatechange = () => {
|
|
52
|
+
if (peer.iceGatheringState === "complete") finish();
|
|
53
|
+
};
|
|
54
|
+
peer.createOffer().then((offer) => peer.setLocalDescription(offer)).catch((error) => {
|
|
55
|
+
logger.warn("preflight: failed to create offer", { error: error instanceof Error ? error.message : String(error) });
|
|
56
|
+
finish();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const rttMs = firstSrflxAt !== null ? Math.round(firstSrflxAt - start) : null;
|
|
60
|
+
return {
|
|
61
|
+
transport: sawSrflx ? "udp" : sawOtherCandidate ? "relay" : "failed",
|
|
62
|
+
rttMs
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.warn("preflight: connectivity probe threw", { error: error instanceof Error ? error.message : String(error) });
|
|
66
|
+
return {
|
|
67
|
+
transport: "failed",
|
|
68
|
+
rttMs: null
|
|
69
|
+
};
|
|
70
|
+
} finally {
|
|
71
|
+
try {
|
|
72
|
+
pc?.close();
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function classifyConnectivity(metrics, thresholds) {
|
|
77
|
+
const reasons = [];
|
|
78
|
+
let quality;
|
|
79
|
+
if (metrics.transport === "failed") {
|
|
80
|
+
quality = "critical";
|
|
81
|
+
reasons.push("Could not establish any WebRTC connectivity (no ICE candidates gathered). Real-time streaming is unlikely to work on this network.");
|
|
82
|
+
} else if (metrics.transport === "relay") {
|
|
83
|
+
quality = "poor";
|
|
84
|
+
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.");
|
|
85
|
+
} else if (metrics.rttMs != null && metrics.rttMs > thresholds.marginalMs) {
|
|
86
|
+
quality = "poor";
|
|
87
|
+
reasons.push(`Network round-trip time is high (~${metrics.rttMs}ms > ${thresholds.marginalMs}ms); the real-time experience may feel laggy.`);
|
|
88
|
+
} else if (metrics.rttMs != null && metrics.rttMs > thresholds.goodMs) {
|
|
89
|
+
quality = "fair";
|
|
90
|
+
reasons.push(`Network round-trip time is elevated (~${metrics.rttMs}ms > ${thresholds.goodMs}ms).`);
|
|
91
|
+
} else quality = "good";
|
|
92
|
+
return {
|
|
93
|
+
quality,
|
|
94
|
+
metrics: {
|
|
95
|
+
transport: metrics.transport,
|
|
96
|
+
rttMs: metrics.rttMs
|
|
97
|
+
},
|
|
98
|
+
reasons
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { classifyConnectivity, gatherIceCandidates };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CustomModelDefinition, ModelDefinition } from "../shared/model.js";
|
|
2
|
+
import { ConnectionQuality } from "./observability/connection-quality.js";
|
|
3
|
+
|
|
4
|
+
//#region src/realtime/preflight-types.d.ts
|
|
5
|
+
type ConnectivityTransport = "udp" | "relay" | "failed";
|
|
6
|
+
type ConnectivityMetrics = {
|
|
7
|
+
transport: ConnectivityTransport;
|
|
8
|
+
rttMs: number | null;
|
|
9
|
+
g2gMs?: number | null;
|
|
10
|
+
ttffMs?: number | null;
|
|
11
|
+
g2gDropRatio?: number | null;
|
|
12
|
+
upstreamJitterMs?: number | null;
|
|
13
|
+
packetLoss?: number | null;
|
|
14
|
+
sampleCount?: number;
|
|
15
|
+
};
|
|
16
|
+
type ConnectivityReport = {
|
|
17
|
+
quality: ConnectionQuality;
|
|
18
|
+
metrics: ConnectivityMetrics;
|
|
19
|
+
reasons: string[];
|
|
20
|
+
};
|
|
21
|
+
type CheckConnectivityOptions = {
|
|
22
|
+
iceServers?: RTCIceServer[];
|
|
23
|
+
iceGatherTimeoutMs?: number;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
deep?: boolean;
|
|
26
|
+
model?: ModelDefinition | CustomModelDefinition;
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { CheckConnectivityOptions, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRealTimeClient } from "../client.js";
|
|
2
|
+
import { createRealTimeSubscribeClient } from "../subscribe-client.js";
|
|
3
|
+
import { createReactNativePreflight } from "./preflight.js";
|
|
4
|
+
import { assertReactNativeReady, prepareReactNativeConnection, unsupportedReactNativeFeature } from "./prepare-connection.js";
|
|
5
|
+
//#region src/realtime/react-native/index.ts
|
|
6
|
+
const createReactNativeRealtime = (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: prepareReactNativeConnection
|
|
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 = createReactNativePreflight({ logger: options.logger });
|
|
22
|
+
return {
|
|
23
|
+
connect: (stream, connectOptions) => publish.connect(stream, connectOptions),
|
|
24
|
+
subscribe: (subscribeOptions) => {
|
|
25
|
+
assertReactNativeReady();
|
|
26
|
+
return subscribe.subscribe(subscribeOptions);
|
|
27
|
+
},
|
|
28
|
+
checkConnectivity: async (checkOptions = {}) => {
|
|
29
|
+
assertReactNativeReady();
|
|
30
|
+
if (checkOptions.deep) unsupportedReactNativeFeature("Deep connectivity preflight");
|
|
31
|
+
return preflight.checkConnectivity(checkOptions);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { createReactNativeRealtime };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { REALTIME_CONFIG } from "../config-realtime.js";
|
|
2
|
+
import { classifyConnectivity, gatherIceCandidates } from "../preflight-connectivity.js";
|
|
3
|
+
//#region src/realtime/react-native/preflight.ts
|
|
4
|
+
function createReactNativePreflight({ logger }) {
|
|
5
|
+
return { checkConnectivity: async (options = {}) => {
|
|
6
|
+
return classifyConnectivity(await gatherIceCandidates(options.iceServers ?? REALTIME_CONFIG.preflight.defaultStunUrls.map((urls) => ({ urls })), options.iceGatherTimeoutMs ?? REALTIME_CONFIG.preflight.iceGatherTimeoutMs, options.signal, logger), REALTIME_CONFIG.preflight.rtt);
|
|
7
|
+
} };
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createReactNativePreflight };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createReactNativeSetupRequiredError, createUnsupportedPlatformFeatureError } from "../../utils/errors.js";
|
|
2
|
+
import { RealtimeObservability } from "../observability/realtime-observability.js";
|
|
3
|
+
import { missingReactNativeRealtimeGlobals } from "../../utils/platform.js";
|
|
4
|
+
import { createLiveKitMediaChannel } from "../media-channel.js";
|
|
5
|
+
//#region src/realtime/react-native/prepare-connection.ts
|
|
6
|
+
function assertReactNativeReady() {
|
|
7
|
+
const missing = missingReactNativeRealtimeGlobals();
|
|
8
|
+
if (missing.length > 0) throw createReactNativeSetupRequiredError(missing);
|
|
9
|
+
}
|
|
10
|
+
function unsupportedReactNativeFeature(feature) {
|
|
11
|
+
throw createUnsupportedPlatformFeatureError(feature, "React Native");
|
|
12
|
+
}
|
|
13
|
+
const prepareReactNativeConnection = ({ stream, mirror, debugQuality, preferredVideoCodec, observability: observabilityOptions }) => {
|
|
14
|
+
assertReactNativeReady();
|
|
15
|
+
if (mirror !== false) unsupportedReactNativeFeature("Outgoing video mirroring");
|
|
16
|
+
if (debugQuality) unsupportedReactNativeFeature("debugQuality");
|
|
17
|
+
return {
|
|
18
|
+
stream: stream ?? new MediaStream(),
|
|
19
|
+
observability: new RealtimeObservability(observabilityOptions),
|
|
20
|
+
videoCodec: preferredVideoCodec,
|
|
21
|
+
createMediaChannel: createLiveKitMediaChannel,
|
|
22
|
+
dispose: () => {}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { assertReactNativeReady, prepareReactNativeConnection, unsupportedReactNativeFeature };
|
|
@@ -36,6 +36,7 @@ function buildInitialStateRequest(initialState) {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
var SignalingChannel = class {
|
|
39
|
+
config;
|
|
39
40
|
ws = null;
|
|
40
41
|
events = mitt();
|
|
41
42
|
pendingAcks = [];
|
|
@@ -66,7 +67,8 @@ var SignalingChannel = class {
|
|
|
66
67
|
const userSetInitialState = opts.initialState != null && (opts.initialState.image != null || opts.initialState.imageRef != null || opts.initialState.prompt != null);
|
|
67
68
|
const joinMessage = {
|
|
68
69
|
type: "livekit_join",
|
|
69
|
-
passthrough: opts.passthrough ?? !userSetInitialState
|
|
70
|
+
passthrough: opts.passthrough ?? !userSetInitialState,
|
|
71
|
+
...opts.frameTiming ? { client_capabilities: { frame_timing: true } } : {}
|
|
70
72
|
};
|
|
71
73
|
if (!this.writeMessage(joinMessage)) {
|
|
72
74
|
roomInfoWait.cancel();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createConsoleLogger } from "../utils/logger.js";
|
|
2
2
|
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
3
|
-
import { MediaChannel } from "./media-channel.js";
|
|
4
3
|
import { SignalingChannel } from "./signaling-channel.js";
|
|
5
4
|
import mitt from "mitt";
|
|
6
5
|
import pRetry, { AbortError } from "p-retry";
|
|
@@ -17,6 +16,7 @@ function getInitialImageSizeKb(image) {
|
|
|
17
16
|
return Math.max(0, Math.round(bytes / 1024));
|
|
18
17
|
}
|
|
19
18
|
var StreamSession = class {
|
|
19
|
+
config;
|
|
20
20
|
signaling;
|
|
21
21
|
media;
|
|
22
22
|
events = mitt();
|
|
@@ -110,7 +110,8 @@ var StreamSession = class {
|
|
|
110
110
|
const { roomInfo, initialStateAck } = await this.signaling.openAndJoin({
|
|
111
111
|
connectTimeout: REALTIME_CONFIG.session.connectionTimeoutMs,
|
|
112
112
|
initialState,
|
|
113
|
-
passthrough: this.config.initialPassthrough
|
|
113
|
+
passthrough: this.config.initialPassthrough,
|
|
114
|
+
frameTiming: this.config.frameTiming
|
|
114
115
|
});
|
|
115
116
|
this.watchInitialStateAck(initialStateAck, attempt);
|
|
116
117
|
if (this.disposed || this.currentAttempt !== attempt) {
|
|
@@ -238,7 +239,7 @@ var StreamSession = class {
|
|
|
238
239
|
logger: this.logger,
|
|
239
240
|
observability: this.config.observability
|
|
240
241
|
});
|
|
241
|
-
this.media =
|
|
242
|
+
this.media = this.config.createMediaChannel({
|
|
242
243
|
observability: this.config.observability,
|
|
243
244
|
localStream: this.config.localStream,
|
|
244
245
|
logger: this.logger,
|
|
@@ -267,4 +268,4 @@ var StreamSession = class {
|
|
|
267
268
|
}
|
|
268
269
|
};
|
|
269
270
|
//#endregion
|
|
270
|
-
export { StreamSession };
|
|
271
|
+
export { StreamSession, encodeSubscribeToken };
|
|
@@ -2,8 +2,8 @@ import { classifyWebrtcError } from "../utils/errors.js";
|
|
|
2
2
|
import { createConsoleLogger } from "../utils/logger.js";
|
|
3
3
|
import { createEventBuffer } from "./event-buffer.js";
|
|
4
4
|
import { REALTIME_CONFIG } from "./config-realtime.js";
|
|
5
|
+
import { loadLiveKitClient } from "./livekit.js";
|
|
5
6
|
import { RealtimeObservability } from "./observability/realtime-observability.js";
|
|
6
|
-
import { ConnectionState, Room, RoomEvent, Track } from "livekit-client";
|
|
7
7
|
//#region src/realtime/subscribe-client.ts
|
|
8
8
|
function decodeSubscribeToken(token) {
|
|
9
9
|
try {
|
|
@@ -16,11 +16,11 @@ function decodeSubscribeToken(token) {
|
|
|
16
16
|
}
|
|
17
17
|
function mapLiveKitState(state) {
|
|
18
18
|
switch (state) {
|
|
19
|
-
case
|
|
20
|
-
case
|
|
21
|
-
case
|
|
22
|
-
case
|
|
23
|
-
case
|
|
19
|
+
case "connecting": return "connecting";
|
|
20
|
+
case "connected": return "connected";
|
|
21
|
+
case "reconnecting":
|
|
22
|
+
case "signalReconnecting": return "reconnecting";
|
|
23
|
+
case "disconnected": return "disconnected";
|
|
24
24
|
default: return "disconnected";
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -63,6 +63,7 @@ const createRealTimeSubscribeClient = (opts) => {
|
|
|
63
63
|
emitOrBuffer("connectionChange", state);
|
|
64
64
|
};
|
|
65
65
|
try {
|
|
66
|
+
const { Room: LiveKitRoom, RoomEvent } = await loadLiveKitClient();
|
|
66
67
|
observability = new RealtimeObservability({
|
|
67
68
|
telemetryEnabled: false,
|
|
68
69
|
apiKey,
|
|
@@ -76,11 +77,11 @@ const createRealTimeSubscribeClient = (opts) => {
|
|
|
76
77
|
apiKey,
|
|
77
78
|
roomName
|
|
78
79
|
});
|
|
79
|
-
room = new
|
|
80
|
+
room = new LiveKitRoom(REALTIME_CONFIG.livekit.roomOptions);
|
|
80
81
|
const activeRoom = room;
|
|
81
82
|
activeRoom.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
|
|
82
83
|
if (!participant.identity.startsWith(REALTIME_CONFIG.livekit.inferenceServerIdentityPrefix)) return;
|
|
83
|
-
if (track.kind !==
|
|
84
|
+
if (track.kind !== "video" && track.kind !== "audio") return;
|
|
84
85
|
const mediaStreamTrack = track.mediaStreamTrack;
|
|
85
86
|
if (!mediaStreamTrack) return;
|
|
86
87
|
const tracks = remoteStream?.getTracks() ?? [];
|
|
@@ -98,7 +99,7 @@ const createRealTimeSubscribeClient = (opts) => {
|
|
|
98
99
|
observability.setLiveKitRoom(activeRoom);
|
|
99
100
|
setState("connected");
|
|
100
101
|
const client = {
|
|
101
|
-
isConnected: () => activeRoom.state ===
|
|
102
|
+
isConnected: () => activeRoom.state === "connected",
|
|
102
103
|
getConnectionState: () => mapLiveKitState(activeRoom.state),
|
|
103
104
|
disconnect: () => {
|
|
104
105
|
observability?.stop();
|
|
@@ -121,4 +122,4 @@ const createRealTimeSubscribeClient = (opts) => {
|
|
|
121
122
|
return { subscribe };
|
|
122
123
|
};
|
|
123
124
|
//#endregion
|
|
124
|
-
export { createRealTimeSubscribeClient };
|
|
125
|
+
export { createRealTimeSubscribeClient, decodeSubscribeToken };
|