Skip to content

Commit 8787b34

Browse files
author
Avaer Kazmer
committed
Add multiplayer.js
1 parent cf6af97 commit 8787b34

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed

app.html

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<script src="html-beautify.js"></script>
2424
<script src="file-type.js"></script>
2525
<script src="namegen.js"></script>
26+
<script src="multiplayer.js"></script>
2627
</head>
2728
<body>
2829
<xr-site></xr-site>

multiplayer.js

+290
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
const defaultIceServers = [
2+
{'urls': 'stun:stun.stunprotocol.org:3478'},
3+
{'urls': 'stun:stun.l.google.com:19302'},
4+
];
5+
6+
function _randomString() {
7+
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
8+
}
9+
10+
class XRChannelConnection extends EventTarget {
11+
constructor(url) {
12+
super();
13+
14+
this.rtcWs = new WebSocket(url);
15+
this.connectionId = _randomString();
16+
this.peerConnections = [];
17+
18+
this.rtcWs.onopen = () => {
19+
// console.log('presence socket open');
20+
21+
this.rtcWs.send(JSON.stringify({
22+
method: 'init',
23+
connectionId: this.connectionId,
24+
}));
25+
};
26+
const _addPeerConnection = peerConnectionId => {
27+
let peerConnection = this.peerConnections.find(peerConnection => peerConnection.connectionId === peerConnectionId);
28+
if (peerConnection && !peerConnection.open) {
29+
peerConnection.close();
30+
peerConnection = null;
31+
}
32+
if (!peerConnection) {
33+
peerConnection = new XRPeerConnection(peerConnectionId);
34+
this.dispatchEvent(new CustomEvent('peerconnection', {
35+
detail: peerConnection,
36+
}));
37+
peerConnection.addEventListener('close', () => {
38+
const index = this.peerConnections.indexOf(peerConnection);
39+
if (index !== -1) {
40+
this.peerConnections.splice(index, 1);
41+
}
42+
});
43+
peerConnection.peerConnection.onicecandidate = e => {
44+
// console.log('ice candidate', e.candidate);
45+
46+
this.rtcWs.send(JSON.stringify({
47+
dst: peerConnectionId,
48+
src: this.connectionId,
49+
method: 'iceCandidate',
50+
candidate: e.candidate,
51+
}));
52+
};
53+
this.peerConnections.push(peerConnection);
54+
55+
if (this.connectionId < peerConnectionId) {
56+
peerConnection.peerConnection
57+
.createOffer()
58+
.then(offer => {
59+
peerConnection.peerConnection.setLocalDescription(offer);
60+
61+
this.rtcWs.send(JSON.stringify({
62+
dst: peerConnectionId,
63+
src: this.connectionId,
64+
method: 'offer',
65+
offer,
66+
}));
67+
});
68+
}
69+
}
70+
};
71+
const _removePeerConnection = peerConnectionId => {
72+
const index = this.peerConnections.findIndex(peerConnection => peerConnection.connectionId === peerConnectionId);
73+
if (index !== -1) {
74+
this.peerConnections.splice(index, 1)[0].close();
75+
} else {
76+
console.warn('no such peer connection', peerConnectionId, this.peerConnections.map(peerConnection => peerConnection.connectionId));
77+
}
78+
};
79+
this.rtcWs.onmessage = e => {
80+
// console.log('got message', e.data);
81+
82+
const data = JSON.parse(e.data);
83+
const {method} = data;
84+
if (method === 'join') {
85+
const {connectionId: peerConnectionId} = data;
86+
_addPeerConnection(peerConnectionId);
87+
} else if (method === 'offer') {
88+
const {src: peerConnectionId, offer} = data;
89+
90+
const peerConnection = this.peerConnections.find(peerConnection => peerConnection.connectionId === peerConnectionId);
91+
if (peerConnection) {
92+
peerConnection.peerConnection.setRemoteDescription(offer)
93+
.then(() => peerConnection.peerConnection.createAnswer())
94+
.then(answer => {
95+
peerConnection.peerConnection.setLocalDescription(answer);
96+
97+
this.rtcWs.send(JSON.stringify({
98+
dst: peerConnectionId,
99+
src: this.connectionId,
100+
method: 'answer',
101+
answer,
102+
}));
103+
});
104+
} else {
105+
console.warn('no such peer connection', peerConnectionId, this.peerConnections.map(peerConnection => peerConnection.connectionId));
106+
}
107+
} else if (method === 'answer') {
108+
const {src: peerConnectionId, answer} = data;
109+
110+
const peerConnection = this.peerConnections.find(peerConnection => peerConnection.connectionId === peerConnectionId);
111+
if (peerConnection) {
112+
peerConnection.peerConnection.setRemoteDescription(answer);
113+
} else {
114+
console.warn('no such peer connection', peerConnectionId, this.peerConnections.map(peerConnection => peerConnection.connectionId));
115+
}
116+
} else if (method === 'iceCandidate') {
117+
const {src: peerConnectionId, candidate} = data;
118+
119+
const peerConnection = this.peerConnections.find(peerConnection => peerConnection.connectionId === peerConnectionId);
120+
if (peerConnection) {
121+
peerConnection.peerConnection.addIceCandidate(candidate)
122+
.catch(err => {
123+
// console.warn(err);
124+
});
125+
} else {
126+
console.warn('no such peer connection', peerConnectionId, this.peerConnections.map(peerConnection => peerConnection.connectionId));
127+
}
128+
} else if (method === 'leave') {
129+
const {connectionId: peerConnectionId} = data;
130+
_removePeerConnection(peerConnectionId);
131+
} else {
132+
this.dispatchEvent(new MessageEvent('message', {
133+
data: e.data,
134+
}));
135+
}
136+
};
137+
this.rtcWs.onclose = () => {
138+
clearInterval(pingInterval);
139+
console.log('rtc closed');
140+
};
141+
this.rtcWs.onerror = err => {
142+
console.warn('rtc error', err);
143+
clearInterval(pingInterval);
144+
};
145+
const pingInterval = setInterval(() => {
146+
this.rtcWs.send(JSON.stringify({
147+
method: 'ping',
148+
}));
149+
}, 30*1000);
150+
}
151+
152+
disconect() {
153+
this.rtcWs.close();
154+
this.rtcWs = null;
155+
156+
for (let i = 0; i < this.peerConnections[i]; i++) {
157+
this.peerConnections[i].close();
158+
}
159+
this.peerConnections.length = 0;
160+
}
161+
162+
send(s) {
163+
this.rtcWs.send(s);
164+
}
165+
166+
update(hmd, gamepads) {
167+
for (let i = 0; i < this.peerConnections.length; i++) {
168+
const peerConnection = this.peerConnections[i];
169+
if (peerConnection.open) {
170+
peerConnection.update(hmd, gamepads);
171+
}
172+
}
173+
}
174+
}
175+
window.XRChannelConnection = XRChannelConnection;
176+
177+
class XRPeerConnection extends EventTarget {
178+
constructor(peerConnectionId) {
179+
super();
180+
181+
this.connectionId = peerConnectionId;
182+
183+
this.peerConnection = new RTCPeerConnection({
184+
iceServers: defaultIceServers,
185+
});
186+
this.open = false;
187+
188+
this.peerConnection.ontrack = e => {
189+
console.log('got track', e);
190+
};
191+
192+
const sendChannel = this.peerConnection.createDataChannel('sendChannel');
193+
this.peerConnection.sendChannel = sendChannel;
194+
let pingInterval = 0;
195+
sendChannel.onopen = () => {
196+
// console.log('data channel local open');
197+
198+
this.open = true;
199+
this.dispatchEvent(new CustomEvent('open'));
200+
201+
pingInterval = setInterval(() => {
202+
sendChannel.send(JSON.stringify({
203+
method: 'ping',
204+
}));
205+
}, 1000);
206+
};
207+
sendChannel.onclose = () => {
208+
// console.log('data channel local close');
209+
210+
_cleanup();
211+
};
212+
sendChannel.onerror = err => {
213+
// console.log('data channel local error', err);
214+
};
215+
let watchdogTimeout = 0;
216+
const _kick = () => {
217+
if (watchdogTimeout) {
218+
clearTimeout(watchdogTimeout);
219+
watchdogTimeout = 0;
220+
}
221+
watchdogTimeout = setTimeout(() => {
222+
this.peerConnection.close();
223+
}, 5000);
224+
};
225+
_kick();
226+
this.peerConnection.ondatachannel = e => {
227+
const {channel} = e;
228+
// console.log('data channel remote open', channel);
229+
channel.onclose = () => {
230+
// console.log('data channel remote close');
231+
this.peerConnection.close();
232+
};
233+
channel.onerror = err => {
234+
// console.log('data channel remote error', err);
235+
};
236+
channel.onmessage = e => {
237+
// console.log('data channel message', e.data);
238+
239+
const data = JSON.parse(e.data);
240+
const {method} = data;
241+
if (method === 'pose') {
242+
this.dispatchEvent(new CustomEvent('pose', {
243+
detail: data,
244+
}))
245+
} else if (method === 'ping') {
246+
// nothing
247+
} else {
248+
this.dispatchEvent(new MessageEvent('message', {
249+
data,
250+
}));
251+
}
252+
253+
_kick();
254+
};
255+
this.peerConnection.recvChannel = channel;
256+
};
257+
this.peerConnection.close = (close => function() {
258+
_cleanup();
259+
260+
return close.apply(this, arguments);
261+
})(this.peerConnection.close);
262+
const _cleanup = () => {
263+
if (this.open) {
264+
this.open = false;
265+
this.dispatchEvent(new CustomEvent('close'));
266+
}
267+
if (pingInterval) {
268+
clearInterval(pingInterval);
269+
pingInterval = 0;
270+
}
271+
};
272+
}
273+
274+
close() {
275+
this.peerConnection.close();
276+
}
277+
278+
send(s) {
279+
this.peerConnection.sendChannel.send(s);
280+
}
281+
282+
update(hmd, gamepads) {
283+
this.send(JSON.stringify({
284+
method: 'pose',
285+
hmd,
286+
gamepads,
287+
}));
288+
}
289+
}
290+
window.XRPeerConnection = XRPeerConnection;

0 commit comments

Comments
 (0)