From e69ad01e700e80fa959dfee08cd556ca0d777ffd Mon Sep 17 00:00:00 2001 From: skylar Date: Sun, 1 Nov 2026 05:43:24 -0800 Subject: Yesod: self-hosted WebXR rooms Start a room in a world and hang out in VR from the headset's browser: A-Frame + Networked-A-Frame over EasyRTC, with a coturn relay that can hide everyone's IP address. Ships with the Campfire, Temple, Cabin, Balcony and Test room worlds. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 11 + LICENSE | 29 + README.md | 472 +++++ deploy/deploy.env.example | 6 + deploy/deploy.sh | 48 + deploy/env.example | 27 + deploy/nginx-yesod.conf | 41 + deploy/turnserver.conf | 64 + deploy/yesod.service | 37 + package-lock.json | 1231 +++++++++++++ package.json | 28 + public/assets/aframe/fonts/Roboto-msdf.json | 1 + public/assets/aframe/fonts/Roboto-msdf.png | Bin 0 -> 80743 bytes public/assets/avatars/astronaut-portrait.webp | Bin 0 -> 5748 bytes public/assets/avatars/astronaut.vrm | Bin 0 -> 1679188 bytes public/assets/avatars/bigbro-portrait.webp | Bin 0 -> 3168 bytes public/assets/avatars/bigbro.vrm | Bin 0 -> 1293092 bytes public/assets/avatars/froggy-portrait.webp | Bin 0 -> 7236 bytes public/assets/avatars/froggy.vrm | Bin 0 -> 1828632 bytes public/assets/avatars/lilbro-portrait.webp | Bin 0 -> 3196 bytes public/assets/avatars/lilbro.vrm | Bin 0 -> 1366204 bytes public/assets/avatars/oldmoustache-portrait.webp | Bin 0 -> 5556 bytes public/assets/avatars/oldmoustache.vrm | Bin 0 -> 1264888 bytes public/assets/avatars/polybot-portrait.webp | Bin 0 -> 6386 bytes public/assets/avatars/polybot.vrm | Bin 0 -> 997048 bytes public/assets/avatars/teddy-portrait.webp | Bin 0 -> 4330 bytes public/assets/avatars/teddy.vrm | Bin 0 -> 1657132 bytes public/assets/avatars/wizzir-portrait.webp | Bin 0 -> 5994 bytes public/assets/avatars/wizzir.vrm | Bin 0 -> 1700172 bytes public/assets/chess/CREDITS.txt | 3 + public/assets/chess/bishop.glb | Bin 0 -> 205004 bytes public/assets/chess/king.glb | Bin 0 -> 132608 bytes public/assets/chess/knight.glb | Bin 0 -> 242824 bytes public/assets/chess/pawn.glb | Bin 0 -> 90780 bytes public/assets/chess/queen.glb | Bin 0 -> 119864 bytes public/assets/chess/rook.glb | Bin 0 -> 124988 bytes public/assets/env/LICENSE-kenney-nature-kit.txt | 23 + public/assets/env/campfire_logs.glb | Bin 0 -> 9284 bytes public/assets/env/campfire_stones.glb | Bin 0 -> 17344 bytes public/assets/env/canoe.glb | Bin 0 -> 15780 bytes public/assets/env/flower_purpleA.glb | Bin 0 -> 7112 bytes public/assets/env/flower_redA.glb | Bin 0 -> 7104 bytes public/assets/env/flower_yellowA.glb | Bin 0 -> 7120 bytes public/assets/env/grass.glb | Bin 0 -> 11496 bytes public/assets/env/grass_large.glb | Bin 0 -> 18504 bytes public/assets/env/lily_large.glb | Bin 0 -> 8044 bytes public/assets/env/lily_small.glb | Bin 0 -> 5664 bytes public/assets/env/log.glb | Bin 0 -> 14288 bytes public/assets/env/log_stack.glb | Bin 0 -> 10896 bytes public/assets/env/mushroom_redGroup.glb | Bin 0 -> 14960 bytes public/assets/env/mushroom_tanGroup.glb | Bin 0 -> 14956 bytes public/assets/env/plant_bush.glb | Bin 0 -> 4396 bytes public/assets/env/plant_bushDetailed.glb | Bin 0 -> 10172 bytes public/assets/env/plant_bushLarge.glb | Bin 0 -> 6436 bytes public/assets/env/rock_largeA.glb | Bin 0 -> 7552 bytes public/assets/env/rock_largeC.glb | Bin 0 -> 7004 bytes public/assets/env/rock_tallB.glb | Bin 0 -> 14060 bytes public/assets/env/rock_tallE.glb | Bin 0 -> 5192 bytes public/assets/env/stump_old.glb | Bin 0 -> 9564 bytes public/assets/env/stump_round.glb | Bin 0 -> 5644 bytes public/assets/env/tent_detailedOpen.glb | Bin 0 -> 15324 bytes public/assets/env/tent_smallClosed.glb | Bin 0 -> 13200 bytes public/assets/env/tree_pineDefaultA.glb | Bin 0 -> 17220 bytes public/assets/env/tree_pineDefaultB.glb | Bin 0 -> 18180 bytes public/assets/env/tree_pineRoundA.glb | Bin 0 -> 14488 bytes public/assets/env/tree_pineRoundC.glb | Bin 0 -> 15332 bytes public/assets/env/tree_pineRoundE.glb | Bin 0 -> 15308 bytes public/assets/env/tree_pineSmallA.glb | Bin 0 -> 12820 bytes public/assets/env/tree_pineTallA.glb | Bin 0 -> 7200 bytes public/assets/env/tree_pineTallB.glb | Bin 0 -> 7204 bytes public/assets/env/tree_pineTallC.glb | Bin 0 -> 8080 bytes public/assets/env/tree_pineTallD.glb | Bin 0 -> 8080 bytes public/assets/hands/leftHandLow.glb | Bin 0 -> 125180 bytes public/assets/hands/rightHandLow.glb | Bin 0 -> 125160 bytes public/assets/sky/milkyway-4k.jpg | Bin 0 -> 2518501 bytes public/assets/sky/stars.bin | Bin 0 -> 145536 bytes public/assets/sounds/chess-1.ogg | Bin 0 -> 1397 bytes public/assets/sounds/chess-2.ogg | Bin 0 -> 1055 bytes public/assets/sounds/chess-3.ogg | Bin 0 -> 869 bytes public/assets/sounds/fire.ogg | Bin 0 -> 353401 bytes public/assets/sounds/owl-barred.ogg | Bin 0 -> 35774 bytes public/assets/sounds/owl-great-horned-1.ogg | Bin 0 -> 20781 bytes public/assets/sounds/owl-great-horned-2.ogg | Bin 0 -> 20949 bytes public/assets/sounds/steps-leaves.ogg | Bin 0 -> 69957 bytes public/assets/sounds/steps-twigs.ogg | Bin 0 -> 57590 bytes public/assets/sounds/twig-1.ogg | Bin 0 -> 7552 bytes public/assets/sounds/twig-2.ogg | Bin 0 -> 22951 bytes public/assets/sounds/wind.ogg | Bin 0 -> 569568 bytes public/assets/temple/CREDITS.txt | 6 + public/assets/temple/stele.webp | Bin 0 -> 314822 bytes public/assets/worlds/balcony.webp | Bin 0 -> 11558 bytes public/assets/worlds/cabin.webp | Bin 0 -> 19700 bytes public/assets/worlds/campfire.webp | Bin 0 -> 13816 bytes public/assets/worlds/temple.webp | Bin 0 -> 12808 bytes public/assets/worlds/testroom.webp | Bin 0 -> 1826 bytes public/index.html | 317 ++++ public/js/app.js | 652 +++++++ public/js/astro.js | 248 +++ public/js/components/ambience.js | 130 ++ public/js/components/balcony.js | 592 +++++++ public/js/components/cabin.js | 1064 ++++++++++++ public/js/components/campsite.js | 479 ++++++ public/js/components/chess.js | 742 ++++++++ public/js/components/hand-tilt.js | 37 + public/js/components/locomotion.js | 312 ++++ public/js/components/nametag.js | 69 + public/js/components/player-info.js | 16 + public/js/components/screens.js | 483 ++++++ public/js/components/sky.js | 498 ++++++ public/js/components/temple.js | 1999 ++++++++++++++++++++++ public/js/components/testroom.js | 72 + public/js/components/voice-level.js | 39 + public/js/components/vrm-avatar.js | 486 ++++++ public/js/components/wield.js | 629 +++++++ public/js/schemas.js | 32 + public/js/share.js | 267 +++ public/js/world.js | 22 + public/vendor/aframe-1.7.1.min.js | 2 + public/vendor/networked-aframe-0.14.3.min.js | 1 + public/worlds.json | 51 + server.js | 344 ++++ tools/portraits/render.html | 85 + tools/portraits/server.ts | 27 + tools/sky/astro_test.js | 76 + tools/sky/build-milkyway.py | 41 + tools/sky/build-stars.py | 38 + tools/sounds/build-sounds.py | 107 ++ 127 files changed, 11984 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deploy/deploy.env.example create mode 100755 deploy/deploy.sh create mode 100644 deploy/env.example create mode 100644 deploy/nginx-yesod.conf create mode 100644 deploy/turnserver.conf create mode 100644 deploy/yesod.service create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/assets/aframe/fonts/Roboto-msdf.json create mode 100644 public/assets/aframe/fonts/Roboto-msdf.png create mode 100644 public/assets/avatars/astronaut-portrait.webp create mode 100644 public/assets/avatars/astronaut.vrm create mode 100644 public/assets/avatars/bigbro-portrait.webp create mode 100644 public/assets/avatars/bigbro.vrm create mode 100644 public/assets/avatars/froggy-portrait.webp create mode 100644 public/assets/avatars/froggy.vrm create mode 100644 public/assets/avatars/lilbro-portrait.webp create mode 100644 public/assets/avatars/lilbro.vrm create mode 100644 public/assets/avatars/oldmoustache-portrait.webp create mode 100644 public/assets/avatars/oldmoustache.vrm create mode 100644 public/assets/avatars/polybot-portrait.webp create mode 100644 public/assets/avatars/polybot.vrm create mode 100644 public/assets/avatars/teddy-portrait.webp create mode 100644 public/assets/avatars/teddy.vrm create mode 100644 public/assets/avatars/wizzir-portrait.webp create mode 100644 public/assets/avatars/wizzir.vrm create mode 100644 public/assets/chess/CREDITS.txt create mode 100644 public/assets/chess/bishop.glb create mode 100644 public/assets/chess/king.glb create mode 100644 public/assets/chess/knight.glb create mode 100644 public/assets/chess/pawn.glb create mode 100644 public/assets/chess/queen.glb create mode 100644 public/assets/chess/rook.glb create mode 100644 public/assets/env/LICENSE-kenney-nature-kit.txt create mode 100644 public/assets/env/campfire_logs.glb create mode 100644 public/assets/env/campfire_stones.glb create mode 100644 public/assets/env/canoe.glb create mode 100644 public/assets/env/flower_purpleA.glb create mode 100644 public/assets/env/flower_redA.glb create mode 100644 public/assets/env/flower_yellowA.glb create mode 100644 public/assets/env/grass.glb create mode 100644 public/assets/env/grass_large.glb create mode 100644 public/assets/env/lily_large.glb create mode 100644 public/assets/env/lily_small.glb create mode 100644 public/assets/env/log.glb create mode 100644 public/assets/env/log_stack.glb create mode 100644 public/assets/env/mushroom_redGroup.glb create mode 100644 public/assets/env/mushroom_tanGroup.glb create mode 100644 public/assets/env/plant_bush.glb create mode 100644 public/assets/env/plant_bushDetailed.glb create mode 100644 public/assets/env/plant_bushLarge.glb create mode 100644 public/assets/env/rock_largeA.glb create mode 100644 public/assets/env/rock_largeC.glb create mode 100644 public/assets/env/rock_tallB.glb create mode 100644 public/assets/env/rock_tallE.glb create mode 100644 public/assets/env/stump_old.glb create mode 100644 public/assets/env/stump_round.glb create mode 100644 public/assets/env/tent_detailedOpen.glb create mode 100644 public/assets/env/tent_smallClosed.glb create mode 100644 public/assets/env/tree_pineDefaultA.glb create mode 100644 public/assets/env/tree_pineDefaultB.glb create mode 100644 public/assets/env/tree_pineRoundA.glb create mode 100644 public/assets/env/tree_pineRoundC.glb create mode 100644 public/assets/env/tree_pineRoundE.glb create mode 100644 public/assets/env/tree_pineSmallA.glb create mode 100644 public/assets/env/tree_pineTallA.glb create mode 100644 public/assets/env/tree_pineTallB.glb create mode 100644 public/assets/env/tree_pineTallC.glb create mode 100644 public/assets/env/tree_pineTallD.glb create mode 100644 public/assets/hands/leftHandLow.glb create mode 100644 public/assets/hands/rightHandLow.glb create mode 100644 public/assets/sky/milkyway-4k.jpg create mode 100644 public/assets/sky/stars.bin create mode 100644 public/assets/sounds/chess-1.ogg create mode 100644 public/assets/sounds/chess-2.ogg create mode 100644 public/assets/sounds/chess-3.ogg create mode 100644 public/assets/sounds/fire.ogg create mode 100644 public/assets/sounds/owl-barred.ogg create mode 100644 public/assets/sounds/owl-great-horned-1.ogg create mode 100644 public/assets/sounds/owl-great-horned-2.ogg create mode 100644 public/assets/sounds/steps-leaves.ogg create mode 100644 public/assets/sounds/steps-twigs.ogg create mode 100644 public/assets/sounds/twig-1.ogg create mode 100644 public/assets/sounds/twig-2.ogg create mode 100644 public/assets/sounds/wind.ogg create mode 100644 public/assets/temple/CREDITS.txt create mode 100644 public/assets/temple/stele.webp create mode 100644 public/assets/worlds/balcony.webp create mode 100644 public/assets/worlds/cabin.webp create mode 100644 public/assets/worlds/campfire.webp create mode 100644 public/assets/worlds/temple.webp create mode 100644 public/assets/worlds/testroom.webp create mode 100644 public/index.html create mode 100644 public/js/app.js create mode 100644 public/js/astro.js create mode 100644 public/js/components/ambience.js create mode 100644 public/js/components/balcony.js create mode 100644 public/js/components/cabin.js create mode 100644 public/js/components/campsite.js create mode 100644 public/js/components/chess.js create mode 100644 public/js/components/hand-tilt.js create mode 100644 public/js/components/locomotion.js create mode 100644 public/js/components/nametag.js create mode 100644 public/js/components/player-info.js create mode 100644 public/js/components/screens.js create mode 100644 public/js/components/sky.js create mode 100644 public/js/components/temple.js create mode 100644 public/js/components/testroom.js create mode 100644 public/js/components/voice-level.js create mode 100644 public/js/components/vrm-avatar.js create mode 100644 public/js/components/wield.js create mode 100644 public/js/schemas.js create mode 100644 public/js/share.js create mode 100644 public/js/world.js create mode 100644 public/vendor/aframe-1.7.1.min.js create mode 100644 public/vendor/networked-aframe-0.14.3.min.js create mode 100644 public/worlds.json create mode 100644 server.js create mode 100644 tools/portraits/render.html create mode 100644 tools/portraits/server.ts create mode 100644 tools/sky/astro_test.js create mode 100644 tools/sky/build-milkyway.py create mode 100644 tools/sky/build-stars.py create mode 100644 tools/sounds/build-sounds.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e983519 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +deno.lock +.passphrase* +tools/portraits/out/ + +# This server's own deploy settings and notes. +deploy/deploy.env +*.local.md + +# Unused, and its source isn't recorded. +public/assets/sky/night-sky-4k.jpg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..674c60e --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +MIT License + +Copyright (c) 2026 The Yesod contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- + +This license covers Yesod's own code. The libraries in public/vendor/ and the +models, images, sounds and data in public/assets/ keep their own licenses, +listed under "Credits" in README.md and in the CREDITS and LICENSE files next +to them. Most are CC0 or public domain; the great horned owl clips +(public/assets/sounds/owl-great-horned-*.ogg) are CC BY-SA 4.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ed8f53 --- /dev/null +++ b/README.md @@ -0,0 +1,472 @@ +# Yesod + +Small self-hosted VR rooms. Anyone can start a room in a world, and a few +people can sit around and talk. It runs in the headset's web browser +(WebXR), so there's nothing to install on a Quest, and everything, including +every library and model, is served from your own server. Nothing is logged +about visitors, and a relay can hide everyone's IP address from the others. + +The name is the sphere of the Tree of Life that holds the astral plane: the +world of images, where people meet without their bodies. + +It comes with five worlds: + +- **Campfire:** a pine clearing under the real night sky. +- **Temple:** a Thelemic temple with the Gnostic Mass hall and the Book 4 + chamber. In VR, reach your hand to the Veil and a label above your + hand offers to open or close it with the trigger; on desktop, press V. It + opens and closes for everyone in the room. You can pick up the Wand and the + Sword from the chamber's altar and draw in the air with them, like the pens + in VRChat. +- **Cabin:** a log cabin by a lake: a fire in a stone hearth, a chess table + on a hexagram rug with an armchair at each side and a third by the fire, a + bookshelf, and a big window where the sky holds the real Moon (true phase, + drawn 2.5× its real size) over the water. In the window-side corner a lantern + hangs over a magick circle and triangle after Lon Milo DuQuette's "Modern + Circle" (Book 4's Tau, diamonds and nine lamps, with the names NVIT, HADIT, + RA HOOR KHVIT, CHAOS, BABALON, LAYLAH and PERDVRABO), scaled down from 9 ft + to about 1.7 m. A black and a white wand lie on the windowsill for drawing + in the air, and on desktop V resets the chess board. +- **Balcony:** a balcony 40 storeys up a glass tower, over a big city on an + overcast, foggy day: two chairs at a little table, a bench, and planters. + The city is plain boxes painted with window textures; the fog and two + drifting cloud layers (one hiding the street, one swallowing the tallest + towers) keep it moody and cheap to draw, and red aircraft lights blink on + the roofs. Wind plays in the background. +- **Test room:** a plain white room for trying things out, with a chess board + on its table (see [Chess](#chess)). + +## Starting and joining rooms + +The first page lists the open rooms: which world, who started it, how long +it's been going, and how many people are in it (a lock means it has a +password). Tap **Join** on one, or **Start a room**: + +- **World:** which place to go. New worlds are listed in `public/worlds.json`. +- **Open room:** listed on the first page for anyone to join. An optional + password keeps it to people you tell; the room is still listed, with a lock. +- **Just me:** the world runs on your device only. Nothing is listed, nothing + connects to the server after the page and world have downloaded, and the + mic isn't used. + +Then pick a name, an avatar and a color, allow the microphone, and tap +**Enter VR**. **Copy link** in a room copies a link straight to its join +screen (the password isn't in it). Each browser remembers your name, avatar, +color, world, choice of room type and the setting below. + +### Sharing a screen + +When joining or starting an open room from a computer, **Join as → My screen** +puts what's on that computer into the room instead of you: no avatar or mic, +just a big screen everyone sees, labeled with your name. The browser asks which +screen, window or tab to show, and the next page shows what's going out, lets +you share something else or stop, and picks the sound. Several people can each +share a screen at once. + +In the room, anyone can move a screen. In VR, point a hand at it and squeeze +grip; it follows your hand at that distance, upright and turned toward you. +Squeeze the other grip too and move your hands apart or together to resize it. +On desktop, look at it and press **G**, scroll to resize, and **G** again to +put it down. Everyone sees where it is, newcomers included. + +Sound depends on the browser doing the sharing: + +| Browser | Sound | +|---|---| +| Chrome, Edge, Chromium, Brave | A shared tab's sound (tick **Also share tab audio**). On Windows, sharing the whole screen can include all the computer's sound. | +| Firefox, Safari (any OS) | None from sharing itself, so pick a sound input on the next page instead. | + +Any browser can use a sound input. On Linux (PipeWire or PulseAudio), choose +**Monitor of …** for everything the computer plays; in Firefox you can pick it +right in the permission prompt. On Windows, enable **Stereo Mix** (Sound +settings → Recording) or install a virtual cable; on a Mac, a loopback device +like BlackHole. Don't also join the room as yourself from the same computer +while sharing its sound that way, or people hear themselves. + +The screen goes out over the same WebRTC connections as voice, so it never +touches the server. The relay carries it when **Hide my IP address** is on, +using about 3 Mbps per viewer at most (1080p, 30 fps), plus 128 kbps stereo +sound. The sharer's upload is that times the number of viewers. + +**Hide my IP address** (on by default) sends your voice and movement through +the relay on your server, so others in the room only ever see the server's address. +Turning it off lets your headset connect straight to the others when it can, +with slightly less delay, but then they can see your IP address. It's +per person: someone who turns it off reveals only their own address. The +setting only appears when the server has a relay configured. + +A room closes a minute after the last person leaves, so reloading the page +brings you back to the same room. A room nobody enters closes after two +minutes. + +| | In VR | On desktop | +|---|---|---| +| Walk | Left stick | WASD / arrows | +| Turn | Right stick (30° snaps) | Q / E, or drag to look | +| Jump | A | Space | +| Sit / stand | X | C | +| Mute | Y or B | M, or the Mic button | +| Pick up / let go (Temple) | Hold grip near the Wand or Sword / release grip | F | +| Draw | Trigger, while holding | Hold the mouse button | +| Erase | Touch a line with an empty hand and pull the trigger | Backspace erases your last line | +| Move a chess piece (Test room) | Hold grip near it, let go over a square | F on it, look at a square, F again | +| Set up the chess board again | Reach for a red button on the table, trigger twice | V | + +In VR the Wand or Sword goes into your hand by its hilt, and when you let go +it floats right where it was until someone picks it up again. Lines come from +the tip: gold from the Wand, pale blue from the Sword. Everyone in the room +sees who's holding what and every line, newcomers are caught up, and no one +can take a weapon out of someone else's hand. A room keeps its 300 newest +lines (100,000 points); the oldest go first. + +### Chess + +The board doesn't know the rules; you move the pieces yourself, like a real +board. The piece your hand would pick up glows, and while you hold one over the +board the square under it lights up, red if there's a piece there already. Let +go and the piece stands in the middle of that square, and the piece that was +there goes off to the side of the board (black's on white's right, white's on +black's). Let go anywhere else and it drops onto the table, or the floor. On +desktop the held piece hovers over wherever you look. Everyone sees the same +board, newcomers included. White is at the end of the table where you arrive. +It's all in `public/js/components/chess.js`; a world adds it with +`` on its tabletop. The Test room and the Cabin +each have one; the Cabin builds its table and chairs around wherever the board +is put. + +Your left wrist shows whether your mic is on and who's here. Everyone else +sees your avatar follow your head and reach for your hands, with your finger +gestures (point, fist, thumbs-up) from the controller buttons. The mouth moves +when you talk, voices come from where people stand, and a dot under each name +glows while that person speaks. + +Sitting puts you on the nearest free log or stump (within about 1.6 m), +facing the fire; each log seats two. Your view settles to seated height +whether you're standing or already sitting at home, worked out from your +avatar's proportions so its hips land on the seat. Others see it sit with its +feet on the ground, leaning when you lean. Walking, jumping, or stepping more +than 0.6 m away in your room stands you up. A jump is a quick hop of about +half a metre, and others see your avatar's knees tuck up. + +## How it works + +``` +Quest browser ──HTTPS──▶ nginx /vr/ ──▶ yesod (Node, 127.0.0.1:8095) + │ • static files + │ • EasyRTC signaling over socket.io + │ • the list of open rooms, room passwords + │ • mints TURN credentials + └──WebRTC voice, avatar data, shared screens──▶ other headsets directly, + or via coturn (UDP 3478, relay 49160–49999) +``` + +- **Client:** [A-Frame](https://aframe.io) 1.7.1 with [Networked-A-Frame](https://github.com/networked-aframe/networked-aframe) 0.14.3 (easyrtc adapter, positional audio). Every connection's session description allows Opus stereo at up to 128 kbps (`stereoOpus` in `app.js`), so a shared screen's sound isn't squeezed into mono voice quality; voices use a little more bandwidth as a result. +- **Server:** `server.js`, using Express 4, socket.io and open-easyrtc. It keeps the open rooms in memory (nothing is written to disk), and each EasyRTC connection may only join the room it signed in to, with that room's password. After 10 wrong passwords in 10 minutes, that IP is locked out for the rest of the window. Room passwords ignore case and surrounding spaces. +- **Relay:** coturn with `use-auth-secret`, so each person gets their own TURN username and a credential that lasts 12 hours. It never relays into private or loopback addresses. With **Hide my IP address** on, the browser is set to `iceTransportPolicy: 'relay'`, so it only ever offers the relay's address. Each person then holds one relay allocation (and one UDP port) per other person in the room; `deploy/turnserver.conf` allows 450 in total and 10 per person, which covers the app's 60-person cap. + +### Limits + +The constants at the top of `server.js` keep a public server from swamping +a small VPS. The first four can be set in the environment +([Settings](#settings)): + +| Limit | Value | Why | +|---|---|---| +| People per room | 8 | Everyone streams voice and movement to everyone else, so a headset handles 7 others comfortably | +| Open rooms | 10 | | +| People in rooms, all together | 60 | | +| People from one IP address | 6 | Enough for a household | +| Open rooms started from one IP | 2 | | +| Rooms started per IP per 10 minutes | 6 | | + +Measured locally with 56 simulated people in 8 rooms, the server's memory +stayed at about 92 MB, the same as idle: the server only relays the brief +signaling that sets up each connection, so it's the relay (coturn) that grows with use. +Its memory per allocation is small (a few hundred allocations is a few tens of +MB); CPU and bandwidth go up with every person relayed. + +### Code map + +| File | What it does | +|---|---| +| `server.js` | HTTP, the room list and room API, passwords and limits, EasyRTC auth, ICE/TURN config | +| `public/worlds.json` | The worlds you can pick: name, line of description, thumbnail, fog, and an optional action and shared state | +| `public/js/world.js` | `CurrentWorld()`: finds the loaded world's component (`data-world`), which locomotion and avatars ask for seats, spawn, bounds and floor height | +| `public/index.html` | Lobby and HUD (monospace, centered, `---+---` rules; small inline CSS), the scene, the networked templates, and each world's `