@relu-interactives/spatial-ecs 0.8.0 → 0.10.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/dist/{System-8D-XBDKZ.d.cts → System-BldSHxZV.d.cts} +214 -81
- package/dist/{System-DPMAW30g.d.ts → System-CADhPyQs.d.ts} +214 -81
- package/dist/{XRHandPose-G0Dl-F0w.d.ts → XRHandPose-BNSwd1y9.d.ts} +145 -2
- package/dist/{XRHandPose-VQMT1WXj.d.cts → XRHandPose-D3Ab1Dfs.d.cts} +145 -2
- package/dist/{chunk-G4XJ53LS.js → chunk-JEPUCLZX.js} +3 -3
- package/dist/{chunk-G4XJ53LS.js.map → chunk-JEPUCLZX.js.map} +1 -1
- package/dist/{chunk-NNDUOQ7O.js → chunk-JNYPJB55.js} +250 -3
- package/dist/chunk-JNYPJB55.js.map +1 -0
- package/dist/{chunk-SGF73YTF.js → chunk-NXASMBDS.js} +4 -4
- package/dist/{chunk-SGF73YTF.js.map → chunk-NXASMBDS.js.map} +1 -1
- package/dist/{chunk-JIJQFW6P.js → chunk-TY3XCHOR.js} +1390 -485
- package/dist/chunk-TY3XCHOR.js.map +1 -0
- package/dist/{chunk-U727NTWQ.js → chunk-WWP46AEF.js} +26 -4
- package/dist/chunk-WWP46AEF.js.map +1 -0
- package/dist/components/index.cjs +151 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +3 -3
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.js +1 -1
- package/dist/docs/guide/bone-attachments-and-ik.md +1 -1
- package/dist/docs/guide/multiplayer.md +65 -0
- package/dist/docs/guide/spatial-ui.md +83 -7
- package/dist/factories/index.cjs +151 -0
- package/dist/factories/index.cjs.map +1 -1
- package/dist/factories/index.d.cts +2 -2
- package/dist/factories/index.d.ts +2 -2
- package/dist/factories/index.js +3 -3
- package/dist/{index-C61n65PE.d.ts → index-6pZO_gV7.d.ts} +27 -2
- package/dist/{index-D9DjrUn3.d.cts → index-BXjwEdNq.d.cts} +6 -3
- package/dist/{index-C70N4BYG.d.cts → index-BdghvXKE.d.cts} +27 -2
- package/dist/{index-DppafVu4.d.ts → index-CVG-hREM.d.ts} +6 -3
- package/dist/index.cjs +1800 -626
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -30
- package/dist/index.d.ts +8 -30
- package/dist/index.js +10 -10
- package/dist/network/index.cjs +858 -8
- package/dist/network/index.cjs.map +1 -1
- package/dist/network/index.d.cts +126 -5
- package/dist/network/index.d.ts +126 -5
- package/dist/network/index.js +683 -8
- package/dist/network/index.js.map +1 -1
- package/dist/systems/index.cjs +1702 -528
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +2 -2
- package/dist/systems/index.d.ts +2 -2
- package/dist/systems/index.js +4 -4
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/webgpu/index.d.ts +2 -2
- package/dist/worldSnapshotUtils.d.cts +1 -1
- package/dist/worldSnapshotUtils.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-JIJQFW6P.js.map +0 -1
- package/dist/chunk-NNDUOQ7O.js.map +0 -1
- package/dist/chunk-U727NTWQ.js.map +0 -1
package/dist/network/index.cjs
CHANGED
|
@@ -87,6 +87,12 @@ var _ColyseusTransport = class _ColyseusTransport {
|
|
|
87
87
|
onCorrect(cb) {
|
|
88
88
|
this.room.onMessage("correct", (m) => cb(m.id, m));
|
|
89
89
|
}
|
|
90
|
+
onSound(cb) {
|
|
91
|
+
this.room.onMessage("sound", (m) => cb(m, m.from));
|
|
92
|
+
}
|
|
93
|
+
onVoice(cb) {
|
|
94
|
+
this.room.onMessage("voice", cb);
|
|
95
|
+
}
|
|
90
96
|
onClose(cb) {
|
|
91
97
|
this.room.onLeave(() => cb());
|
|
92
98
|
}
|
|
@@ -108,6 +114,12 @@ var _ColyseusTransport = class _ColyseusTransport {
|
|
|
108
114
|
send(name, payload) {
|
|
109
115
|
this.room.send("m", { name, payload });
|
|
110
116
|
}
|
|
117
|
+
sendSound(sound) {
|
|
118
|
+
this.room.send("sound", sound);
|
|
119
|
+
}
|
|
120
|
+
sendVoice(message) {
|
|
121
|
+
this.room.send("voice", message);
|
|
122
|
+
}
|
|
111
123
|
leave() {
|
|
112
124
|
void this.room.leave();
|
|
113
125
|
}
|
|
@@ -2203,6 +2215,127 @@ var _IKComponent = class _IKComponent extends Component {
|
|
|
2203
2215
|
__name(_IKComponent, "IKComponent");
|
|
2204
2216
|
var IKComponent = _IKComponent;
|
|
2205
2217
|
|
|
2218
|
+
// core/ui/uiStyle.ts
|
|
2219
|
+
var isRecord = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
|
|
2220
|
+
var finite = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isFinite(value), "finite");
|
|
2221
|
+
var nonEmpty = /* @__PURE__ */ __name((value) => typeof value === "string" && value.length > 0, "nonEmpty");
|
|
2222
|
+
function normalizeStops(value) {
|
|
2223
|
+
if (!Array.isArray(value)) return null;
|
|
2224
|
+
const stops = [];
|
|
2225
|
+
for (const stop of value) {
|
|
2226
|
+
if (isRecord(stop) && finite(stop.at) && nonEmpty(stop.color)) {
|
|
2227
|
+
stops.push({ at: Math.min(1, Math.max(0, stop.at)), color: stop.color });
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return stops.length >= 2 ? stops : null;
|
|
2231
|
+
}
|
|
2232
|
+
__name(normalizeStops, "normalizeStops");
|
|
2233
|
+
function normalizePaint(value) {
|
|
2234
|
+
if (!isRecord(value)) return void 0;
|
|
2235
|
+
if (value.kind === "solid" && nonEmpty(value.color)) {
|
|
2236
|
+
return { kind: "solid", color: value.color };
|
|
2237
|
+
}
|
|
2238
|
+
if (value.kind === "linear") {
|
|
2239
|
+
const stops = normalizeStops(value.stops);
|
|
2240
|
+
if (stops) {
|
|
2241
|
+
return {
|
|
2242
|
+
kind: "linear",
|
|
2243
|
+
angle: finite(value.angle) ? value.angle : 180,
|
|
2244
|
+
stops
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
if (value.kind === "radial") {
|
|
2249
|
+
const stops = normalizeStops(value.stops);
|
|
2250
|
+
if (stops) return { kind: "radial", stops };
|
|
2251
|
+
}
|
|
2252
|
+
if (value.kind === "image" && nonEmpty(value.src)) {
|
|
2253
|
+
const fit = value.fit === "contain" || value.fit === "fill" ? value.fit : "cover";
|
|
2254
|
+
return {
|
|
2255
|
+
kind: "image",
|
|
2256
|
+
src: value.src,
|
|
2257
|
+
fit,
|
|
2258
|
+
...nonEmpty(value.tint) ? { tint: value.tint } : {}
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
return void 0;
|
|
2262
|
+
}
|
|
2263
|
+
__name(normalizePaint, "normalizePaint");
|
|
2264
|
+
function normalizeShadow(value) {
|
|
2265
|
+
if (!isRecord(value) || !nonEmpty(value.color)) return void 0;
|
|
2266
|
+
return {
|
|
2267
|
+
x: finite(value.x) ? value.x : 0,
|
|
2268
|
+
y: finite(value.y) ? value.y : 0,
|
|
2269
|
+
blur: finite(value.blur) ? Math.max(0, value.blur) : 0,
|
|
2270
|
+
color: value.color
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
__name(normalizeShadow, "normalizeShadow");
|
|
2274
|
+
function normalizeText(value) {
|
|
2275
|
+
if (!isRecord(value)) return void 0;
|
|
2276
|
+
const text = {};
|
|
2277
|
+
if (nonEmpty(value.font)) text.font = value.font;
|
|
2278
|
+
if (finite(value.weight)) text.weight = value.weight;
|
|
2279
|
+
if (finite(value.tracking)) text.tracking = value.tracking;
|
|
2280
|
+
if (value.transform === "upper" || value.transform === "lower") {
|
|
2281
|
+
text.transform = value.transform;
|
|
2282
|
+
}
|
|
2283
|
+
const paint = normalizePaint(value.paint);
|
|
2284
|
+
if (paint) text.paint = paint;
|
|
2285
|
+
if (finite(value.maxLines) && value.maxLines >= 1) {
|
|
2286
|
+
text.maxLines = Math.floor(value.maxLines);
|
|
2287
|
+
}
|
|
2288
|
+
const shadow = normalizeShadow(value.shadow);
|
|
2289
|
+
if (shadow) text.shadow = shadow;
|
|
2290
|
+
return Object.keys(text).length > 0 ? text : void 0;
|
|
2291
|
+
}
|
|
2292
|
+
__name(normalizeText, "normalizeText");
|
|
2293
|
+
function normalizeUiStyle(value) {
|
|
2294
|
+
if (!isRecord(value)) return void 0;
|
|
2295
|
+
const style = {};
|
|
2296
|
+
const fill = normalizePaint(value.fill);
|
|
2297
|
+
if (fill) style.fill = fill;
|
|
2298
|
+
if (isRecord(value.border) && finite(value.border.width)) {
|
|
2299
|
+
const paint = normalizePaint(value.border.paint);
|
|
2300
|
+
if (paint) {
|
|
2301
|
+
style.border = { width: Math.max(0, value.border.width), paint };
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (finite(value.radius)) {
|
|
2305
|
+
style.radius = Math.max(0, value.radius);
|
|
2306
|
+
} else if (Array.isArray(value.radius) && value.radius.length === 4 && value.radius.every(finite)) {
|
|
2307
|
+
style.radius = value.radius.map((r) => Math.max(0, r));
|
|
2308
|
+
}
|
|
2309
|
+
if (Array.isArray(value.shadows)) {
|
|
2310
|
+
const shadows = value.shadows.map(normalizeShadow).filter((s) => !!s);
|
|
2311
|
+
if (shadows.length > 0) style.shadows = shadows;
|
|
2312
|
+
}
|
|
2313
|
+
if (isRecord(value.edge) && finite(value.edge.width) && nonEmpty(value.edge.color)) {
|
|
2314
|
+
style.edge = {
|
|
2315
|
+
width: Math.max(0, value.edge.width),
|
|
2316
|
+
color: value.edge.color
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
if (finite(value.opacity)) {
|
|
2320
|
+
style.opacity = Math.min(1, Math.max(0, value.opacity));
|
|
2321
|
+
}
|
|
2322
|
+
const text = normalizeText(value.text);
|
|
2323
|
+
if (text) style.text = text;
|
|
2324
|
+
return Object.keys(style).length > 0 ? style : void 0;
|
|
2325
|
+
}
|
|
2326
|
+
__name(normalizeUiStyle, "normalizeUiStyle");
|
|
2327
|
+
function normalizeUiTokens(value) {
|
|
2328
|
+
if (!isRecord(value)) return void 0;
|
|
2329
|
+
const tokens = {};
|
|
2330
|
+
for (const [key, token] of Object.entries(value)) {
|
|
2331
|
+
if (nonEmpty(token) && key.length > 0 && !key.includes("/")) {
|
|
2332
|
+
tokens[key] = token;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return Object.keys(tokens).length > 0 ? tokens : void 0;
|
|
2336
|
+
}
|
|
2337
|
+
__name(normalizeUiTokens, "normalizeUiTokens");
|
|
2338
|
+
|
|
2206
2339
|
// core/components/UIPanel.ts
|
|
2207
2340
|
var _UIPanelComponent = class _UIPanelComponent extends Component {
|
|
2208
2341
|
constructor(options) {
|
|
@@ -2216,6 +2349,10 @@ var _UIPanelComponent = class _UIPanelComponent extends Component {
|
|
|
2216
2349
|
this.borderColor = options?.borderColor ?? "#333333";
|
|
2217
2350
|
this.pixelDensity = options?.pixelDensity ?? 1024;
|
|
2218
2351
|
this.follow = options?.follow ?? "world";
|
|
2352
|
+
const style = normalizeUiStyle(options?.style);
|
|
2353
|
+
if (style) this.style = style;
|
|
2354
|
+
const tokens = normalizeUiTokens(options?.tokens);
|
|
2355
|
+
if (tokens) this.tokens = tokens;
|
|
2219
2356
|
}
|
|
2220
2357
|
};
|
|
2221
2358
|
__name(_UIPanelComponent, "UIPanelComponent");
|
|
@@ -2235,6 +2372,11 @@ var _UITextComponent = class _UITextComponent extends Component {
|
|
|
2235
2372
|
this.valign = options?.valign ?? "middle";
|
|
2236
2373
|
this.lineHeight = options?.lineHeight ?? 1.25;
|
|
2237
2374
|
this.padding = options?.padding ?? 0.01;
|
|
2375
|
+
if (options?.pixelDensity !== void 0) {
|
|
2376
|
+
this.pixelDensity = options.pixelDensity;
|
|
2377
|
+
}
|
|
2378
|
+
const style = normalizeUiStyle(options?.style);
|
|
2379
|
+
if (style) this.style = style;
|
|
2238
2380
|
}
|
|
2239
2381
|
};
|
|
2240
2382
|
__name(_UITextComponent, "UITextComponent");
|
|
@@ -2256,6 +2398,13 @@ var _UIButtonComponent = class _UIButtonComponent extends Component {
|
|
|
2256
2398
|
this.cornerRadius = options?.cornerRadius ?? 0.02;
|
|
2257
2399
|
this.borderWidth = options?.borderWidth ?? 2e-3;
|
|
2258
2400
|
this.borderColor = options?.borderColor ?? "#3a3a3a";
|
|
2401
|
+
if (options?.pixelDensity !== void 0) {
|
|
2402
|
+
this.pixelDensity = options.pixelDensity;
|
|
2403
|
+
}
|
|
2404
|
+
const style = normalizeUiStyle(options?.style);
|
|
2405
|
+
if (style) this.style = style;
|
|
2406
|
+
if (options?.icon) this.icon = options.icon;
|
|
2407
|
+
if (options?.iconPosition) this.iconPosition = options.iconPosition;
|
|
2259
2408
|
}
|
|
2260
2409
|
};
|
|
2261
2410
|
__name(_UIButtonComponent, "UIButtonComponent");
|
|
@@ -2272,6 +2421,12 @@ var _UIToggleComponent = class _UIToggleComponent extends Component {
|
|
|
2272
2421
|
this.trackColor = options?.trackColor ?? "#2a2a2a";
|
|
2273
2422
|
this.accentColor = options?.accentColor ?? "#ff6b35";
|
|
2274
2423
|
this.knobColor = options?.knobColor ?? "#ffffff";
|
|
2424
|
+
if (options?.pixelDensity !== void 0) {
|
|
2425
|
+
this.pixelDensity = options.pixelDensity;
|
|
2426
|
+
}
|
|
2427
|
+
const style = normalizeUiStyle(options?.style);
|
|
2428
|
+
if (style) this.style = style;
|
|
2429
|
+
if (options?.variant) this.variant = options.variant;
|
|
2275
2430
|
}
|
|
2276
2431
|
};
|
|
2277
2432
|
__name(_UIToggleComponent, "UIToggleComponent");
|
|
@@ -2292,6 +2447,14 @@ var _UISliderComponent = class _UISliderComponent extends Component {
|
|
|
2292
2447
|
this.trackColor = options?.trackColor ?? "#2a2a2a";
|
|
2293
2448
|
this.accentColor = options?.accentColor ?? "#ff6b35";
|
|
2294
2449
|
this.knobColor = options?.knobColor ?? "#ffffff";
|
|
2450
|
+
if (options?.pixelDensity !== void 0) {
|
|
2451
|
+
this.pixelDensity = options.pixelDensity;
|
|
2452
|
+
}
|
|
2453
|
+
const style = normalizeUiStyle(options?.style);
|
|
2454
|
+
if (style) this.style = style;
|
|
2455
|
+
if (options?.variant) this.variant = options.variant;
|
|
2456
|
+
if (options?.step !== void 0) this.step = options.step;
|
|
2457
|
+
if (options?.readOnly !== void 0) this.readOnly = options.readOnly;
|
|
2295
2458
|
}
|
|
2296
2459
|
};
|
|
2297
2460
|
__name(_UISliderComponent, "UISliderComponent");
|
|
@@ -2572,6 +2735,14 @@ function applyParentLinks(snapshot, idMap, objectManagementSystem, world) {
|
|
|
2572
2735
|
__name(applyParentLinks, "applyParentLinks");
|
|
2573
2736
|
|
|
2574
2737
|
// core/hydration/postCreateComponents.ts
|
|
2738
|
+
var OPTIONAL_UI_FIELDS = {
|
|
2739
|
+
pixelDensity: "number",
|
|
2740
|
+
icon: "string",
|
|
2741
|
+
iconPosition: "string",
|
|
2742
|
+
variant: "string",
|
|
2743
|
+
step: "number",
|
|
2744
|
+
readOnly: "boolean"
|
|
2745
|
+
};
|
|
2575
2746
|
function remapEntityRefsInScriptData(data, idMap) {
|
|
2576
2747
|
const result = {};
|
|
2577
2748
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -2955,6 +3126,20 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
|
|
|
2955
3126
|
const target = component;
|
|
2956
3127
|
for (const [key, value] of Object.entries(source)) {
|
|
2957
3128
|
const current = target[key];
|
|
3129
|
+
if (key === "style") {
|
|
3130
|
+
const style = normalizeUiStyle(value);
|
|
3131
|
+
if (style) target.style = style;
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
if (key === "tokens") {
|
|
3135
|
+
const tokens = normalizeUiTokens(value);
|
|
3136
|
+
if (tokens) target.tokens = tokens;
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
if (OPTIONAL_UI_FIELDS[key] === typeof value) {
|
|
3140
|
+
target[key] = value;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
2958
3143
|
if ((typeof value === "string" || typeof value === "number" || typeof value === "boolean") && typeof current === typeof value) {
|
|
2959
3144
|
target[key] = value;
|
|
2960
3145
|
}
|
|
@@ -3917,16 +4102,16 @@ function instantiateSubtree(world, objectManagementSystem, snapshot) {
|
|
|
3917
4102
|
__name(instantiateSubtree, "instantiateSubtree");
|
|
3918
4103
|
|
|
3919
4104
|
// core/prefab/types.ts
|
|
3920
|
-
var
|
|
4105
|
+
var isRecord2 = /* @__PURE__ */ __name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isRecord");
|
|
3921
4106
|
function parsePrefabAsset(json) {
|
|
3922
|
-
if (!
|
|
4107
|
+
if (!isRecord2(json)) {
|
|
3923
4108
|
return null;
|
|
3924
4109
|
}
|
|
3925
4110
|
if (typeof json.version !== "number" || json.version < 1) {
|
|
3926
4111
|
return null;
|
|
3927
4112
|
}
|
|
3928
4113
|
const snapshot = json.snapshot;
|
|
3929
|
-
if (!
|
|
4114
|
+
if (!isRecord2(snapshot)) {
|
|
3930
4115
|
return null;
|
|
3931
4116
|
}
|
|
3932
4117
|
const entities = snapshot.entities;
|
|
@@ -3938,7 +4123,7 @@ function parsePrefabAsset(json) {
|
|
|
3938
4123
|
return null;
|
|
3939
4124
|
}
|
|
3940
4125
|
if (!components.every(
|
|
3941
|
-
(block) =>
|
|
4126
|
+
(block) => isRecord2(block) && typeof block.component === "string" && Array.isArray(block.entries)
|
|
3942
4127
|
)) {
|
|
3943
4128
|
return null;
|
|
3944
4129
|
}
|
|
@@ -4572,6 +4757,623 @@ var _SnapshotBuffer = class _SnapshotBuffer {
|
|
|
4572
4757
|
__name(_SnapshotBuffer, "SnapshotBuffer");
|
|
4573
4758
|
var SnapshotBuffer = _SnapshotBuffer;
|
|
4574
4759
|
|
|
4760
|
+
// core/network/sdp.ts
|
|
4761
|
+
var OPUS_VOICE = {
|
|
4762
|
+
useinbandfec: "1",
|
|
4763
|
+
usedtx: "1",
|
|
4764
|
+
stereo: "0",
|
|
4765
|
+
"sprop-stereo": "0",
|
|
4766
|
+
maxaveragebitrate: "20000",
|
|
4767
|
+
maxplaybackrate: "24000"
|
|
4768
|
+
};
|
|
4769
|
+
var VOICE_PTIME = 40;
|
|
4770
|
+
function mergeFmtp(fmtp, params) {
|
|
4771
|
+
const pairs = fmtp.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
4772
|
+
const i = part.indexOf("=");
|
|
4773
|
+
return i < 0 ? [part, ""] : [part.slice(0, i), part.slice(i + 1)];
|
|
4774
|
+
});
|
|
4775
|
+
for (const [key, value] of Object.entries(params)) {
|
|
4776
|
+
const found = pairs.find((pair) => pair[0].toLowerCase() === key.toLowerCase());
|
|
4777
|
+
if (found) found[1] = value;
|
|
4778
|
+
else pairs.push([key, value]);
|
|
4779
|
+
}
|
|
4780
|
+
return pairs.map(([k, v]) => v === "" ? k : `${k}=${v}`).join(";");
|
|
4781
|
+
}
|
|
4782
|
+
__name(mergeFmtp, "mergeFmtp");
|
|
4783
|
+
function tuneOpus(sdp, params = OPUS_VOICE, ptime = VOICE_PTIME) {
|
|
4784
|
+
const eol = sdp.includes("\r\n") ? "\r\n" : "\n";
|
|
4785
|
+
const sections = [[]];
|
|
4786
|
+
for (const line of sdp.split(eol)) {
|
|
4787
|
+
if (line.startsWith("m=")) sections.push([]);
|
|
4788
|
+
sections[sections.length - 1].push(line);
|
|
4789
|
+
}
|
|
4790
|
+
const out = [];
|
|
4791
|
+
for (const section of sections) {
|
|
4792
|
+
const opus = section.flatMap((line) => {
|
|
4793
|
+
const match = /^a=rtpmap:(\d+) opus\/48000/i.exec(line);
|
|
4794
|
+
return match ? [match[1]] : [];
|
|
4795
|
+
});
|
|
4796
|
+
if (!section[0]?.startsWith("m=audio") || !opus.length) {
|
|
4797
|
+
out.push(...section);
|
|
4798
|
+
continue;
|
|
4799
|
+
}
|
|
4800
|
+
const tuned = [];
|
|
4801
|
+
for (const line of section) {
|
|
4802
|
+
const fmtp = /^a=fmtp:(\d+) (.*)$/.exec(line);
|
|
4803
|
+
if (fmtp && opus.includes(fmtp[1])) {
|
|
4804
|
+
tuned.push(`a=fmtp:${fmtp[1]} ${mergeFmtp(fmtp[2], params)}`);
|
|
4805
|
+
continue;
|
|
4806
|
+
}
|
|
4807
|
+
tuned.push(line);
|
|
4808
|
+
const rtpmap = /^a=rtpmap:(\d+) opus\/48000/i.exec(line);
|
|
4809
|
+
if (rtpmap && !section.some((l) => l.startsWith(`a=fmtp:${rtpmap[1]} `))) {
|
|
4810
|
+
tuned.push(`a=fmtp:${rtpmap[1]} ${mergeFmtp("", params)}`);
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
if (ptime && !tuned.some((line) => line.startsWith("a=ptime:"))) {
|
|
4814
|
+
const at = tuned[tuned.length - 1] === "" ? tuned.length - 1 : tuned.length;
|
|
4815
|
+
tuned.splice(at, 0, `a=ptime:${ptime}`);
|
|
4816
|
+
}
|
|
4817
|
+
out.push(...tuned);
|
|
4818
|
+
}
|
|
4819
|
+
return out.join(eol);
|
|
4820
|
+
}
|
|
4821
|
+
__name(tuneOpus, "tuneOpus");
|
|
4822
|
+
|
|
4823
|
+
// core/network/voice.ts
|
|
4824
|
+
var LEVEL_BPS = [24e3, 16e3, 1e4];
|
|
4825
|
+
var WORST_LEVEL = 2;
|
|
4826
|
+
var TICK_MS = 2e3;
|
|
4827
|
+
var POLL_MS = 100;
|
|
4828
|
+
var GOOD_TO_STEP_UP_MS = 1e4;
|
|
4829
|
+
var MAX_LOSS = 0.05;
|
|
4830
|
+
var MAX_RTT_MS = 400;
|
|
4831
|
+
var TALKING_LEVEL = 0.06;
|
|
4832
|
+
var DUCK_SECONDS = 0.6;
|
|
4833
|
+
var MAX_RESTARTS = 3;
|
|
4834
|
+
function startingLevel(others) {
|
|
4835
|
+
if (others <= 2) return 0;
|
|
4836
|
+
return others <= 4 ? 1 : 2;
|
|
4837
|
+
}
|
|
4838
|
+
__name(startingLevel, "startingLevel");
|
|
4839
|
+
function adaptLevel(level, best, bad, goodMs) {
|
|
4840
|
+
let next = Math.max(level, best);
|
|
4841
|
+
if (bad) {
|
|
4842
|
+
return { level: Math.min(WORST_LEVEL, next + 1), goodMs: 0 };
|
|
4843
|
+
}
|
|
4844
|
+
let good = goodMs + TICK_MS;
|
|
4845
|
+
if (good >= GOOD_TO_STEP_UP_MS && next > best) {
|
|
4846
|
+
next -= 1;
|
|
4847
|
+
good = 0;
|
|
4848
|
+
}
|
|
4849
|
+
return { level: next, goodMs: good };
|
|
4850
|
+
}
|
|
4851
|
+
__name(adaptLevel, "adaptLevel");
|
|
4852
|
+
function micError(error) {
|
|
4853
|
+
const name = error.name;
|
|
4854
|
+
if (name === "NotAllowedError") return "Microphone blocked: allow it in your browser settings";
|
|
4855
|
+
if (name === "NotFoundError") return "No microphone found";
|
|
4856
|
+
return "Could not start the microphone";
|
|
4857
|
+
}
|
|
4858
|
+
__name(micError, "micError");
|
|
4859
|
+
async function readLinkStats(pc) {
|
|
4860
|
+
const stats = await pc.getStats();
|
|
4861
|
+
let loss = 0;
|
|
4862
|
+
let rttMs = 0;
|
|
4863
|
+
stats.forEach((report) => {
|
|
4864
|
+
const r = report;
|
|
4865
|
+
if (r.type === "remote-inbound-rtp" && r.kind === "audio") {
|
|
4866
|
+
if (typeof r.fractionLost === "number") loss = Math.max(loss, r.fractionLost);
|
|
4867
|
+
}
|
|
4868
|
+
if (r.type === "candidate-pair" && r.state === "succeeded" && r.nominated !== false) {
|
|
4869
|
+
if (typeof r.currentRoundTripTime === "number") {
|
|
4870
|
+
rttMs = Math.max(rttMs, r.currentRoundTripTime * 1e3);
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
});
|
|
4874
|
+
return { loss, rttMs };
|
|
4875
|
+
}
|
|
4876
|
+
__name(readLinkStats, "readLinkStats");
|
|
4877
|
+
var _VoiceChat = class _VoiceChat {
|
|
4878
|
+
constructor(transport, deps) {
|
|
4879
|
+
this.transport = transport;
|
|
4880
|
+
this.deps = deps;
|
|
4881
|
+
this.available = false;
|
|
4882
|
+
this.state = "off";
|
|
4883
|
+
this.micMuted = false;
|
|
4884
|
+
this.talking = false;
|
|
4885
|
+
this.error = "";
|
|
4886
|
+
/** Hear each voice from its character, when there is one. Off, everyone's in your ear. */
|
|
4887
|
+
this.positional = true;
|
|
4888
|
+
this.peersById = /* @__PURE__ */ new Map();
|
|
4889
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
4890
|
+
this.config = {};
|
|
4891
|
+
this.track = null;
|
|
4892
|
+
this.meter = null;
|
|
4893
|
+
this.level = 0;
|
|
4894
|
+
this.goodMs = 0;
|
|
4895
|
+
this.stopTimers = null;
|
|
4896
|
+
this.disposed = false;
|
|
4897
|
+
transport.onVoice((message) => this.fromRoom(message));
|
|
4898
|
+
transport.sendVoice({ t: "hello" });
|
|
4899
|
+
}
|
|
4900
|
+
get peers() {
|
|
4901
|
+
return [...this.peersById.values()].map((p) => ({
|
|
4902
|
+
id: p.id,
|
|
4903
|
+
connected: p.connected,
|
|
4904
|
+
talking: p.talking,
|
|
4905
|
+
micOn: p.micOn,
|
|
4906
|
+
muted: p.muted,
|
|
4907
|
+
volume: p.volume
|
|
4908
|
+
}));
|
|
4909
|
+
}
|
|
4910
|
+
onChange(listener) {
|
|
4911
|
+
this.listeners.add(listener);
|
|
4912
|
+
return () => {
|
|
4913
|
+
this.listeners.delete(listener);
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
changed() {
|
|
4917
|
+
for (const listener of this.listeners) listener();
|
|
4918
|
+
}
|
|
4919
|
+
/** Turn voice on. Call from a tap or key: that's what lets the browser ask for the mic. */
|
|
4920
|
+
async enable() {
|
|
4921
|
+
if (this.state !== "off") return true;
|
|
4922
|
+
if (!this.available) return false;
|
|
4923
|
+
this.error = "";
|
|
4924
|
+
this.state = "connecting";
|
|
4925
|
+
this.changed();
|
|
4926
|
+
this.deps.setCapture(true);
|
|
4927
|
+
try {
|
|
4928
|
+
await this.acquire();
|
|
4929
|
+
} catch (error) {
|
|
4930
|
+
this.deps.setCapture(false);
|
|
4931
|
+
this.state = "off";
|
|
4932
|
+
this.error = micError(error);
|
|
4933
|
+
this.changed();
|
|
4934
|
+
return false;
|
|
4935
|
+
}
|
|
4936
|
+
if (this.disposed || this.state !== "connecting") {
|
|
4937
|
+
this.release();
|
|
4938
|
+
return false;
|
|
4939
|
+
}
|
|
4940
|
+
this.micMuted = false;
|
|
4941
|
+
this.transport.sendVoice({ t: "join" });
|
|
4942
|
+
return true;
|
|
4943
|
+
}
|
|
4944
|
+
async acquire() {
|
|
4945
|
+
const stream = await this.deps.getMic({
|
|
4946
|
+
audio: {
|
|
4947
|
+
echoCancellation: true,
|
|
4948
|
+
noiseSuppression: true,
|
|
4949
|
+
autoGainControl: true,
|
|
4950
|
+
channelCount: 1
|
|
4951
|
+
},
|
|
4952
|
+
video: false
|
|
4953
|
+
});
|
|
4954
|
+
this.track = stream.getAudioTracks()[0] ?? null;
|
|
4955
|
+
if (this.track) this.track.onended = () => this.lost();
|
|
4956
|
+
this.meter?.close();
|
|
4957
|
+
this.meter = this.deps.analyse(stream);
|
|
4958
|
+
}
|
|
4959
|
+
release() {
|
|
4960
|
+
try {
|
|
4961
|
+
this.track?.stop();
|
|
4962
|
+
} catch {
|
|
4963
|
+
}
|
|
4964
|
+
this.track = null;
|
|
4965
|
+
this.meter?.close();
|
|
4966
|
+
this.meter = null;
|
|
4967
|
+
this.talking = false;
|
|
4968
|
+
this.deps.setCapture(false);
|
|
4969
|
+
}
|
|
4970
|
+
/** The mic stopped by itself: the phone took it for a call, or the device went away. */
|
|
4971
|
+
lost() {
|
|
4972
|
+
if (this.state === "off") return;
|
|
4973
|
+
this.track = null;
|
|
4974
|
+
this.micMuted = true;
|
|
4975
|
+
this.error = "Your mic stopped: tap the mic to turn it back on";
|
|
4976
|
+
this.transport.sendVoice({ t: "mic", on: false });
|
|
4977
|
+
this.sendMicToAll();
|
|
4978
|
+
this.changed();
|
|
4979
|
+
}
|
|
4980
|
+
/** Turn voice off. The mic stops at once. */
|
|
4981
|
+
disable() {
|
|
4982
|
+
if (this.state === "off") return;
|
|
4983
|
+
this.transport.sendVoice({ t: "leave" });
|
|
4984
|
+
this.shutDown();
|
|
4985
|
+
this.changed();
|
|
4986
|
+
}
|
|
4987
|
+
shutDown() {
|
|
4988
|
+
this.state = "off";
|
|
4989
|
+
this.micMuted = false;
|
|
4990
|
+
this.stopTimers?.();
|
|
4991
|
+
this.stopTimers = null;
|
|
4992
|
+
for (const peer of [...this.peersById.values()]) this.closePeer(peer);
|
|
4993
|
+
this.release();
|
|
4994
|
+
}
|
|
4995
|
+
/** Mute or unmute this player's mic. Everyone in voice sees it. */
|
|
4996
|
+
async setMicMuted(muted) {
|
|
4997
|
+
if (this.state !== "on") return;
|
|
4998
|
+
if (!muted && !this.track) {
|
|
4999
|
+
try {
|
|
5000
|
+
this.deps.setCapture(true);
|
|
5001
|
+
await this.acquire();
|
|
5002
|
+
this.error = "";
|
|
5003
|
+
} catch (error) {
|
|
5004
|
+
this.error = micError(error);
|
|
5005
|
+
this.changed();
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
this.micMuted = muted;
|
|
5010
|
+
this.transport.sendVoice({ t: "mic", on: !muted });
|
|
5011
|
+
this.sendMicToAll();
|
|
5012
|
+
this.changed();
|
|
5013
|
+
}
|
|
5014
|
+
/** Stop hearing someone. They aren't told. */
|
|
5015
|
+
setPeerMuted(id, muted) {
|
|
5016
|
+
const peer = this.peersById.get(id);
|
|
5017
|
+
if (!peer) return;
|
|
5018
|
+
peer.muted = muted;
|
|
5019
|
+
peer.player?.setVolume(muted ? 0 : peer.volume);
|
|
5020
|
+
this.changed();
|
|
5021
|
+
}
|
|
5022
|
+
setPeerVolume(id, volume) {
|
|
5023
|
+
const peer = this.peersById.get(id);
|
|
5024
|
+
if (!peer) return;
|
|
5025
|
+
peer.volume = volume;
|
|
5026
|
+
if (!peer.muted) peer.player?.setVolume(volume);
|
|
5027
|
+
this.changed();
|
|
5028
|
+
}
|
|
5029
|
+
fromRoom(m) {
|
|
5030
|
+
if (m.t === "caps") {
|
|
5031
|
+
this.available = m.available && this.deps.supported();
|
|
5032
|
+
this.changed();
|
|
5033
|
+
} else if (m.t === "refused") {
|
|
5034
|
+
if (this.state === "off") return;
|
|
5035
|
+
this.shutDown();
|
|
5036
|
+
this.error = m.reason === "full" ? "Voice is full in this room" : "Voice chat isn't available in this game";
|
|
5037
|
+
this.changed();
|
|
5038
|
+
} else if (m.t === "welcome") {
|
|
5039
|
+
if (this.state !== "connecting") return;
|
|
5040
|
+
this.config = {
|
|
5041
|
+
iceServers: m.iceServers,
|
|
5042
|
+
iceTransportPolicy: m.relayOnly ? "relay" : "all"
|
|
5043
|
+
};
|
|
5044
|
+
this.state = "on";
|
|
5045
|
+
this.level = startingLevel(m.peers.length);
|
|
5046
|
+
this.startTimers();
|
|
5047
|
+
for (const other of m.peers) {
|
|
5048
|
+
void this.call(this.addPeer(other.id, true, other.mic));
|
|
5049
|
+
}
|
|
5050
|
+
this.changed();
|
|
5051
|
+
} else if (this.state !== "on") ; else if (m.t === "joined") {
|
|
5052
|
+
this.addPeer(m.id, false, true);
|
|
5053
|
+
this.retune();
|
|
5054
|
+
this.changed();
|
|
5055
|
+
} else if (m.t === "left") {
|
|
5056
|
+
const peer = this.peersById.get(m.id);
|
|
5057
|
+
if (peer) this.closePeer(peer);
|
|
5058
|
+
this.retune();
|
|
5059
|
+
this.changed();
|
|
5060
|
+
} else if (m.t === "mic") {
|
|
5061
|
+
const peer = this.peersById.get(m.id);
|
|
5062
|
+
if (peer) peer.micOn = m.on;
|
|
5063
|
+
this.changed();
|
|
5064
|
+
} else if (m.t === "signal") {
|
|
5065
|
+
void this.onSignal(m.from, m.signal);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
addPeer(id, caller, micOn) {
|
|
5069
|
+
const existing = this.peersById.get(id);
|
|
5070
|
+
if (existing) return existing;
|
|
5071
|
+
const pc = this.deps.createConnection(this.config);
|
|
5072
|
+
const peer = {
|
|
5073
|
+
id,
|
|
5074
|
+
pc,
|
|
5075
|
+
caller,
|
|
5076
|
+
audio: null,
|
|
5077
|
+
pendingIce: [],
|
|
5078
|
+
player: null,
|
|
5079
|
+
meter: null,
|
|
5080
|
+
connected: false,
|
|
5081
|
+
micOn,
|
|
5082
|
+
muted: false,
|
|
5083
|
+
volume: 1,
|
|
5084
|
+
talking: false,
|
|
5085
|
+
restarts: 0
|
|
5086
|
+
};
|
|
5087
|
+
this.peersById.set(id, peer);
|
|
5088
|
+
pc.ontrack = (event) => {
|
|
5089
|
+
if (event.track.kind === "audio") this.hear(peer, event.track);
|
|
5090
|
+
};
|
|
5091
|
+
pc.onicecandidate = (event) => {
|
|
5092
|
+
if (event.candidate) this.signal(id, { kind: "ice", candidate: event.candidate.toJSON() });
|
|
5093
|
+
};
|
|
5094
|
+
pc.onconnectionstatechange = () => this.connectionChanged(peer);
|
|
5095
|
+
return peer;
|
|
5096
|
+
}
|
|
5097
|
+
signal(to, signal) {
|
|
5098
|
+
this.transport.sendVoice({ t: "signal", to, signal });
|
|
5099
|
+
}
|
|
5100
|
+
hear(peer, track) {
|
|
5101
|
+
peer.player?.stop();
|
|
5102
|
+
peer.meter?.close();
|
|
5103
|
+
peer.player = this.deps.playRemote(track);
|
|
5104
|
+
peer.player.setVolume(peer.muted ? 0 : peer.volume);
|
|
5105
|
+
peer.meter = this.deps.analyse(track);
|
|
5106
|
+
}
|
|
5107
|
+
connectionChanged(peer) {
|
|
5108
|
+
if (this.peersById.get(peer.id) !== peer) return;
|
|
5109
|
+
const state = peer.pc.connectionState;
|
|
5110
|
+
if (state === "connected") {
|
|
5111
|
+
peer.connected = true;
|
|
5112
|
+
peer.restarts = 0;
|
|
5113
|
+
void this.applyBitrate(peer);
|
|
5114
|
+
this.changed();
|
|
5115
|
+
} else if (state === "failed") {
|
|
5116
|
+
peer.connected = false;
|
|
5117
|
+
this.changed();
|
|
5118
|
+
if (peer.caller && peer.restarts < MAX_RESTARTS) {
|
|
5119
|
+
peer.restarts += 1;
|
|
5120
|
+
void this.offer(peer, true);
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
async call(peer) {
|
|
5125
|
+
peer.audio = peer.pc.addTransceiver("audio", { direction: "sendrecv" });
|
|
5126
|
+
await this.sendMic(peer);
|
|
5127
|
+
await this.offer(peer, false);
|
|
5128
|
+
}
|
|
5129
|
+
async offer(peer, restart) {
|
|
5130
|
+
try {
|
|
5131
|
+
if (restart) peer.pc.restartIce();
|
|
5132
|
+
await peer.pc.setLocalDescription(await peer.pc.createOffer({ iceRestart: restart }));
|
|
5133
|
+
const sdp = peer.pc.localDescription?.sdp;
|
|
5134
|
+
if (sdp) this.signal(peer.id, { kind: "offer", sdp });
|
|
5135
|
+
} catch (error) {
|
|
5136
|
+
console.warn("[relu] Couldn't start a voice connection.", error);
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
async onSignal(from, signal) {
|
|
5140
|
+
const peer = this.addPeer(from, false, true);
|
|
5141
|
+
try {
|
|
5142
|
+
if (signal.kind === "ice") {
|
|
5143
|
+
if (peer.pc.remoteDescription) await peer.pc.addIceCandidate(signal.candidate);
|
|
5144
|
+
else peer.pendingIce.push(signal.candidate);
|
|
5145
|
+
return;
|
|
5146
|
+
}
|
|
5147
|
+
await peer.pc.setRemoteDescription({ type: signal.kind, sdp: tuneOpus(signal.sdp) });
|
|
5148
|
+
for (const candidate of peer.pendingIce.splice(0)) {
|
|
5149
|
+
await peer.pc.addIceCandidate(candidate);
|
|
5150
|
+
}
|
|
5151
|
+
if (signal.kind === "answer") return;
|
|
5152
|
+
if (!peer.audio) {
|
|
5153
|
+
const slot = peer.pc.getTransceivers().find((t) => t.receiver.track.kind === "audio");
|
|
5154
|
+
if (slot) {
|
|
5155
|
+
slot.direction = "sendrecv";
|
|
5156
|
+
peer.audio = slot;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
await this.sendMic(peer);
|
|
5160
|
+
await peer.pc.setLocalDescription(await peer.pc.createAnswer());
|
|
5161
|
+
const sdp = peer.pc.localDescription?.sdp;
|
|
5162
|
+
if (sdp) this.signal(from, { kind: "answer", sdp });
|
|
5163
|
+
} catch (error) {
|
|
5164
|
+
console.warn("[relu] A voice connection failed to set up.", error);
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
async sendMic(peer) {
|
|
5168
|
+
const sender = peer.audio?.sender;
|
|
5169
|
+
if (!sender) return;
|
|
5170
|
+
await sender.replaceTrack(this.micMuted ? null : this.track).catch(() => void 0);
|
|
5171
|
+
await this.applyBitrate(peer);
|
|
5172
|
+
}
|
|
5173
|
+
sendMicToAll() {
|
|
5174
|
+
for (const peer of this.peersById.values()) void this.sendMic(peer);
|
|
5175
|
+
}
|
|
5176
|
+
async applyBitrate(peer) {
|
|
5177
|
+
const sender = peer.audio?.sender;
|
|
5178
|
+
if (!sender) return;
|
|
5179
|
+
try {
|
|
5180
|
+
const parameters = sender.getParameters();
|
|
5181
|
+
if (!parameters.encodings?.length) parameters.encodings = [{}];
|
|
5182
|
+
parameters.encodings[0].maxBitrate = LEVEL_BPS[this.level];
|
|
5183
|
+
await sender.setParameters(parameters);
|
|
5184
|
+
} catch {
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
setLevel(level) {
|
|
5188
|
+
if (level === this.level) return;
|
|
5189
|
+
this.level = level;
|
|
5190
|
+
for (const peer of this.peersById.values()) void this.applyBitrate(peer);
|
|
5191
|
+
}
|
|
5192
|
+
/** Someone came or went: the rate each connection gets changes with it. */
|
|
5193
|
+
retune() {
|
|
5194
|
+
this.setLevel(Math.max(this.level, startingLevel(this.peersById.size)));
|
|
5195
|
+
}
|
|
5196
|
+
closePeer(peer) {
|
|
5197
|
+
peer.player?.stop();
|
|
5198
|
+
peer.meter?.close();
|
|
5199
|
+
peer.pc.ontrack = null;
|
|
5200
|
+
peer.pc.onicecandidate = null;
|
|
5201
|
+
peer.pc.onconnectionstatechange = null;
|
|
5202
|
+
peer.pc.close();
|
|
5203
|
+
this.peersById.delete(peer.id);
|
|
5204
|
+
}
|
|
5205
|
+
startTimers() {
|
|
5206
|
+
const stopPoll = this.deps.every(POLL_MS, () => this.poll());
|
|
5207
|
+
const stopTick = this.deps.every(TICK_MS, () => void this.tick());
|
|
5208
|
+
this.stopTimers = () => {
|
|
5209
|
+
stopPoll();
|
|
5210
|
+
stopTick();
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
/** Who's talking, where they are, and the music under them. */
|
|
5214
|
+
poll() {
|
|
5215
|
+
let anyone = false;
|
|
5216
|
+
let moved2 = false;
|
|
5217
|
+
for (const peer of this.peersById.values()) {
|
|
5218
|
+
const talking = !peer.muted && (peer.meter?.level() ?? 0) > TALKING_LEVEL;
|
|
5219
|
+
if (talking !== peer.talking) moved2 = true;
|
|
5220
|
+
peer.talking = talking;
|
|
5221
|
+
anyone || (anyone = talking);
|
|
5222
|
+
peer.player?.setPosition(this.positional ? this.deps.positionOf(peer.id) : null);
|
|
5223
|
+
}
|
|
5224
|
+
const me = !this.micMuted && (this.meter?.level() ?? 0) > TALKING_LEVEL;
|
|
5225
|
+
if (me !== this.talking) moved2 = true;
|
|
5226
|
+
this.talking = me;
|
|
5227
|
+
if (anyone) this.deps.duck(DUCK_SECONDS);
|
|
5228
|
+
if (moved2) this.changed();
|
|
5229
|
+
}
|
|
5230
|
+
/** Step quality to what the connections can carry. */
|
|
5231
|
+
async tick() {
|
|
5232
|
+
let bad = false;
|
|
5233
|
+
for (const peer of this.peersById.values()) {
|
|
5234
|
+
if (!peer.connected) continue;
|
|
5235
|
+
const stats = await readLinkStats(peer.pc).catch(() => null);
|
|
5236
|
+
if (stats && (stats.loss > MAX_LOSS || stats.rttMs > MAX_RTT_MS)) bad = true;
|
|
5237
|
+
}
|
|
5238
|
+
if (this.state !== "on") return;
|
|
5239
|
+
const step = adaptLevel(this.level, startingLevel(this.peersById.size), bad, this.goodMs);
|
|
5240
|
+
this.goodMs = step.goodMs;
|
|
5241
|
+
this.setLevel(step.level);
|
|
5242
|
+
}
|
|
5243
|
+
/** The room is going: stop everything. There's no one left to tell. */
|
|
5244
|
+
dispose() {
|
|
5245
|
+
this.disposed = true;
|
|
5246
|
+
this.shutDown();
|
|
5247
|
+
this.available = false;
|
|
5248
|
+
this.changed();
|
|
5249
|
+
this.listeners.clear();
|
|
5250
|
+
}
|
|
5251
|
+
};
|
|
5252
|
+
__name(_VoiceChat, "VoiceChat");
|
|
5253
|
+
var VoiceChat = _VoiceChat;
|
|
5254
|
+
|
|
5255
|
+
// core/network/voiceBrowser.ts
|
|
5256
|
+
var FULL_VOLUME_DISTANCE = 2;
|
|
5257
|
+
var SILENT_DISTANCE = 60;
|
|
5258
|
+
var POSITION_SMOOTHING = 0.05;
|
|
5259
|
+
function voiceGraph(mixer, stream) {
|
|
5260
|
+
const context = mixer?.listener.context;
|
|
5261
|
+
if (!mixer || !context || context.state !== "running") return null;
|
|
5262
|
+
const bus = mixer.input("voice");
|
|
5263
|
+
const source = context.createMediaStreamSource(stream);
|
|
5264
|
+
const gain = context.createGain();
|
|
5265
|
+
const panner = context.createPanner();
|
|
5266
|
+
panner.panningModel = "equalpower";
|
|
5267
|
+
panner.distanceModel = "inverse";
|
|
5268
|
+
panner.refDistance = FULL_VOLUME_DISTANCE;
|
|
5269
|
+
panner.maxDistance = SILENT_DISTANCE;
|
|
5270
|
+
source.connect(gain);
|
|
5271
|
+
gain.connect(bus);
|
|
5272
|
+
let placed = false;
|
|
5273
|
+
return {
|
|
5274
|
+
setVolume(volume) {
|
|
5275
|
+
gain.gain.setTargetAtTime(Math.max(0, Math.min(2, volume)), context.currentTime, 0.02);
|
|
5276
|
+
},
|
|
5277
|
+
setPosition(position) {
|
|
5278
|
+
if (Boolean(position) !== placed) {
|
|
5279
|
+
placed = !placed;
|
|
5280
|
+
gain.disconnect();
|
|
5281
|
+
panner.disconnect();
|
|
5282
|
+
if (placed) {
|
|
5283
|
+
gain.connect(panner);
|
|
5284
|
+
panner.connect(bus);
|
|
5285
|
+
} else {
|
|
5286
|
+
gain.connect(bus);
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
if (!position) return;
|
|
5290
|
+
const now = context.currentTime;
|
|
5291
|
+
panner.positionX.setTargetAtTime(position.x, now, POSITION_SMOOTHING);
|
|
5292
|
+
panner.positionY.setTargetAtTime(position.y, now, POSITION_SMOOTHING);
|
|
5293
|
+
panner.positionZ.setTargetAtTime(position.z, now, POSITION_SMOOTHING);
|
|
5294
|
+
},
|
|
5295
|
+
stop() {
|
|
5296
|
+
source.disconnect();
|
|
5297
|
+
gain.disconnect();
|
|
5298
|
+
panner.disconnect();
|
|
5299
|
+
}
|
|
5300
|
+
};
|
|
5301
|
+
}
|
|
5302
|
+
__name(voiceGraph, "voiceGraph");
|
|
5303
|
+
function playRemote(mixer, track) {
|
|
5304
|
+
const stream = new MediaStream([track]);
|
|
5305
|
+
const element = document.createElement("audio");
|
|
5306
|
+
element.autoplay = true;
|
|
5307
|
+
element.setAttribute("playsinline", "");
|
|
5308
|
+
element.srcObject = stream;
|
|
5309
|
+
element.style.display = "none";
|
|
5310
|
+
const graph = voiceGraph(mixer, stream);
|
|
5311
|
+
element.muted = graph !== null;
|
|
5312
|
+
document.body.appendChild(element);
|
|
5313
|
+
void element.play().catch(() => void 0);
|
|
5314
|
+
return {
|
|
5315
|
+
setVolume(volume) {
|
|
5316
|
+
if (graph) graph.setVolume(volume);
|
|
5317
|
+
else element.volume = Math.max(0, Math.min(1, volume));
|
|
5318
|
+
},
|
|
5319
|
+
setPosition(position) {
|
|
5320
|
+
graph?.setPosition(position);
|
|
5321
|
+
},
|
|
5322
|
+
stop() {
|
|
5323
|
+
graph?.stop();
|
|
5324
|
+
element.pause();
|
|
5325
|
+
element.srcObject = null;
|
|
5326
|
+
element.remove();
|
|
5327
|
+
}
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5330
|
+
__name(playRemote, "playRemote");
|
|
5331
|
+
function analyse(mixer, from) {
|
|
5332
|
+
const context = mixer?.listener.context;
|
|
5333
|
+
if (!context) return null;
|
|
5334
|
+
const stream = from instanceof MediaStream ? from : new MediaStream([from]);
|
|
5335
|
+
const source = context.createMediaStreamSource(stream);
|
|
5336
|
+
const analyser = context.createAnalyser();
|
|
5337
|
+
analyser.fftSize = 512;
|
|
5338
|
+
source.connect(analyser);
|
|
5339
|
+
const samples = new Float32Array(analyser.fftSize);
|
|
5340
|
+
return {
|
|
5341
|
+
level() {
|
|
5342
|
+
analyser.getFloatTimeDomainData(samples);
|
|
5343
|
+
let sum = 0;
|
|
5344
|
+
for (const sample of samples) sum += sample * sample;
|
|
5345
|
+
return Math.min(1, Math.sqrt(sum / samples.length) * 3);
|
|
5346
|
+
},
|
|
5347
|
+
close() {
|
|
5348
|
+
source.disconnect();
|
|
5349
|
+
analyser.disconnect();
|
|
5350
|
+
}
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
__name(analyse, "analyse");
|
|
5354
|
+
function browserVoiceDeps(world, positionOf) {
|
|
5355
|
+
const mixer = /* @__PURE__ */ __name(() => world.getAudioSystem()?.getMixer() ?? null, "mixer");
|
|
5356
|
+
return {
|
|
5357
|
+
supported: /* @__PURE__ */ __name(() => typeof RTCPeerConnection !== "undefined" && globalThis.isSecureContext && Boolean(navigator.mediaDevices?.getUserMedia), "supported"),
|
|
5358
|
+
createConnection: /* @__PURE__ */ __name((config) => new RTCPeerConnection(config), "createConnection"),
|
|
5359
|
+
getMic: /* @__PURE__ */ __name((constraints) => navigator.mediaDevices.getUserMedia(constraints), "getMic"),
|
|
5360
|
+
setCapture(on) {
|
|
5361
|
+
const session = navigator.audioSession;
|
|
5362
|
+
if (session) session.type = on ? "play-and-record" : "playback";
|
|
5363
|
+
if (on) void mixer()?.listener.context.resume();
|
|
5364
|
+
},
|
|
5365
|
+
analyse: /* @__PURE__ */ __name((from) => analyse(mixer(), from), "analyse"),
|
|
5366
|
+
playRemote: /* @__PURE__ */ __name((track) => playRemote(mixer(), track), "playRemote"),
|
|
5367
|
+
duck: /* @__PURE__ */ __name((seconds) => mixer()?.duckMusic(seconds), "duck"),
|
|
5368
|
+
positionOf,
|
|
5369
|
+
every(ms, fn) {
|
|
5370
|
+
const timer = setInterval(fn, ms);
|
|
5371
|
+
return () => clearInterval(timer);
|
|
5372
|
+
}
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
__name(browserVoiceDeps, "browserVoiceDeps");
|
|
5376
|
+
|
|
4575
5377
|
// core/network/NetworkSystem.ts
|
|
4576
5378
|
var POSITION_EPSILON = 1e-3;
|
|
4577
5379
|
var ROTATION_EPSILON = 5e-4;
|
|
@@ -4611,9 +5413,14 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4611
5413
|
this.listeners = /* @__PURE__ */ new Map();
|
|
4612
5414
|
this.handles = /* @__PURE__ */ new Map();
|
|
4613
5415
|
this.spawnedAvatar = false;
|
|
5416
|
+
/** Voice chat, once connected with it on. */
|
|
5417
|
+
this.voice = null;
|
|
4614
5418
|
}
|
|
4615
|
-
/**
|
|
4616
|
-
|
|
5419
|
+
/**
|
|
5420
|
+
* Join the shared world. Call after hydrating, so scene objects exist.
|
|
5421
|
+
* `voice: true` adds voice chat; the room still decides whether it's offered.
|
|
5422
|
+
*/
|
|
5423
|
+
connect(world, transport, options = {}) {
|
|
4617
5424
|
this.world = world;
|
|
4618
5425
|
this.transport = transport;
|
|
4619
5426
|
world.setNetwork(this);
|
|
@@ -4626,7 +5433,14 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4626
5433
|
for (const fn of this.listeners.get(name) ?? []) fn(payload, from);
|
|
4627
5434
|
});
|
|
4628
5435
|
transport.onCorrect((id, pose) => this.corrected(id, pose));
|
|
5436
|
+
transport.onSound((sound) => this.heardSound(sound));
|
|
4629
5437
|
transport.onClose(() => this.disconnected());
|
|
5438
|
+
if (options.voice) {
|
|
5439
|
+
this.voice = new VoiceChat(
|
|
5440
|
+
transport,
|
|
5441
|
+
options.voice === true ? browserVoiceDeps(world, (id) => this.positionOf(id)) : options.voice
|
|
5442
|
+
);
|
|
5443
|
+
}
|
|
4630
5444
|
this.registerScene();
|
|
4631
5445
|
this.spawnAvatar();
|
|
4632
5446
|
}
|
|
@@ -4670,11 +5484,12 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4670
5484
|
}
|
|
4671
5485
|
});
|
|
4672
5486
|
}
|
|
4673
|
-
track(id, entityId, owner, view2) {
|
|
5487
|
+
track(id, entityId, owner, view2, prefab = "") {
|
|
4674
5488
|
const entry = {
|
|
4675
5489
|
id,
|
|
4676
5490
|
entityId,
|
|
4677
5491
|
owner,
|
|
5492
|
+
prefab,
|
|
4678
5493
|
buffer: new SnapshotBuffer(),
|
|
4679
5494
|
sentAt: 0,
|
|
4680
5495
|
sent: null,
|
|
@@ -4707,7 +5522,7 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4707
5522
|
this.entityChanged(view2);
|
|
4708
5523
|
return;
|
|
4709
5524
|
}
|
|
4710
|
-
const entry = this.track(view2.id, null, view2.owner, view2);
|
|
5525
|
+
const entry = this.track(view2.id, null, view2.owner, view2, view2.prefab);
|
|
4711
5526
|
if (!view2.prefab || !this.world) return;
|
|
4712
5527
|
euler.setFromQuaternion(quat.set(view2.qx, view2.qy, view2.qz, view2.qw));
|
|
4713
5528
|
void spawnPrefab(this.world, view2.prefab, {
|
|
@@ -4764,7 +5579,23 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4764
5579
|
if (transform) applyPose(transform, pose, true);
|
|
4765
5580
|
entry.sent = pose;
|
|
4766
5581
|
}
|
|
5582
|
+
heardSound(sound) {
|
|
5583
|
+
const { id, volume, x, y, z } = sound;
|
|
5584
|
+
const placed = x !== void 0 && y !== void 0 && z !== void 0;
|
|
5585
|
+
this.world?.getAudioSystem()?.playSlot(id, { volume, position: placed ? { x, y, z } : void 0 });
|
|
5586
|
+
}
|
|
5587
|
+
/** Where a player's character is: the first thing they spawned, which is their avatar. */
|
|
5588
|
+
positionOf(peerId) {
|
|
5589
|
+
for (const entry of this.tracked.values()) {
|
|
5590
|
+
if (entry.owner !== peerId || !entry.prefab || entry.entityId === null) continue;
|
|
5591
|
+
const position = this.world?.getComponent(entry.entityId, TransformComponent)?.position;
|
|
5592
|
+
return position ? { x: position.x, y: position.y, z: position.z } : null;
|
|
5593
|
+
}
|
|
5594
|
+
return null;
|
|
5595
|
+
}
|
|
4767
5596
|
disconnected() {
|
|
5597
|
+
this.voice?.dispose();
|
|
5598
|
+
this.voice = null;
|
|
4768
5599
|
this.transport = null;
|
|
4769
5600
|
this.world?.setNetwork(null);
|
|
4770
5601
|
}
|
|
@@ -4808,6 +5639,8 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4808
5639
|
}
|
|
4809
5640
|
}
|
|
4810
5641
|
dispose() {
|
|
5642
|
+
this.voice?.dispose();
|
|
5643
|
+
this.voice = null;
|
|
4811
5644
|
this.transport?.leave();
|
|
4812
5645
|
this.transport = null;
|
|
4813
5646
|
this.world?.setNetwork(null);
|
|
@@ -4854,7 +5687,22 @@ var _NetworkSystem = class _NetworkSystem {
|
|
|
4854
5687
|
get players() {
|
|
4855
5688
|
return self.transport?.players ?? [];
|
|
4856
5689
|
},
|
|
5690
|
+
get voice() {
|
|
5691
|
+
return self.voice;
|
|
5692
|
+
},
|
|
4857
5693
|
send: /* @__PURE__ */ __name((name, payload) => self.transport?.send(name, payload ?? null), "send"),
|
|
5694
|
+
playSlot: /* @__PURE__ */ __name((id, options = {}) => {
|
|
5695
|
+
const played = self.world?.getAudioSystem()?.playSlot(id, options) ?? false;
|
|
5696
|
+
if (played) {
|
|
5697
|
+
const { volume, position } = options;
|
|
5698
|
+
self.transport?.sendSound({
|
|
5699
|
+
id,
|
|
5700
|
+
...volume === void 0 ? {} : { volume },
|
|
5701
|
+
...position ? { x: position.x, y: position.y, z: position.z } : {}
|
|
5702
|
+
});
|
|
5703
|
+
}
|
|
5704
|
+
return played;
|
|
5705
|
+
}, "playSlot"),
|
|
4858
5706
|
onMessage: /* @__PURE__ */ __name((name, fn) => {
|
|
4859
5707
|
const set = self.listeners.get(name) ?? /* @__PURE__ */ new Set();
|
|
4860
5708
|
self.listeners.set(name, set);
|
|
@@ -4891,6 +5739,8 @@ var NetworkSystem = _NetworkSystem;
|
|
|
4891
5739
|
exports.ColyseusTransport = ColyseusTransport;
|
|
4892
5740
|
exports.NetworkSystem = NetworkSystem;
|
|
4893
5741
|
exports.SnapshotBuffer = SnapshotBuffer;
|
|
5742
|
+
exports.VoiceChat = VoiceChat;
|
|
5743
|
+
exports.browserVoiceDeps = browserVoiceDeps;
|
|
4894
5744
|
exports.withLatency = withLatency;
|
|
4895
5745
|
//# sourceMappingURL=index.cjs.map
|
|
4896
5746
|
//# sourceMappingURL=index.cjs.map
|