Multi-user VR Office Space Using A-Frame And NAF

Image for post
Image for post
IN PROGRESS: Virtual Office in a virtual landscape

PLEASE NOTE THIS ARTICLE IS INCOMPLETE AT THIS TIME (may have errors and/or omissions). ATTEMPT AT YOUR OWN RISK UNTIL THIS MESSAGE IS REMOVED. THANK YOU.

Super Advanced Article — not kidding: The idea for this article started off with the need to have multi-user access to a common VR space. There are a multitude of practical applications for this which I won’t go into, but I will choose one, “Virtual Office Space”.

A common place for people to meet, discuss and work together on projects, ideas and things. The idea is simple, because many of us do this regularly, but the technical solution is somewhat complex.

So let’s dive right in!

Here is a example (you’ll need two people or two tabs open to test):

For those who haven’t been following me, I have written a number of technical articles on A-Frame and virtual reality using a relatively new technology called WebXR. These are require to get a basic understanding of how the virtual environment is created. So unfortunately, or fortunately, however you look at it, you should probably start with these first.

So with your new understanding, we will now move forward!

A-Frame has what I would call an auxiliary component called Networked A-Frame or NAF for short. Currently there are two versions on Github, with possibly more coming (and hopefully some bug fixes too). There is also an active channel of developers and users on Slack which can be found under or @ #A-Frame, #networked-aframe.

So here is the story of why there are two NAFs.

Here are the two repositories:

The Original:

The Forked Mozilla:

I was unable to get the Mozilla one working on my windows server setup (sad face). You may have more luck, so don’t let that stop you!

However, for this article I will be using the original, which does have some bugs, but works “sometimes” after proper browser config and a number of refreshes.

My Windows VPS Setup

So here is where it gets technical. My background is in Windows servers, not Linux. So, although I’d probably recommend you setting up your server with a Unix based OS, mine is going to be setup with a Standard Windows Server 2019 VPS, Ok?

You could go cloud as well, as long as you have a dedicated IP address and you’ll need an SSL certificate for the VR to work. We’ll get that for free in a bit.

The are a number of different providers of VPS (Virtual Private Server) on the internet. Shop around (mama told me), and choose one that matches your monthly budget. Mine is a 2 CPU core slice Windows Server 2019 (AMD64) at https://www.interserver.net/ .

Now, domain name. I had an old domain name already, funbit64.com which I was going to use for another project, but instead decided to repurpose as my VR sandbox server domain name. If you purchase one from your host provider, you should be good. If not get an admin to help you to redirect your domain name DNS to the (usually 2) Namesevers of your selected provider.

Now you have your VPS and domain name. Setup you windows server, being sure to harden it securely with at the least all the necessary windows security updates (very important).

Did I say this was going to be easy? No. Your attention to detail is critical in all the following steps.

Setup and configure Remote Desktop to access your new Windows Server VPS. If it’s not enabled already, enable remote desktop on the server. Then set it up on your client machine.

The next part involves setting up the iisnode configuration on the server. Now there are ways to do a reverse proxy for this. Don’t! Why? Because your going to need sockets setup properly and the iisnode approach will do that for you.

Follow this video patiently, which was very helpful in my setup. He is setting the server up for a different Node.js project, but over 90% of the configuration is the same for us. Don’t worry that he is setting up a server 2012 configuration, it works for 2019 as well. I did not have to add an A record, mine was already setup.

STOP about 12:33 minutes into the video, he begins cloning his project into his DATA directory. This is where we will diverge. Instead we will clone our networked-aframe project into a different NAF directory.

Setting up a Windows Server 2019 for IISnode and WebRTC
Image for post
Image for post
NAF directory created under C:\Users\Administrator\NAF

Open a COMMAND prompt window, your path should start at “C:\users\Administrator” and

git clone https://github.com/networked-aframe/networked-aframe.git
rename networked-aframe NAF
cd NAF
rename examples public_html
dir
cd server

Using your favorite file editor on the server, paste, save and replace this file named “easyrtc-server.js” to the /server directory overwriting the existing file.

