Skip to content
Snippets Groups Projects
hub.js 18.4 KiB
Newer Older
console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`);

import "./assets/stylesheets/hub.scss";
import "aframe";
import "./utils/logging";
import { patchWebGLRenderingContext } from "./utils/webgl";
patchWebGLRenderingContext();

import "three/examples/js/loaders/GLTFLoader";
import "networked-aframe/src/index";
netpro2k's avatar
netpro2k committed
import "aframe-teleport-controls";
Robert Long's avatar
Robert Long committed
import "aframe-billboard-component";
import "webrtc-adapter";
import "aframe-slice9-component";
import "aframe-motion-capture-components";
import "./utils/audio-context-fix";
Greg Fodor's avatar
Greg Fodor committed
import { getReticulumFetchUrl } from "./utils/phoenix-utils";
Greg Fodor's avatar
Greg Fodor committed
import nextTick from "./utils/next-tick";
import { addAnimationComponents } from "./utils/animation";
import { Presence } from "phoenix";
Greg Fodor's avatar
Greg Fodor committed
import "./components/scene-components";
Robert Long's avatar
Robert Long committed
import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future
netpro2k's avatar
netpro2k committed
import "./components/mute-mic";
netpro2k's avatar
netpro2k committed
import "./components/bone-mute-state-indicator";
import "./components/bone-visibility";
joni's avatar
joni committed
import "./components/in-world-hud";
Robert Long's avatar
Robert Long committed
import "./components/virtual-gamepad-controls";
Robert Long's avatar
Robert Long committed
import "./components/ik-controller";
Robert Long's avatar
Robert Long committed
import "./components/hand-controls2";
import "./components/character-controller";
import "./components/hoverable-visuals";
import "./components/hover-visuals";
joni's avatar
joni committed
import "./components/haptic-feedback";
import "./components/offset-relative-to";
import "./components/player-info";
import "./components/debug";
import "./components/hand-poses";
import "./components/gltf-bundle";
import "./components/hud-controller";
import "./components/freeze-controller";
import "./components/icon-button";
import "./components/text-button";
import "./components/block-button";
import "./components/visible-while-frozen";
import "./components/stats-plus";
import "./components/networked-avatar";
import "./components/avatar-replay";
import "./components/media-views";
import "./components/pinch-to-move";
import "./components/pitch-yaw-rotator";
import "./components/auto-scale-cannon-physics-body";
import "./components/position-at-box-shape-border";
import "./components/pinnable";
Greg Fodor's avatar
Greg Fodor committed
import "./components/pin-networked-object-button";
import "./components/remove-networked-object-button";
import "./components/camera-focus-button";
Greg Fodor's avatar
Greg Fodor committed
import "./components/mirror-camera-button";
import "./components/destroy-at-extreme-distances";
import "./components/gamma-factor";
netpro2k's avatar
netpro2k committed
import "./components/visible-to-owner";
import "./components/camera-tool";
johnshaughnessy's avatar
johnshaughnessy committed
import "./components/scene-sound";
import "./components/emit-state-change";
import "./components/action-to-event";
import "./components/emit-scene-event-on-remove";
import "./components/stop-event-propagation";
import "./components/animation";
Greg Fodor's avatar
Greg Fodor committed
import "./components/follow-in-lower-fov";
Greg Fodor's avatar
Greg Fodor committed
import ReactDOM from "react-dom";
import React from "react";
import UIRoot from "./react-components/ui-root";
import HubChannel from "./utils/hub-channel";
import LinkChannel from "./utils/link-channel";
import { connectToReticulum } from "./utils/phoenix-utils";
import { disableiOSZoom } from "./utils/disable-ios-zoom";
import { proxiedUrlFor } from "./utils/media-utils";
Greg Fodor's avatar
Greg Fodor committed
import MessageDispatch from "./message-dispatch";
Greg Fodor's avatar
Greg Fodor committed
import SceneEntryManager from "./scene-entry-manager";
Greg Fodor's avatar
Greg Fodor committed
import Subscriptions from "./subscriptions";
Greg Fodor's avatar
Greg Fodor committed
import { createInWorldLogMessage } from "./react-components/chat-message";
import "./systems/nav";
import "./systems/personal-space-bubble";
import "./systems/app-mode";
Robert Long's avatar
Robert Long committed
import "./systems/exit-on-blur";
Greg Fodor's avatar
Greg Fodor committed
import "./systems/camera-tools";
johnshaughnessy's avatar
johnshaughnessy committed
import "./systems/userinput/userinput";
Greg Fodor's avatar
Greg Fodor committed
import "./systems/camera-mirror";
import "./systems/userinput/userinput-debug";
import { App } from "./App";

window.APP = new App();
joni's avatar
joni committed
window.APP.RENDER_ORDER = {
  HUD_BACKGROUND: 1,
  HUD_ICONS: 2,
  CURSOR: 3
};
const store = window.APP.store;
const qs = new URLSearchParams(location.search);
const isMobile = AFRAME.utils.device.isMobile();

window.APP.quality = qs.get("quality") || isMobile ? "low" : "high";
import "aframe-physics-system";
import "aframe-physics-extras";
import "super-hands";
import "./components/super-networked-interactable";
import "./components/networked-counter";
import "./components/event-repeater";
import "./components/controls-shape-offset";
import "./components/set-yxz-order";
import "./components/cardboard-controls";

import "./components/tools/pen";
import "./components/tools/networked-drawing";
import "./components/tools/drawing-manager";

joni's avatar
joni committed
import registerNetworkSchemas from "./network-schemas";
Greg Fodor's avatar
Greg Fodor committed
import registerTelemetry from "./telemetry";
import { warmSerializeElement } from "./utils/serialize-element";
joni's avatar
joni committed

Greg Fodor's avatar
Greg Fodor committed
import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
Greg Fodor's avatar
Greg Fodor committed
import qsTruthy from "./utils/qs_truthy";
const isBotMode = qsTruthy("bot");
const isTelemetryDisabled = qsTruthy("disable_telemetry");
const isDebug = qsTruthy("debug");
if (!isBotMode && !isTelemetryDisabled) {
  registerTelemetry();
}
const concurrentLoadDetector = new ConcurrentLoadDetector();
concurrentLoadDetector.start();
store.init();
Greg Fodor's avatar
Greg Fodor committed
function getPlatformUnsupportedReason() {
  if (typeof RTCDataChannelEvent === "undefined") return "no_data_channels";
  return null;
}

function pollForSupportAvailability(callback) {
  const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability");
  let isSupportAvailable = null;

  const updateIfChanged = () =>
    fetch(availabilityUrl).then(({ ok }) => {
      if (isSupportAvailable === ok) return;
      isSupportAvailable = ok;
      callback(isSupportAvailable);
    });

  updateIfChanged();
  setInterval(updateIfChanged, 30000);
}

function setupLobbyCamera() {
  const camera = document.querySelector("#player-camera");
  const previewCamera = document.querySelector("#environment-scene").object3D.getObjectByName("scene-preview-camera");

  if (previewCamera) {
    camera.object3D.position.copy(previewCamera.position);
    camera.object3D.rotation.copy(previewCamera.rotation);
    camera.object3D.rotation.reorder("YXZ");
Greg Fodor's avatar
Greg Fodor committed
    camera.object3D.updateMatrix();
  } else {
    const cameraPos = camera.object3D.position;
    camera.object3D.position.set(cameraPos.x, 2.5, cameraPos.z);
  }

  camera.setAttribute("scene-preview-camera", "positionOnly: true; duration: 60");
  camera.components["pitch-yaw-rotator"].set(camera.object3D.rotation.x, camera.object3D.rotation.y);
Greg Fodor's avatar
Greg Fodor committed
}

let uiProps = {};

function mountUI(props = {}) {
  const scene = document.querySelector("a-scene");
  const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
  const forcedVREntryType = qs.get("vr_entry_type");
  ReactDOM.render(
        concurrentLoadDetector,
        disableAutoExitOnConcurrentLoad,
        forcedVREntryType,
        ...props
Greg Fodor's avatar
Greg Fodor committed
function remountUI(props) {
  uiProps = { ...uiProps, ...props };
  mountUI(uiProps);
Greg Fodor's avatar
Greg Fodor committed
async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) {
  const scene = document.querySelector("a-scene");
Greg Fodor's avatar
Greg Fodor committed
  if (NAF.connection.isConnected()) {
    // Send complete sync on phoenix re-join.
    NAF.connection.entities.completeSync(null, true);
    return;
  }
Greg Fodor's avatar
Greg Fodor committed
  const hub = data.hubs[0];
  const defaultSpaceTopic = hub.topics[0];
  const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb");
  const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle");
  const sceneUrl = (glbAsset || bundleAsset).src;
  const hasExtension = /\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl);

  console.log(`Scene URL: ${sceneUrl}`);
  const environmentScene = document.querySelector("#environment-scene");
Greg Fodor's avatar
Greg Fodor committed
  const objectsScene = document.querySelector("#objects-scene");
  const objectsUrl = getReticulumFetchUrl(`/${hub.hub_id}/objects.gltf`);
  const objectsEl = document.createElement("a-entity");
  objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true });
  objectsScene.appendChild(objectsEl);
Greg Fodor's avatar
Greg Fodor committed

  if (glbAsset || hasExtension) {
    const gltfEl = document.createElement("a-entity");
    gltfEl.setAttribute("gltf-model-plus", { src: proxiedUrlFor(sceneUrl), useCache: false, inflate: true });
Greg Fodor's avatar
Greg Fodor committed
    gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded"));
    environmentScene.appendChild(gltfEl);
  } else {
    // TODO kill bundles
    environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
  }
Greg Fodor's avatar
Greg Fodor committed
  remountUI({
    hubId: hub.hub_id,
    hubName: hub.name,
    hubEntryCode: hub.entry_code,
Greg Fodor's avatar
Greg Fodor committed
    onSendMessage: messageDispatch.dispatch
Greg Fodor's avatar
Greg Fodor committed
  });
Greg Fodor's avatar
Greg Fodor committed
  document
    .querySelector("#hud-hub-entry-link")
    .setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" });

  // Wait for scene objects to load before connecting, so there is no race condition on network state.
  objectsEl.addEventListener("model-loaded", async el => {
    if (el.target !== objectsEl) return;
    scene.setAttribute("networked-scene", {
      room: hub.hub_id,
      serverURL: process.env.JANUS_SERVER,
      debug: !!isDebug
    while (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) await nextTick();

    scene.components["networked-scene"]
      .connect()
      .then(() => {
        NAF.connection.adapter.reliableTransport = (clientId, dataType, data) => {
          const payload = { dataType, data };

          if (clientId) {
            payload.clientId = clientId;
          }

          hubChannel.channel.push("naf", payload);
        };
      })
      .catch(connectError => {
        // hacky until we get return codes
        const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i);
        console.error(connectError);
        remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" });
        entryManager.exitScene();
netpro2k's avatar
netpro2k committed

Greg Fodor's avatar
Greg Fodor committed
}
netpro2k's avatar
netpro2k committed

async function runBotMode(scene, entryManager) {
  const noop = () => {};
  scene.renderer = { setAnimationLoop: noop, render: noop };
  while (!NAF.connection.isConnected()) await nextTick();
  entryManager.enterSceneWhenLoaded(new MediaStream(), false);
}
document.addEventListener("DOMContentLoaded", async () => {
  warmSerializeElement();

  const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0];
  console.log(`Hub ID: ${hubId}`);

  const subscriptions = new Subscriptions(hubId);
Greg Fodor's avatar
Greg Fodor committed

  if (navigator.serviceWorker) {
      navigator.serviceWorker
        .register("/hub.service.js")
        .then(() => {
          navigator.serviceWorker.ready
            .then(registration => subscriptions.setRegistration(registration))
            .catch(() => subscriptions.setRegistrationFailed());
        })
        .catch(() => subscriptions.setRegistrationFailed());
    } catch (e) {
      subscriptions.setRegistrationFailed();
    }
Greg Fodor's avatar
Greg Fodor committed
  const scene = document.querySelector("a-scene");
  scene.removeAttribute("keyboard-shortcuts"); // Remove F and ESC hotkeys from aframe

Greg Fodor's avatar
Greg Fodor committed
  const hubChannel = new HubChannel(store);
Greg Fodor's avatar
Greg Fodor committed
  const entryManager = new SceneEntryManager(hubChannel);
  entryManager.init();
Greg Fodor's avatar
Greg Fodor committed
  const linkChannel = new LinkChannel(store);
Greg Fodor's avatar
Greg Fodor committed
  window.APP.scene = scene;
  scene.addEventListener("enter-vr", () => {
    document.body.classList.add("vr-mode");

    if (!scene.is("entered")) {
      // If VR headset is activated, refreshing page will fire vrdisplayactivate
      // which puts A-Frame in VR mode, so exit VR mode whenever it is attempted
      // to be entered and we haven't entered the room yet.
Greg Fodor's avatar
Greg Fodor committed
      console.log("Pre-emptively exiting VR mode.");
  scene.addEventListener("exit-vr", () => document.body.classList.remove("vr-mode"));

Greg Fodor's avatar
Greg Fodor committed
  registerNetworkSchemas();
Greg Fodor's avatar
Greg Fodor committed

  remountUI({
    hubChannel,
    linkChannel,
    subscriptions,
    enterScene: entryManager.enterScene,
    exitScene: entryManager.exitScene,
    initialIsSubscribed: subscriptions.isSubscribed()
  });
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  scene.addEventListener("action_focus_chat", () => document.querySelector(".chat-focus-target").focus());
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable }));
  const platformUnsupportedReason = getPlatformUnsupportedReason();

  if (platformUnsupportedReason) {
Greg Fodor's avatar
Greg Fodor committed
    remountUI({ platformUnsupportedReason });
    entryManager.exitScene();
  if (qs.get("required_version") && process.env.BUILD_VERSION) {
    const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)"
Greg Fodor's avatar
Greg Fodor committed

    if (qs.get("required_version") !== buildNumber) {
      remountUI({ roomUnavailableReason: "version_mismatch" });
      setTimeout(() => document.location.reload(), 5000);
      entryManager.exitScene();
  const availableVREntryTypes = await getAvailableVREntryTypes();

  if (availableVREntryTypes.isInHMD) {
    remountUI({ availableVREntryTypes, forcedVREntryType: "vr" });
Greg Fodor's avatar
Greg Fodor committed

    if (/Oculus/.test(navigator.userAgent)) {
      // HACK - The polyfill reports Cardboard as the primary VR display on startup out ahead of Oculus Go on Oculus Browser 5.5.0 beta. This display is cached by A-Frame,
      // so we need to resolve that and get the real VRDisplay before entering as well.
      const displays = await navigator.getVRDisplays();
      const vrDisplay = displays.length && displays[0];
      AFRAME.utils.device.getVRDisplay = () => vrDisplay;
    }
  } else {
    remountUI({ availableVREntryTypes });
  }
  const environmentScene = document.querySelector("#environment-scene");

  environmentScene.addEventListener("bundleloaded", () => {
Greg Fodor's avatar
Greg Fodor committed
    remountUI({ environmentSceneLoaded: true });
    for (const modelEl of environmentScene.children) {
      addAnimationComponents(modelEl);
    }

Greg Fodor's avatar
Greg Fodor committed
    setupLobbyCamera();
Greg Fodor's avatar
Greg Fodor committed
    // Replace renderer with a noop renderer to reduce bot resource usage.
    if (isBotMode) {
      runBotMode(scene, entryManager);
  const socket = connectToReticulum(isDebug);
  remountUI({ sessionId: socket.params().session_id });
Greg Fodor's avatar
Greg Fodor committed
  // Hub local channel
  const context = {
    mobile: isMobile,
    hmd: availableVREntryTypes.isInHMD
  };

Greg Fodor's avatar
Greg Fodor committed
  // Reticulum global channel
  const retPhxChannel = socket.channel(`ret`, { hub_id: hubId });
  retPhxChannel
    .join()
    .receive("ok", async data => subscriptions.setVapidPublicKey(data.vapid_public_key))
    .receive("error", res => {
      subscriptions.setVapidPublicKey(null);
      console.error(res);
    });

  const pushSubscriptionEndpoint = await subscriptions.getCurrentEndpoint();
  const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context };
  const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload);
Greg Fodor's avatar
Greg Fodor committed
  const presenceLogEntries = [];
  const addToPresenceLog = entry => {
Greg Fodor's avatar
Greg Fodor committed
    presenceLogEntries.push(entry);
    remountUI({ presenceLogEntries });
Greg Fodor's avatar
Greg Fodor committed
    scene.emit(`presence-log-${entry.type}`);
Greg Fodor's avatar
Greg Fodor committed

    // Fade out and then remove
    setTimeout(() => {
      entry.expired = true;
      remountUI({ presenceLogEntries });

      setTimeout(() => {
        presenceLogEntries.splice(presenceLogEntries.indexOf(entry), 1);
        remountUI({ presenceLogEntries });
      }, 5000);
Greg Fodor's avatar
Greg Fodor committed
    }, 20000);
Greg Fodor's avatar
Greg Fodor committed
  const messageDispatch = new MessageDispatch(scene, entryManager, hubChannel, addToPresenceLog, remountUI);

  hubPhxChannel
    .join()
    .receive("ok", async data => {
      hubChannel.setPhoenixChannel(hubPhxChannel);
      subscriptions.setHubChannel(hubChannel);
      subscriptions.setSubscribed(data.subscriptions.web_push);
      remountUI({ initialIsSubscribed: subscriptions.isSubscribed() });
      await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data);
    })
    .receive("error", res => {
      if (res.reason === "closed") {
        entryManager.exitScene();
        remountUI({ roomUnavailableReason: "closed" });
      }

      console.error(res);
    });

  const hubPhxPresence = new Presence(hubPhxChannel);

Greg Fodor's avatar
Greg Fodor committed
  let isInitialSync = true;
Greg Fodor's avatar
Greg Fodor committed
  const vrHudPresenceCount = document.querySelector("#hud-presence-count");
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  hubPhxPresence.onSync(() => {
    remountUI({ presences: hubPhxPresence.state });
Greg Fodor's avatar
Greg Fodor committed
    const occupantCount = Object.entries(hubPhxPresence.state).length;
Greg Fodor's avatar
Greg Fodor committed
    vrHudPresenceCount.setAttribute("text", "value", occupantCount.toString());
Greg Fodor's avatar
Greg Fodor committed

    if (!isInitialSync) return;
    // Wire up join/leave event handlers after initial sync.
    isInitialSync = false;
    hubPhxPresence.onJoin((sessionId, current, info) => {
      const meta = info.metas[info.metas.length - 1];

      if (current) {
        // Change to existing presence
        const isSelf = sessionId === socket.params().session_id;
        const currentMeta = current.metas[0];

        if (!isSelf && currentMeta.presence !== meta.presence && meta.profile.displayName) {
Greg Fodor's avatar
Greg Fodor committed
          addToPresenceLog({
Greg Fodor's avatar
Greg Fodor committed
            presence: meta.presence,
            name: meta.profile.displayName
          });
        }
Greg Fodor's avatar
Greg Fodor committed

        if (currentMeta.profile && meta.profile && currentMeta.profile.displayName !== meta.profile.displayName) {
          addToPresenceLog({
            type: "display_name_changed",
            oldName: currentMeta.profile.displayName,
            newName: meta.profile.displayName
          });
        }
      } else {
        // New presence
Greg Fodor's avatar
Greg Fodor committed
        const meta = info.metas[0];
Greg Fodor's avatar
Greg Fodor committed

        if (meta.presence && meta.profile.displayName) {
Greg Fodor's avatar
Greg Fodor committed
          addToPresenceLog({
            type: "join",
            presence: meta.presence,
Greg Fodor's avatar
Greg Fodor committed
            name: meta.profile.displayName
          });
        }
Greg Fodor's avatar
Greg Fodor committed

    hubPhxPresence.onLeave((sessionId, current, info) => {
      if (current && current.metas.length > 0) return;

      const meta = info.metas[0];

      if (meta.profile.displayName) {
        addToPresenceLog({
          type: "leave",
          name: meta.profile.displayName
        });
      }
    });
Greg Fodor's avatar
Greg Fodor committed
  });

Greg Fodor's avatar
Greg Fodor committed
  hubPhxChannel.on("naf", data => {
Greg Fodor's avatar
Greg Fodor committed
    if (!NAF.connection.adapter) return;
    NAF.connection.adapter.onData(data);
  hubPhxChannel.on("message", ({ session_id, type, body }) => {
    const userInfo = hubPhxPresence.state[session_id];
Greg Fodor's avatar
Greg Fodor committed
    if (!userInfo) return;
    const maySpawn = scene.is("entered");
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
    const incomingMessage = { name: userInfo.metas[0].profile.displayName, type, body, maySpawn };

    if (scene.is("vr-mode")) {
      createInWorldLogMessage(incomingMessage);
    }

Greg Fodor's avatar
Greg Fodor committed
    addToPresenceLog(incomingMessage);
Greg Fodor's avatar
Greg Fodor committed
  });

  linkChannel.setSocket(socket);
Greg Fodor's avatar
Greg Fodor committed
});