import "./assets/stylesheets/hub.scss"; import moment from "moment-timezone"; import uuid from "uuid/v4"; import queryString from "query-string"; import { Socket } from "phoenix"; import { patchWebGLRenderingContext } from "./utils/webgl"; patchWebGLRenderingContext(); import "aframe-xr"; import "./vendor/GLTFLoader"; import "networked-aframe"; import "naf-janus-adapter"; import "aframe-teleport-controls"; import "aframe-input-mapping-component"; import "aframe-billboard-component"; import "aframe-rounded"; import "webrtc-adapter"; import trackpad_dpad4 from "./behaviours/trackpad-dpad4"; import joystick_dpad4 from "./behaviours/joystick-dpad4"; import { PressedMove } from "./activators/pressedmove"; import { ReverseY } from "./activators/reversey"; import "./activators/shortpress"; import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future import "./components/mute-mic"; import "./components/audio-feedback"; import "./components/bone-mute-state-indicator"; import "./components/bone-visibility"; import "./components/in-world-hud"; import "./components/virtual-gamepad-controls"; import "./components/ik-controller"; import "./components/hand-controls2"; import "./components/character-controller"; import "./components/haptic-feedback"; import "./components/networked-video-player"; import "./components/offset-relative-to"; import "./components/water"; import "./components/skybox"; import "./components/layers"; import "./components/spawn-controller"; import "./components/hide-when-quality"; import "./components/player-info"; import "./components/debug"; import "./components/animation-mixer"; import "./components/loop-animation"; import "./components/hand-poses"; import "./components/gltf-model-plus"; import "./components/gltf-bundle"; import "./components/hud-controller"; import "./components/stats-plus"; import ReactDOM from "react-dom"; import React from "react"; import UIRoot from "./react-components/ui-root"; import HubChannel from "./utils/hub-channel"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; import "./systems/exit-on-blur"; import "./gltf-component-mappings"; import { App } from "./App"; window.APP = new App(); const qs = queryString.parse(location.search); const isMobile = AFRAME.utils.device.isMobile(); if (qs.quality) { window.APP.quality = qs.quality; } else { window.APP.quality = isMobile ? "low" : "high"; } import "aframe-physics-system"; import "aframe-physics-extras"; import "aframe-extras/src/pathfinding"; import "super-hands"; import "./components/super-networked-interactable"; import "./components/networked-counter"; import "./components/super-spawner"; import "./components/super-cursor"; import "./components/event-repeater"; import "./components/nav-mesh-helper"; import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; import Store from "./storage/store"; import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; function qsTruthy(param) { const val = qs[param]; // if the param exists but is not set (e.g. "?foo&bar"), its value is null. return val === null || /1|on|true/i.test(val); } registerTelemetry(); AFRAME.registerInputBehaviour("trackpad_dpad4", trackpad_dpad4); AFRAME.registerInputBehaviour("joystick_dpad4", joystick_dpad4); AFRAME.registerInputActivator("pressedmove", PressedMove); AFRAME.registerInputActivator("reverseY", ReverseY); AFRAME.registerInputMappings(inputConfig, true); const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); const hubChannel = new HubChannel(store); concurrentLoadDetector.start(); // Always layer in any new default profile bits store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); // Regenerate name to encourage users to change it. if (!store.state.profile.has_changed_name) { store.update({ profile: { display_name: generateRandomName() } }); } async function exitScene() { if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop()); } hubChannel.disconnect(); const scene = document.querySelector("a-scene"); scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this document.body.removeChild(scene); } function applyProfileFromStore(playerRig) { const displayName = store.state.profile.display_name; playerRig.setAttribute("player-info", { displayName, avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") }); document.querySelector("a-scene").emit("username-changed", { username: displayName }); } async function enterScene(mediaStream, enterInVR, janusRoomId) { const scene = document.querySelector("a-scene"); const playerRig = document.querySelector("#player-rig"); document.querySelector("a-scene canvas").classList.remove("blurred"); scene.render(); scene.setAttribute("stats-plus", false); if (enterInVR) { scene.enterVR(); } AFRAME.registerInputActions(inGameActions, "default"); document.querySelector("#player-camera").setAttribute("look-controls", ""); scene.setAttribute("networked-scene", { room: janusRoomId, serverURL: process.env.JANUS_SERVER }); if (isMobile || qsTruthy("mobile")) { playerRig.setAttribute("virtual-gamepad-controls", {}); } const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); applyProfileOnPlayerRig(); store.addEventListener("statechanged", applyProfileOnPlayerRig); const avatarScale = parseInt(qs.avatar_scale, 10); if (avatarScale) { playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); } const videoTracks = mediaStream.getVideoTracks(); let sharingScreen = videoTracks.length > 0; const screenEntityId = `${NAF.clientId}-screen`; let screenEntity = document.getElementById(screenEntityId); scene.addEventListener("action_share_screen", () => { sharingScreen = !sharingScreen; if (sharingScreen) { for (const track of videoTracks) { mediaStream.addTrack(track); } } else { for (const track of mediaStream.getVideoTracks()) { mediaStream.removeTrack(track); } } NAF.connection.adapter.setLocalMediaStream(mediaStream); screenEntity.setAttribute("visible", sharingScreen); }); if (!qsTruthy("offline")) { document.body.addEventListener("connected", () => { hubChannel.sendEntryEvent().then(() => { store.update({ lastEnteredAt: moment().toJSON() }); }); }); scene.components["networked-scene"].connect(); if (mediaStream) { NAF.connection.adapter.setLocalMediaStream(mediaStream); if (screenEntity) { screenEntity.setAttribute("visible", sharingScreen); } else if (sharingScreen) { const sceneEl = document.querySelector("a-scene"); screenEntity = document.createElement("a-entity"); screenEntity.id = screenEntityId; screenEntity.setAttribute("offset-relative-to", { target: "#player-camera", offset: "0 0 -2", on: "action_share_screen" }); screenEntity.setAttribute("networked", { template: "#video-template" }); sceneEl.appendChild(screenEntity); } } } } function mountUI(scene, props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.vr_entry_type || null; const enableScreenSharing = qsTruthy("enable_screen_sharing"); const htmlPrefix = document.body.dataset.htmlPrefix || ""; const showProfileEntry = !store.state.profile.has_changed_name; ReactDOM.render( <UIRoot {...{ scene, enterScene, exitScene, concurrentLoadDetector, disableAutoExitOnConcurrentLoad, forcedVREntryType, enableScreenSharing, store, htmlPrefix, showProfileEntry, ...props }} />, document.getElementById("ui-root") ); } const onReady = async () => { const scene = document.querySelector("a-scene"); document.querySelector("a-scene canvas").classList.add("blurred"); window.APP.scene = scene; registerNetworkSchemas(); mountUI(scene); let modifiedProps = {}; const remountUI = props => { modifiedProps = { ...modifiedProps, ...props }; mountUI(scene, modifiedProps); }; getAvailableVREntryTypes().then(availableVREntryTypes => { remountUI({ availableVREntryTypes }); }); const environmentRoot = document.querySelector("#environment-root"); const initialEnvironmentEl = document.createElement("a-entity"); initialEnvironmentEl.addEventListener("bundleloaded", () => { remountUI({ initialEnvironmentLoaded: true }); // Wait a tick plus some margin so that the environments actually render. setTimeout(() => scene.renderer.animate(null), 100); }); environmentRoot.appendChild(initialEnvironmentEl); if (qs.room) { // If ?room is set, this is `yarn start`, so just use a default environment and query string room. remountUI({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 }); initialEnvironmentEl.setAttribute("gltf-bundle", { src: "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json" // src: "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json" // src: "https://asset-bundles-prod.reticulum.io/rooms/atrium/AtriumMeshes.bundle.json" // src: "https://asset-bundles-prod.reticulum.io/rooms/courtyard/CourtyardMeshes.bundle.json" }); return; } // Connect to reticulum over phoenix channels to get hub info. const hubId = qs.hub_id || document.location.pathname.substring(1).split("/")[0]; console.log(`Hub ID: ${hubId}`); const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:"; const socketPort = qs.phx_port || (process.env.NODE_ENV === "production" ? document.location.port : 443); const socketHost = qs.phx_host || (process.env.NODE_ENV === "production" ? document.location.hostname : process.env.DEV_RETICULUM_SERVER); const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; console.log(`Phoenix Channel URL: ${socketUrl}`); const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); socket.connect(); const channel = socket.channel(`hub:${hubId}`, {}); channel .join() .receive("ok", data => { const hub = data.hubs[0]; const defaultSpaceTopic = hub.topics[0]; const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; remountUI({ janusRoomId: defaultSpaceTopic.janus_room_id }); initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); hubChannel.setPhoenixChannel(channel); }) .receive("error", res => console.error(res)); }; document.addEventListener("DOMContentLoaded", onReady);