//
// Example modification to read SSL certificate file and start https:
//
// see: https://stackoverflow.com/questions/42753566/nodejs-load-pfx-certificate-from-file
//
//const https = require('https');
//const fs = require('fs');
//const options = {
// pfx: fs.readFileSync('test/fixtures/test_cert.pfx'),
// passphrase: 'sample'
//};
//https.createServer(options, (req, res) => {
// res.writeHead(200);
// res.end('hello world\n');
//}).listen(8000);
// Load required modules
const https = require("https"); // https server core module
const path = require("path");
const express = require("express"); // web framework external module
const socketIo = require("socket.io"); // web socket external module
const easyrtc = require("open-easyrtc"); // EasyRTC external module
const fs = require('fs'); // file system module
const options = {
pfx: fs.readFileSync('C:/ProgramData/certify/assets/yourdomainname.com/YourCertificateFilename.pfx'), //lets get our SSL certificate (must change when it expires)
//passphrase: 'testifitworks'
};
// Set process name
process.title = "networked-aframe-server";
// Get port or default to 3025
const port = process.env.PORT || 3025;
// Setup and configure Express http server.
const app = express();
app.use(express.static(path.resolve(__dirname, "..", "public_html")));
// Serve the example and build the bundle in development.
if (process.env.NODE_ENV === "development") {
const webpackMiddleware = require("webpack-dev-middleware");
const webpack = require("webpack");
const config = require("../webpack.dev");
app.use(
webpackMiddleware(webpack(config), {
publicPath: "/dist/"
})
);
}
// Start Express http server
const webServer = https.createServer(options,app); // we are now using a SSL certificate (see top comments above)
// Start Socket.io so it attaches itself to Express server
const socketServer = socketIo.listen(webServer, {"log level": 1});
const myIceServers = [
{"urls":"stun:stun1.l.google.com:19302"},
{"urls":"stun:stun2.l.google.com:19302"},
// {
// "urls":"turn:[ADDRESS]:[PORT]",
// "username":"[USERNAME]",
// "credential":"[CREDENTIAL]"
// },
// {
// "urls":"turn:[ADDRESS]:[PORT][?transport=tcp]",
// "username":"[USERNAME]",
// "credential":"[CREDENTIAL]"
// }
];
easyrtc.setOption("appIceServers", myIceServers);
easyrtc.setOption("logLevel", "debug");
easyrtc.setOption("demosEnable", false);
// Overriding the default easyrtcAuth listener, only so we can directly access its callback
easyrtc.events.on("easyrtcAuth", (socket, easyrtcid, msg, socketCallback, callback) => {
easyrtc.events.defaultListeners.easyrtcAuth(socket, easyrtcid, msg, socketCallback, (err, connectionObj) => {
if (err || !msg.msgData || !msg.msgData.credential || !connectionObj) {
callback(err, connectionObj);
return;
}
connectionObj.setField("credential", msg.msgData.credential, {"isShared":false});console.log("["+easyrtcid+"] Credential saved!", connectionObj.getFieldValueSync("credential"));callback(err, connectionObj);
});
});
// To test, lets print the credential to the console for every room join!
easyrtc.events.on("roomJoin", (connectionObj, roomName, roomParameter, callback) => {
console.log("["+connectionObj.getEasyrtcid()+"] Credential retrieved!", connectionObj.getFieldValueSync("credential"));
easyrtc.events.defaultListeners.roomJoin(connectionObj, roomName, roomParameter, callback);
});
// Start EasyRTC server
const rtc = easyrtc.listen(app, socketServer, null, (err, rtcRef) => {
console.log("Initiated");
rtcRef.events.on("roomCreate", (appObj, creatorConnectionObj, roomName, roomOptions, callback) => {
console.log("roomCreate fired! Trying to create: " + roomName);
appObj.events.defaultListeners.roomCreate(appObj, creatorConnectionObj, roomName, roomOptions, callback);
});
});
// Listen on port
webServer.listen(port, () => {
console.log("listening on http://localhost:" + port);
});
cd ..

Add this file for the server configuration and save it in the root of the NAF directory C:\users\Adminitrator\NAF\ you renamed as filename “web.config”.

