Skip to content

Commit 4dafab6

Browse files
authored
Merge pull request #114 from musehq/dev
v2.3.2
2 parents d00e3bd + e95b0c7 commit 4dafab6

File tree

10 files changed

+213
-130
lines changed

10 files changed

+213
-130
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
spacesvr
88
</h3>
99
<h5 align="center">
10-
A standardized reality for future of the 3D Web.
10+
A standardized reality for the future of the 3D Web.
1111
</h5>
1212

1313
<div align="center">
@@ -37,7 +37,7 @@
3737

3838
The mission of spacesvr is to organize and implement the standards for experiencing 3D content on the web in the same way that there exists standards for experiencing 2D content with HTML/CSS/JS.
3939

40-
spacesvr is designed to empower the artist. Instead of worrying about file structures or basic functionality like cross-device compatability, artists should spend their time telling their story. As such, consumption is optimized for simplicity, and the organization provides a framework to mediate along.
40+
spacesvr is designed to empower the artist. Instead of worrying about file structures or basic functionality like cross-device compatability, artists should spend their time telling their story. As such, consumption is optimized for simplicity, and the organization provides a framework to tell stories.
4141

4242
spacesvr is actively maintained by [Muse](https://www.muse.place?utm_source=npmjs&utm_campaign=learn_more), a YC-backed startup that provides tooling for visually building worlds. Muse's mission is to accelerate the adoption of 3D websites by increasing their accessibility, both for the end user and for the creator. Muse is completely built on spacesvr.
4343

@@ -212,6 +212,9 @@ type NetworkState = {
212212
connect: (config?: ConnectionConfig) => Promise<void>; // when autoconnect is off, use this to manually connect
213213
connections: Map<string, DataConnection>; // reference to active peer connections
214214
disconnect: () => void;
215+
voice: boolean; // whether voice is enabled
216+
setVoice: (v: boolean) => void; // enable/disable voice
217+
mediaConnections: Map<string, MediaConnection>; // reference to active media connections
215218
useChannel: <Data = any, State = any>(
216219
id: string,
217220
type: ChannelType,
@@ -382,6 +385,17 @@ Adds an infinite plane to walk on (added by default with the Environment Layer)
382385
/>
383386
```
384387

388+
#### Model
389+
390+
Quickly add a GLTF/GLB model to your scene. Will handle Suspense, KTX2, Draco, Meshopt.
391+
392+
```tsx
393+
<Model
394+
src="https://link-to-your-model.glb"
395+
center={false} // whether to center the model so its bounds are centered on its origin
396+
/>
397+
```
398+
385399
#### Video
386400

387401
Add a video file to your space with positional audio. Handles media playback rules for Safari, iOS, etc.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useNetwork } from "spacesvr";
2+
import { useEffect } from "react";
3+
4+
export default function PingPongMulti() {
5+
const { voice, setVoice } = useNetwork();
6+
7+
useEffect(() => {
8+
setTimeout(() => {
9+
console.log("setting to ", !voice);
10+
setVoice(!voice);
11+
}, 5000);
12+
}, [setVoice, voice]);
13+
14+
return null;
15+
}

examples/worlds/Multiplayer/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { StandardReality, Background, Model } from "spacesvr";
22
import LightSwitch from "./ideas/LightSwitch";
3+
import PingPongMulti from "./ideas/PingPongMulti";
34

45
export default function Multiplayer() {
56
return (
67
<StandardReality
78
playerProps={{ pos: [5, 1, 0], rot: Math.PI }}
89
networkProps={{ autoconnect: true, voice: true }}
910
>
11+
{/*<PingPongMulti />*/}
1012
<Background color={0xffffff} />
1113
<fog attach="fog" args={[0xffffff, 10, 90]} />
1214
<ambientLight />

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.3.1",
3+
"version": "2.3.2",
44
"private": true,
55
"description": "A standardized reality for future of the 3D Web",
66
"keywords": [

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.3.1",
51+
text: "v2.3.2",
5252
link: "https://www.npmjs.com/package/spacesvr",
5353
},
5454
...menuItems,

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,11 @@ export default function NetworkedEntities() {
4949
},
5050
}));
5151

52-
const snapshot: Snapshot = {
52+
SI.vault.add({
5353
id: Math.random().toString(),
5454
time: new Date().getTime(),
5555
state,
56-
};
57-
58-
SI.vault.add(snapshot);
56+
});
5957
});
6058

6159
// send own player data
@@ -77,23 +75,27 @@ export default function NetworkedEntities() {
7775
let i = 0;
7876
for (const entityState of snapshot.state) {
7977
const { x, y, z, q } = entityState;
80-
obj.position.x = x as number;
81-
obj.position.y = y as number;
82-
obj.position.z = z as number;
78+
obj.position.set(x as number, y as number, z as number);
8379
obj.position.y -= 0.2; // they were floating before, idk where the constant comes from really
8480
const quat = q as Quat;
85-
obj.quaternion.x = quat.x;
86-
obj.quaternion.y = quat.y;
87-
obj.quaternion.z = quat.z;
88-
obj.quaternion.w = quat.w;
81+
obj.quaternion.set(
82+
quat.x as number,
83+
quat.y as number,
84+
quat.z as number,
85+
quat.w as number
86+
);
8987
obj.updateMatrix();
9088
mesh.current.setMatrixAt(i, obj.matrix);
9189

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
90+
const posAudio = entities[i]?.posAudio;
91+
if (posAudio) {
92+
obj.matrix.decompose(
93+
posAudio.position,
94+
posAudio.quaternion,
95+
posAudio.scale
96+
);
97+
posAudio.rotation.y += Math.PI; // for some reason it's flipped
98+
posAudio.updateMatrix();
9799
}
98100

99101
i++;
@@ -107,11 +109,15 @@ export default function NetworkedEntities() {
107109
}
108110

109111
return (
110-
<group>
112+
<group name="spacesvr-entities">
111113
{entities.map(
112114
(entity) =>
113115
entity.posAudio && (
114-
<primitive key={entity.posAudio.uuid} object={entity.posAudio} />
116+
<primitive
117+
key={entity.posAudio.uuid}
118+
object={entity.posAudio}
119+
matrixAutoUpdate={false}
120+
/>
115121
)
116122
)}
117123
<instancedMesh

src/layers/Network/ideas/NetworkedEntities/logic/entity.ts

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type Entity = {
1212
};
1313

1414
export const useEntities = (): Entity[] => {
15-
const { connections, connected, voiceStreams } = useNetwork();
15+
const { connections, connected, mediaConnections } = useNetwork();
1616
const { paused } = useEnvironment();
1717

1818
const listener = useListener();
@@ -23,80 +23,74 @@ export const useEntities = (): Entity[] => {
2323

2424
const entities = useMemo<Entity[]>(() => [], []);
2525

26-
const sameIds = (ids1: string[], ids2: string[]) =>
27-
ids1.sort().join(",") === ids2.sort().join(",");
26+
const needsAudio = (e: Entity) => mediaConnections.has(e.id) && !e.posAudio;
2827

2928
// 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, () => {
29+
useLimitedFrame(5, () => {
3330
if (!connected) return;
3431

35-
// check for changes in connections
36-
if (!sameIds(connectionIds.current, Array.from(connections.keys()))) {
37-
connectionIds.current = Array.from(connections.keys());
32+
// changed flag to trigger re-render at the end
33+
let changed = false;
3834

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 });
35+
// remove old entities
36+
entities.map((e) => {
37+
if (!connections.has(e.id)) {
38+
if (e.posAudio) {
39+
e.posAudio.remove();
40+
e.posAudio = undefined;
5041
}
42+
entities.splice(entities.indexOf(e), 1);
43+
changed = true;
5144
}
45+
});
5246

53-
rerender();
47+
// add in new entities
48+
for (const id of Array.from(connections.keys())) {
49+
if (!entities.some((e) => e.id === id)) {
50+
entities.push({ id, posAudio: undefined });
51+
changed = true;
52+
}
5453
}
5554

5655
// 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
56+
if (!firstPaused) {
57+
// remove media connections streams that are no longer connected
6458
entities.map((e) => {
65-
if (!voiceIds.current.includes(e.id)) {
59+
if (!mediaConnections.has(e.id)) {
6660
e.posAudio?.remove();
6761
e.posAudio = undefined;
62+
changed = true;
6863
}
6964
});
7065

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;
66+
entities.filter(needsAudio).map((e) => {
67+
// add in new media connections if the stream is active
68+
const mediaConn = mediaConnections.get(e.id);
69+
if (!mediaConn) return;
70+
if (!mediaConn.remoteStream) return;
7871

7972
const audioElem = document.createElement("audio");
80-
audioElem.srcObject = stream;
73+
audioElem.srcObject = mediaConn.remoteStream; // remote is incoming, local is own voice
8174
audioElem.muted = true;
8275
audioElem.autoplay = true;
8376
audioElem.loop = true;
8477
//@ts-ignore
8578
audioElem.playsInline = true;
8679

8780
const posAudio = new PositionalAudio(listener);
88-
posAudio.userData.peerId = id;
89-
posAudio.setMediaStreamSource(stream);
81+
posAudio.userData.peerId = e.id;
82+
posAudio.setMediaStreamSource(audioElem.srcObject);
9083
posAudio.setRefDistance(2);
91-
posAudio.setDirectionalCone(200, 290, 0.2);
92-
posAudio.setVolume(0.6);
84+
posAudio.setDirectionalCone(200, 290, 0.35);
9385

9486
// posAudio.add(new PositionalAudioHelper(posAudio, 1));
95-
entity.posAudio = posAudio;
96-
}
87+
e.posAudio = posAudio;
9788

98-
rerender();
89+
changed = true;
90+
});
9991
}
92+
93+
if (changed) rerender();
10094
});
10195

10296
return entities;

src/layers/Network/logic/connection.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { useEffect, useMemo, useState } from "react";
2-
import { DataConnection, Peer } from "peerjs";
2+
import { DataConnection, MediaConnection, 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";
9+
import { useVoiceConnections } from "./voice";
1010
import { getMuseIceServers } from "./ice";
1111

1212
export type ConnectionState = {
1313
connected: boolean;
1414
connect: (config?: ConnectionConfig) => Promise<void>;
1515
connections: Map<string, DataConnection>;
16-
voiceStreams: Map<string, MediaStream>;
16+
mediaConnections: Map<string, MediaConnection>;
1717
disconnect: () => void;
1818
voice: boolean;
1919
setVoice: (v: boolean) => void;
@@ -43,6 +43,10 @@ export const useConnection = (
4343
console.log("connection closed with peer");
4444
connections.delete(conn.peer);
4545
});
46+
conn.on("error", () => {
47+
console.log("connection closed with peer");
48+
connections.delete(conn.peer);
49+
});
4650
channels.greet(conn);
4751
connections.set(conn.peer, conn);
4852
});
@@ -125,16 +129,16 @@ export const useConnection = (
125129

126130
const [voice, setVoice] = useState(!!externalConfig.voice);
127131
useEffect(() => setVoice(!!externalConfig.voice), [externalConfig.voice]);
128-
const voiceStreams = useVoice(voice, peer, connections);
132+
const mediaConnections = useVoiceConnections(voice, peer, connections);
129133

130134
return {
131135
connected,
132136
connect,
133137
disconnect,
134138
connections,
135-
voiceStreams,
136139
useChannel: channels.useChannel,
137140
voice,
138141
setVoice,
142+
mediaConnections,
139143
};
140144
};

0 commit comments

Comments
 (0)