Skip to content

Commit 0c0853d

Browse files
authored
Merge pull request #109 from musehq/dev
v2.3.0
2 parents 9c7a576 + 8e7de34 commit 0c0853d

File tree

11 files changed

+356
-55
lines changed

11 files changed

+356
-55
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ type PlayerState = {
190190

191191
#### Network Layer
192192

193-
_Provides multiplayer out-of-the-box. Muse provides signalling servers for everyone, but not STUN/TURN servers, which [you need for best performance](https://www.twilio.com/docs/stun-turn/faq#faq-what-is-nat)._
193+
_Provides multiplayer out-of-the-box. Muse provides signalling servers and [STUN/TURN](https://www.twilio.com/docs/stun-turn/faq#faq-what-is-nat) servers for everyone :)._
194194

195195
```tsx
196196
type NetworkProps = {
@@ -200,6 +200,7 @@ type NetworkProps = {
200200
host?: string; // signalling host url, uses Muse's servers by default
201201
sessionId?: string; // if you know the session id you want to use, enter it here
202202
worldName?: string; // the worldname to hash your signal peers by, by default set to the path name
203+
voice?: boolean; // whether to enable spatial voice chat, false by default
203204
};
204205
```
205206

examples/worlds/Multiplayer/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function Multiplayer() {
55
return (
66
<StandardReality
77
playerProps={{ pos: [5, 1, 0], rot: Math.PI }}
8-
networkProps={{ autoconnect: true }}
8+
networkProps={{ autoconnect: true, voice: true }}
99
>
1010
<Background color={0xffffff} />
1111
<fog attach="fog" args={[0xffffff, 10, 90]} />

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "spacesvr",
3-
"version": "2.2.1",
3+
"version": "2.3.0",
44
"private": true,
55
"description": "A standardized reality for future of the 3D Web",
66
"keywords": [

src/layers/Environment/logic/environment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useState,
88
} from "react";
99
import { Device, DeviceState, useDevice } from "./device";
10+
import { AudioContext } from "three";
1011

1112
export type MenuItem = { text: string; action: () => void };
1213

@@ -33,6 +34,12 @@ export const useEnvironmentState = (name: string): EnvironmentState => {
3334

3435
const setPaused = (p: boolean) => {
3536
setPausedValue(p);
37+
38+
// hook into paused click event to make sure global context is running
39+
// https://github.com/mrdoob/three.js/blob/342946c8392639028da439b6dc0597e58209c696/src/audio/AudioContext.js#L9
40+
const context = AudioContext.getContext();
41+
if (context.state !== "running") context.resume();
42+
3643
// call all pause events
3744
events.map((ev: PauseEvent) => ev.apply(null, [p]));
3845
};

src/layers/Environment/ui/PauseMenu/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default function PauseMenu(props: PauseMenuProps) {
4848
const PAUSE_ITEMS: PauseItem[] = [
4949
...pauseMenuItems,
5050
{
51-
text: "v2.2.1",
51+
text: "v2.3.0",
5252
link: "https://www.npmjs.com/package/spacesvr",
5353
},
5454
...menuItems,

src/layers/Network/ideas/NetworkedEntities.tsx renamed to src/layers/Network/ideas/NetworkedEntities/index.tsx

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,62 @@
1-
import { useMemo, useRef, useState } from "react";
1+
import { useMemo, useRef } from "react";
22
import {
33
CylinderBufferGeometry,
44
InstancedMesh,
55
MeshNormalMaterial,
6-
Object3D,
76
} from "three";
8-
import { useNetwork } from "../logic/network";
9-
import { useLimitedFrame } from "../../../logic/limiter";
7+
import { useNetwork } from "../../logic/network";
8+
import { useLimitedFrame } from "../../../../logic/limiter";
109
import { SnapshotInterpolation } from "@geckos.io/snapshot-interpolation";
1110
import {
1211
Snapshot,
1312
Entity as EntityState,
1413
Quat,
1514
} from "@geckos.io/snapshot-interpolation/lib/types";
15+
import { useObj } from "./logic/resources";
16+
import { useEntities } from "./logic/entity";
1617

1718
export default function NetworkedEntities() {
18-
const { connections, connected, useChannel } = useNetwork();
19+
const { connected, useChannel } = useNetwork();
1920

2021
const mesh = useRef<InstancedMesh>(null);
2122
const geo = useMemo(() => new CylinderBufferGeometry(0.3, 0.3, 1, 32), []);
2223
const mat = useMemo(() => new MeshNormalMaterial(), []);
23-
const obj = useMemo(() => {
24-
const o = new Object3D();
25-
o.matrixAutoUpdate = false;
26-
return o;
27-
}, []);
28-
29-
// check for a change in player list, re-render if there is a change
30-
const [entityIds, setEntityIds] = useState<string[]>([]);
31-
useLimitedFrame(6, () => {
32-
if (!connected) return;
33-
const ids = Array.from(connections.keys());
34-
const sameIds = ids.sort().join(",") === entityIds.sort().join(",");
35-
if (!sameIds) setEntityIds(ids);
36-
});
24+
const obj = useObj();
25+
26+
const entities = useEntities();
3727

28+
// set up channel to send/receive data
3829
const NETWORK_FPS = 12;
39-
type Entity = { pos: number[]; rot: number[] };
30+
type EntityTransform = { pos: number[]; rot: number[] };
4031
const SI = useMemo(() => new SnapshotInterpolation(NETWORK_FPS), []);
41-
const entityChannel = useChannel<Entity, { [key in string]: Entity }>(
42-
"player",
43-
"stream",
44-
(m, s) => {
45-
if (!m.conn) return;
46-
s[m.conn.peer] = m.data;
47-
48-
const state: EntityState[] = Object.keys(s).map((key) => ({
49-
id: key,
50-
x: s[key].pos[0],
51-
y: s[key].pos[1],
52-
z: s[key].pos[2],
53-
q: {
54-
x: s[key].rot[0],
55-
y: s[key].rot[1],
56-
z: s[key].rot[2],
57-
w: s[key].rot[3],
58-
},
59-
}));
60-
61-
const snapshot: Snapshot = {
62-
id: Math.random().toString(),
63-
time: new Date().getTime(),
64-
state,
65-
};
66-
67-
SI.vault.add(snapshot);
68-
}
69-
);
32+
const entityChannel = useChannel<
33+
EntityTransform,
34+
{ [id: string]: EntityTransform }
35+
>("player", "stream", (m, s) => {
36+
if (!m.conn) return;
37+
s[m.conn.peer] = m.data;
38+
39+
const state: EntityState[] = Object.keys(s).map((key) => ({
40+
id: key,
41+
x: s[key].pos[0],
42+
y: s[key].pos[1],
43+
z: s[key].pos[2],
44+
q: {
45+
x: s[key].rot[0],
46+
y: s[key].rot[1],
47+
z: s[key].rot[2],
48+
w: s[key].rot[3],
49+
},
50+
}));
51+
52+
const snapshot: Snapshot = {
53+
id: Math.random().toString(),
54+
time: new Date().getTime(),
55+
state,
56+
};
57+
58+
SI.vault.add(snapshot);
59+
});
7060

7161
// send own player data
7262
useLimitedFrame(NETWORK_FPS, ({ camera }) => {
@@ -98,6 +88,14 @@ export default function NetworkedEntities() {
9888
obj.quaternion.w = quat.w;
9989
obj.updateMatrix();
10090
mesh.current.setMatrixAt(i, obj.matrix);
91+
92+
const audio = entities[i]?.posAudio;
93+
if (audio) {
94+
obj.matrix.decompose(audio.position, audio.quaternion, audio.scale);
95+
audio.updateMatrix();
96+
audio.rotateY(Math.PI); // for some reason it's flipped
97+
}
98+
10199
i++;
102100
}
103101

@@ -110,9 +108,15 @@ export default function NetworkedEntities() {
110108

111109
return (
112110
<group>
111+
{entities.map(
112+
(entity) =>
113+
entity.posAudio && (
114+
<primitive key={entity.posAudio.uuid} object={entity.posAudio} />
115+
)
116+
)}
113117
<instancedMesh
114118
ref={mesh}
115-
args={[geo, mat, entityIds.length]}
119+
args={[geo, mat, entities.length]}
116120
matrixAutoUpdate={false}
117121
/>
118122
</group>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useEffect, useMemo, useRef, useState } from "react";
2+
import { useLimitedFrame } from "../../../../../logic/limiter";
3+
import { useNetwork } from "../../../logic/network";
4+
import { PositionalAudio } from "three";
5+
import { useListener } from "./resources";
6+
import { useEnvironment } from "../../../../Environment";
7+
import { PositionalAudioHelper } from "three/examples/jsm/helpers/PositionalAudioHelper";
8+
9+
type Entity = {
10+
id: string;
11+
posAudio: PositionalAudio | undefined;
12+
};
13+
14+
export const useEntities = (): Entity[] => {
15+
const { connections, connected, voiceStreams } = useNetwork();
16+
const { paused } = useEnvironment();
17+
18+
const listener = useListener();
19+
const [ct, setCt] = useState(0);
20+
const rerender = () => setCt(Math.random());
21+
const [firstPaused, setFirstPaused] = useState(true);
22+
useEffect(() => setFirstPaused(paused && firstPaused), [paused, firstPaused]);
23+
24+
const entities = useMemo<Entity[]>(() => [], []);
25+
26+
const sameIds = (ids1: string[], ids2: string[]) =>
27+
ids1.sort().join(",") === ids2.sort().join(",");
28+
29+
// check for a change in player list, re-render if there is a change
30+
const connectionIds = useRef<string[]>([]);
31+
const voiceIds = useRef<string[]>([]);
32+
useLimitedFrame(6, () => {
33+
if (!connected) return;
34+
35+
// check for changes in connections
36+
if (!sameIds(connectionIds.current, Array.from(connections.keys()))) {
37+
connectionIds.current = Array.from(connections.keys());
38+
39+
// remove entities that are no longer connected
40+
entities.map((e) => {
41+
if (!connectionIds.current.includes(e.id)) {
42+
entities.splice(entities.indexOf(e), 1);
43+
}
44+
});
45+
46+
// add in new entities
47+
for (const id of connectionIds.current) {
48+
if (!entities.some((e) => e.id === id)) {
49+
entities.push({ id, posAudio: undefined });
50+
}
51+
}
52+
53+
rerender();
54+
}
55+
56+
// dont run until first time unpaused to make sure audio context is running from first press
57+
if (
58+
!firstPaused &&
59+
!sameIds(voiceIds.current, Array.from(voiceStreams.keys()))
60+
) {
61+
voiceIds.current = Array.from(voiceStreams.keys());
62+
63+
// remove voice streams that are no longer connected
64+
entities.map((e) => {
65+
if (!voiceIds.current.includes(e.id)) {
66+
e.posAudio?.remove();
67+
e.posAudio = undefined;
68+
}
69+
});
70+
71+
// add in new voice streams
72+
for (const id of voiceIds.current) {
73+
const entity = entities.find((e) => e.id === id);
74+
if (!entity) continue;
75+
76+
const stream = voiceStreams.get(id)!;
77+
if (!stream) continue;
78+
79+
const audioElem = document.createElement("audio");
80+
audioElem.srcObject = stream;
81+
audioElem.muted = true;
82+
audioElem.autoplay = true;
83+
audioElem.loop = true;
84+
//@ts-ignore
85+
audioElem.playsInline = true;
86+
87+
const posAudio = new PositionalAudio(listener);
88+
posAudio.userData.peerId = id;
89+
posAudio.setMediaStreamSource(stream);
90+
posAudio.setRefDistance(2);
91+
posAudio.setDirectionalCone(200, 290, 0.2);
92+
posAudio.setVolume(0.6);
93+
94+
// posAudio.add(new PositionalAudioHelper(posAudio, 1));
95+
entity.posAudio = posAudio;
96+
}
97+
98+
rerender();
99+
}
100+
});
101+
102+
return entities;
103+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useThree } from "@react-three/fiber";
2+
import { useMemo } from "react";
3+
import { AudioListener, Object3D } from "three";
4+
5+
export const useListener = (): AudioListener => {
6+
const cam = useThree((st) => st.camera);
7+
return useMemo(() => {
8+
const listen = new AudioListener();
9+
cam.add(listen);
10+
return listen;
11+
}, [cam]);
12+
};
13+
14+
export const useObj = (): Object3D => {
15+
return useMemo(() => {
16+
const o = new Object3D();
17+
o.matrixAutoUpdate = false;
18+
return o;
19+
}, []);
20+
};

src/layers/Network/logic/connection.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import { useMemo, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import { DataConnection, Peer } from "peerjs";
33
import { isLocalNetwork } from "./local";
44
import { LocalSignaller } from "./signallers/LocalSignaller";
55
import { MuseSignaller } from "./signallers/MuseSignaller";
66
import { useWaving } from "./wave";
77
import { Signaller, SignallerConfig } from "./signallers";
88
import { Channels, useChannels } from "./channels";
9+
import { useVoice } from "./voice";
10+
import { getMuseIceServers } from "./ice";
911

1012
export type ConnectionState = {
1113
connected: boolean;
1214
connect: (config?: ConnectionConfig) => Promise<void>;
1315
connections: Map<string, DataConnection>;
16+
voiceStreams: Map<string, MediaStream>;
1417
disconnect: () => void;
18+
setVoice: (v: boolean) => void;
1519
} & Pick<Channels, "useChannel">;
1620

1721
export type ConnectionConfig = {
1822
iceServers?: RTCIceServer[];
23+
voice?: boolean;
1924
} & SignallerConfig;
2025

2126
export const useConnection = (
@@ -56,8 +61,14 @@ export const useConnection = (
5661

5762
const finalConfig = { ...externalConfig, ...config };
5863

64+
if (!finalConfig.iceServers) {
65+
const servers = await getMuseIceServers(finalConfig.host);
66+
if (servers) finalConfig.iceServers = servers;
67+
}
68+
5969
const peerConfig: any = {};
6070
if (finalConfig.iceServers) peerConfig.iceServers = finalConfig.iceServers;
71+
6172
const p = new Peer({ config: peerConfig });
6273

6374
p.on("connection", registerConnection); // incoming
@@ -111,11 +122,17 @@ export const useConnection = (
111122

112123
useWaving(1, signaller, disconnect);
113124

125+
const [voice, setVoice] = useState(!!externalConfig.voice);
126+
useEffect(() => setVoice(!!externalConfig.voice), [externalConfig.voice]);
127+
const voiceStreams = useVoice(voice, peer, connections);
128+
114129
return {
115130
connected,
116131
connect,
117132
disconnect,
118133
connections,
134+
voiceStreams,
119135
useChannel: channels.useChannel,
136+
setVoice,
120137
};
121138
};

0 commit comments

Comments
 (0)