<configuration>
<system.webServer>
<!-- indicates that the index.js file is a node.js application
to be handled by the iisnode module -->
<handlers>
<add name="iisnode" path="/server/easyrtc-server.js" verb="*" modules="iisnode" />
<!-- <add name="iisnode-socketio" path="server-socketio.js" verb="*" modules="iisnode" /> -->
</handlers>

<!-- indicate that all static the URL paths begining with 'socket.io' should be redirected to the index.js node.js applicationn to avoid IIS attempting to serve that content using other handlers (e.g static file handlers) -->
<rewrite>
<rules>
<!-- <rule name="LogFile" patternSyntax="ECMAScript">
<match url="socket.io"/>
<action type="Rewrite" url="server-socketio.js"/>
</rule> -->
<rule name="StaticContent" patternSyntax="Wildcard">
<action type="Rewrite" url="public{REQUEST_URI}" logRewrittenUrl="true" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<match url="*.*" />
</rule>
<rule name="DynamicContent">
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="/server/easyrtc-server.js" logRewrittenUrl="true" />
<match url="/*" />
</rule>
</rules>
</rewrite>
<!--iisnode loggingEnabled="true" /-->
<!-- disable the IIS websocket module to allow node.js to provide its own WeSocket implementatiion -->
<webSocket enabled="false" />
</system.webServer>
<appSettings>
<add key="NODE_ENV" value="production" />
</appSettings>
</configuration>

From the command prompt

npm installnpm audit fixnpm audit fix --force

You may get some errors (usually OK to ignore these). Also at some point you may need to do a BUILD command here (Note: author confirm).

Replace the package.json file in NAF with the contents of this file

{
"name": "NAF",
"version": "1.0.0",
"homepage": ".",
"description": "Networked-Aframe multi-user VR",
"scripts": {
"start": "node ./server/easyrtc-server.js"
},
"author": "Michael McAnally",
"dependencies": {
"networked-aframe": "^0.7.0"
}
}

NOW A FEW THINGS BEFORE WE CONTINUE

So up to this point we have created a NAF directory and modified a few files.

The /server/easyrtc-server.js file is the one we will run in node to act as our secondary webserver handling all the NAF requests on port 3025. This is configured in the file. Also in the file is the SSL certificate you will need to change to your certificate.

Make note of the filename and path stored to your certificate generated in “Certify the Web” application. Replace “YourCertificateFilename.pfx” on aproximately line 31 of the easyrtc-server.js file. This makes sure that the certificate you are using in IIS matches the certificate used in the Node webserver; or you will get an error.

Now continue the video above at time index 15:09 to 17:23. This will basically explain the web.config file you created in the root of the NAF directory.

Image for post
Image for post
node configuration in IIS (Basic Settings)

Now find you iisnode directory here C:\Program Files\iisnode\

Go down into www

Create an empty NAF directory

cd \
cd "Program Files\iisnode"
cd www
md NAF

This is kind of a dummy directory with nothing in it. We will be running the Node.js server as a separate process. But iisnode should pass through on port 3025 and SSL will be returned to the browser on the client side.

Open Internet Information Server (IIS) manager console and select node under the default website. See screenshot above. Choose “Basic Settings” on the right control panel.

Set the Physical Path to:

C:\Program Files\iisnode\www\NAF

The Application Pool should be “Node Web Site”, press OK.

Now finally, let’s launch the node server; open a command prompt:

cd NAF
npm start

You now have a server listening on port 3025. Go to a client browser, off of you server, and type your domain name into the browser URL address using https:// followed by your domain name, followed by a :3025.

Mine is: https://funbit64.com:3025/

You should get something like this . . .

Image for post
Image for post
This means your NAF setup is working

That should do it for server configuration. However now you need the A-frame repositories copied into the public_html directory of your node server.

Copy the following repositories into your public_html directory.

I encountered some problems with the latest a-frame master and NAF. So use the A-Frame V1.1.0:

You’ll also need:

and the following put in the js directory, which you will need to create . . .

https://funbit64.com:3025/js/spawn-in-circle.component.js

Once you have all of the above A-Frame components installed on your server you can use this download zip file of a few more assets, and then modify or use the code below to test out a “basic-audio” environment from which to build further. You should put the html file NAF-audio.html in the public_html folder and server it with https://yourdomain.com:3025/NAF-audio.html from a browser.

<html>
<head>
<meta charset="utf-8">
<title>Dev Example — Networked-Aframe</title>
<meta name="description" content="Dev Example — Networked-Aframe">
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.slim.js"></script>
<script src="/easyrtc/easyrtc.js"></script>
<script src="/build.js"></script>
<script>window.NAF || document.write('<script src="https://unpkg.com/networked-aframe/dist/networked-aframe.min.js">\x3C/script>')</script>
<script src="https://unpkg.com/aframe-randomizer-components@^3.0.1/dist/aframe-randomizer-components.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.5/dist/aframe-particle-system-component.min.js"></script>
<script src="/js/spawn-in-circle.component.js"></script>
</head>
<body>
<a-scene networked-scene="
room: basic-audio;
debug: true;
adapter: easyrtc;
audio: true;
">
<a-assets>
<img id="grid" src="https://img.gs/bbdkhfbzkk/stretch/https://i.imgur.com/25P1geh.png" crossorigin="anonymous">
<img id="sky" src="https://i.imgur.com/WqlqEkq.jpg" crossorigin="anonymous" />
<!-- Templates --><!-- Avatar -->
<template id="avatar-template">
<a-entity class="avatar" networked-audio-source>
<a-sphere class="head"
color="#ffffff"
scale="0.45 0.5 0.4"
></a-sphere>
<a-entity class="face"
position="0 0.05 0"
>
<a-sphere class="eye"
color="#efefef"
position="0.16 0.1 -0.35"
scale="0.12 0.12 0.12"
>
<a-sphere class="pupil"
color="#000"
position="0 0 -1"
scale="0.2 0.2 0.2"
></a-sphere>
</a-sphere>
<a-sphere class="eye"
color="#efefef"
position="-0.16 0.1 -0.35"
scale="0.12 0.12 0.12"
>
<a-sphere class="pupil"
color="#000"
position="0 0 -1"
scale="0.2 0.2 0.2"
></a-sphere>
</a-sphere>
</a-entity>
</a-entity>
</template>
<!-- /Templates -->
</a-assets>
<a-entity id="player"
networked="template:#avatar-template;attachTemplateToLocal:false;"
camera
position="0 1.6 0"
spawn-in-circle="radius:3"
wasd-controls look-controls
>
<a-sphere class="head"
visible="false"
random-color
></a-sphere>
</a-entity>
<a-entity position="0 0 0"
geometry="primitive: plane; width: 10000; height: 10000;" rotation="-90 0 0"
material="src: #grid; repeat: 10000 10000; transparent: true; metalness:0.6; roughness: 0.4; sphericalEnvMap: #sky;"></a-entity>
<a-entity light="color: #ccccff; intensity: 1; type: ambient;" visible=""></a-entity>
<a-entity light="color: #ffaaff; intensity: 1.5" position="5 5 5"></a-entity>
<a-sky src="#sky" rotation="0 -90 0"></a-sky>

</a-scene>
<script>
// On mobile remove elements that are resource heavy
var isMobile = AFRAME.utils.device.isMobile();
if (isMobile) {
var particles = document.getElementById('particles');
particles.parentNode.removeChild(particles);
}
</script>
<script>
// Define custom schema for syncing avatar color, set by random-color
NAF.schemas.add({
template: '#avatar-template',
components: [
'position',
'rotation',
{
selector: '.head',
component: 'material',
property: 'color'
}
]
});
// Called by Networked-Aframe when connected to server
function onConnect () {
console.log("onConnect", new Date());
}
</script>
</body>
</html>

C:\Users\Administrator\NAF\

Go into that directory and execute the command bellow:

npm start

This will start the networked-aframe server for NAF capabilities on port 3025.

Then go into a browser (not on your server) and enter this URL to test:

https://yourdomain.com:3025/NAF-audio.html

Author note (March 3, 2021): continue to finish article after debugging a few more things . . . then remove this message and the one at the top.

Founder, VR coder, Sci-Fi Blogger with ideas for human technological-evolution.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store