Skip to content
Snippets Groups Projects
hub.js 20.8 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 screenfull from "screenfull";
import "three/examples/js/loaders/GLTFLoader";
import "networked-aframe/src/index";
netpro2k's avatar
netpro2k committed
import "aframe-teleport-controls";
import "aframe-input-mapping-component";
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";
import trackpad_dpad4 from "./behaviours/trackpad-dpad4";
joni's avatar
joni committed
import trackpad_scrolling from "./behaviours/trackpad-scrolling";
import joystick_dpad4 from "./behaviours/joystick-dpad4";
import msft_mr_axis_with_deadzone from "./behaviours/msft-mr-axis-with-deadzone";
joni's avatar
joni committed
import { PressedMove } from "./activators/pressedmove";
joni's avatar
joni committed
import { ReverseY } from "./activators/reversey";
import { ObjectContentOrigins } from "./object-types";

joni's avatar
joni committed
import "./activators/shortpress";
joni's avatar
joni committed

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";
joni's avatar
joni committed
import "./components/haptic-feedback";
import "./components/networked-video-player";
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";
joni's avatar
joni committed
import "./components/look-on-mobile";
import "./components/pitch-yaw-rotator";
import "./components/input-configurator";
import "./components/auto-scale-cannon-physics-body";
import "./components/position-at-box-shape-border";
import "./components/remove-networked-object-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";
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 { addMedia, resolveMedia } from "./utils/media-utils";
import "./systems/nav";
import "./systems/personal-space-bubble";
import "./systems/app-mode";
Robert Long's avatar
Robert Long committed
import "./systems/exit-on-blur";
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";
Greg Fodor's avatar
Greg Fodor committed
import "./components/grabbable-toggle";
import "./components/cardboard-controls";

import "./components/nav-mesh-helper";
import "./systems/tunnel-effect";
import "./components/tools/pen";
import "./components/tools/networked-drawing";
import "./components/tools/drawing-manager";

joni's avatar
joni committed
import registerNetworkSchemas from "./network-schemas";
import { inGameActions, config as inputConfig } from "./input-mappings";
Greg Fodor's avatar
Greg Fodor committed
import registerTelemetry from "./telemetry";
joni's avatar
joni committed

import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } 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();
}
AFRAME.registerInputBehaviour("trackpad_dpad4", trackpad_dpad4);
joni's avatar
joni committed
AFRAME.registerInputBehaviour("trackpad_scrolling", trackpad_scrolling);
AFRAME.registerInputBehaviour("joystick_dpad4", joystick_dpad4);
AFRAME.registerInputBehaviour("msft_mr_axis_with_deadzone", msft_mr_axis_with_deadzone);
AFRAME.registerInputActivator("pressedmove", PressedMove);
AFRAME.registerInputActivator("reverseY", ReverseY);
Greg Fodor's avatar
Greg Fodor committed
AFRAME.registerInputMappings(inputConfig, true);
const concurrentLoadDetector = new ConcurrentLoadDetector();
concurrentLoadDetector.start();
store.init();
function mountUI(scene, props = {}) {
  const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
  const forcedVREntryType = qs.get("vr_entry_type");
  const enableScreenSharing = qsTruthy("enable_screen_sharing");
  const showProfileEntry = !store.state.activity.hasChangedName;
  ReactDOM.render(
        concurrentLoadDetector,
        disableAutoExitOnConcurrentLoad,
        forcedVREntryType,
        showProfileEntry,
        ...props
function requestFullscreen() {
  if (screenfull.enabled && !screenfull.isFullscreen) screenfull.request();
}

const onReady = async () => {
  const scene = document.querySelector("a-scene");
  const hubChannel = new HubChannel(store);
  const linkChannel = new LinkChannel(store);
joni's avatar
joni committed
  document.querySelector("canvas").classList.add("blurred");
  registerNetworkSchemas();

Greg Fodor's avatar
Greg Fodor committed
  let uiProps = { hubChannel, linkChannel };
  const remountUI = props => {
    uiProps = { ...uiProps, ...props };
    mountUI(scene, uiProps);
  const applyProfileFromStore = playerRig => {
    const displayName = store.state.profile.displayName;
    playerRig.setAttribute("player-info", {
      displayName,
      avatarSrc: "#" + (store.state.profile.avatarId || "botdefault")
    const hudController = playerRig.querySelector("[hud-controller]");
    hudController.setAttribute("hud-controller", { showTip: !store.state.activity.hasFoundFreeze });
    document.querySelector("a-scene").emit("username-changed", { username: displayName });
Greg Fodor's avatar
Greg Fodor committed
  const pollForSupportAvailability = callback => {
    let isSupportAvailable = null;
    const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability");

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

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

  const exitScene = () => {
    if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
      NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
    }
    if (hubChannel) {
      hubChannel.disconnect();
    }
    const scene = document.querySelector("a-scene");
    if (scene) {
      if (scene.renderer) {
        scene.renderer.setAnimationLoop(null); // Stop animation loop, TODO A-Frame should do this
      }
      document.body.removeChild(scene);
    }
    document.body.removeEventListener("touchend", requestFullscreen);
  const enterScene = async (mediaStream, enterInVR, hubId) => {
    const scene = document.querySelector("a-scene");
    // Get aframe inspector url using the webpack file-loader.
    const aframeInspectorUrl = require("file-loader?name=assets/js/[name]-[hash].[ext]!aframe-inspector/dist/aframe-inspector.min.js");
    // Set the aframe-inspector url to our hosted copy.
    scene.setAttribute("inspector", { url: aframeInspectorUrl });

    if (!isBotMode) {
      scene.classList.add("no-cursor");
    }
    const playerRig = document.querySelector("#player-rig");
joni's avatar
joni committed
    document.querySelector("canvas").classList.remove("blurred");
    scene.render();

    if (enterInVR) {
      scene.enterVR();
    } else if (AFRAME.utils.device.isMobile()) {
      document.body.addEventListener("touchend", requestFullscreen);
    }

    AFRAME.registerInputActions(inGameActions, "default");

    scene.setAttribute("networked-scene", {
    if (isDebug) {
      scene.setAttribute("networked-scene", { debug: true });
    }

    scene.setAttribute("stats-plus", false);

    if (isMobile || qsTruthy("mobile")) {
      playerRig.setAttribute("virtual-gamepad-controls", {});
    }

    const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig);
    applyProfileOnPlayerRig();
    store.addEventListener("statechanged", applyProfileOnPlayerRig);

    const avatarScale = parseInt(qs.get("avatar_scale"), 10);

    if (avatarScale) {
      playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale });
    }

    const videoTracks = mediaStream ? 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);
    });

joni's avatar
joni committed
    document.body.addEventListener("blocked", ev => {
      NAF.connection.entities.removeEntitiesOfClient(ev.detail.clientId);
    });

    document.body.addEventListener("unblocked", ev => {
      NAF.connection.entities.completeSync(ev.detail.clientId);
    });

    const offset = { x: 0, y: 0, z: -1.5 };
    const spawnMediaInfrontOfPlayer = (src, contentOrigin) => {
      const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true);

      orientation.then(or => {
        entity.setAttribute("offset-relative-to", {
          target: "#player-camera",
          offset,
          orientation: or
        });
      });
    };

netpro2k's avatar
netpro2k committed
    scene.addEventListener("add_media", e => {
Brian Peiris's avatar
Brian Peiris committed
      const contentOrigin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL;
Brian Peiris's avatar
Brian Peiris committed
      spawnMediaInfrontOfPlayer(e.detail, contentOrigin);
    scene.addEventListener("action_spawn_camera", () => {
      const entity = document.createElement("a-entity");
      entity.setAttribute("networked", { template: "#interactable-camera" });
      entity.setAttribute("offset-relative-to", {
        target: "#player-camera",
        offset: { x: 0, y: 0, z: -1.5 }
      });
      scene.appendChild(entity);
    });

    scene.addEventListener("object_spawned", e => {
      if (hubChannel) {
        hubChannel.sendObjectSpawnedEvent(e.detail.objectType);
      }
    document.addEventListener("paste", e => {
      if (e.target.nodeName === "INPUT") return;
      const url = e.clipboardData.getData("text");
      const files = e.clipboardData.files && e.clipboardData.files;
      if (url) {
        spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL);
      } else {
        for (const file of files) {
          spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.CLIPBOARD);
    document.addEventListener("dragover", e => {
      e.preventDefault();
    });
netpro2k's avatar
netpro2k committed

    document.addEventListener("drop", e => {
      e.preventDefault();
      const url = e.dataTransfer.getData("url");
      const files = e.dataTransfer.files;
      if (url) {
        spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL);
      } else {
        for (const file of files) {
          spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.FILE);
netpro2k's avatar
netpro2k committed

    if (!qsTruthy("offline")) {
      document.body.addEventListener("connected", () => {
        if (!isBotMode) {
          hubChannel.sendEntryEvent().then(() => {
            store.update({ activity: { lastEnteredAt: new Date().toISOString() } });
        remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 });
Brian Peiris's avatar
Brian Peiris committed
      });

      document.body.addEventListener("clientConnected", () => {
        remountUI({
          occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
Brian Peiris's avatar
Brian Peiris committed
        });
      });

      document.body.addEventListener("clientDisconnected", () => {
        remountUI({
          occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
Brian Peiris's avatar
Brian Peiris committed
        });
      });

      scene.components["networked-scene"].connect().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" });
        exitScene();

        return;
      });

      const sendHubDataMessage = function(clientId, dataType, data, reliable) {
        const event = "naf";
        const payload = { dataType, data };
        if (clientId != null) {
          payload.clientId = clientId;
        }
        if (reliable) {
          hubChannel.channel.push(event, payload);
        } else {
          const topic = hubChannel.channel.topic;
          const join_ref = hubChannel.channel.joinRef();
          hubChannel.channel.socket.push({ topic, event, payload, join_ref, ref: null }, false);
        }
      NAF.connection.adapter.reliableTransport = (clientId, dataType, data) =>
        sendHubDataMessage(clientId, dataType, data, true);
      /*NAF.connection.adapter.unreliableTransport = (clientId, dataType, data) =>
        sendHubDataMessage(clientId, dataType, data, false);*/
      if (isDebug) {
        NAF.connection.adapter.session.options.verbose = true;
      }

      if (isBotMode) {
        playerRig.setAttribute("avatar-replay", {
          camera: "#player-camera",
          leftController: "#player-left-controller",
          rightController: "#player-right-controller"

        const audioEl = document.createElement("audio");
        const audioInput = document.querySelector("#bot-audio-input");
        audioInput.onchange = () => {
          audioEl.loop = true;
          audioEl.muted = true;
          audioEl.crossorigin = "anonymous";
          audioEl.src = URL.createObjectURL(audioInput.files[0]);
          document.body.appendChild(audioEl);
        };
        const dataInput = document.querySelector("#bot-data-input");
        dataInput.onchange = () => {
          const url = URL.createObjectURL(dataInput.files[0]);
          playerRig.setAttribute("avatar-replay", { recordingUrl: url });
        };
        await new Promise(resolve => audioEl.addEventListener("canplay", resolve));
        mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]);
        audioEl.play();
      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);
        }
      }
Greg Fodor's avatar
Greg Fodor committed

      pollForSupportAvailability(isSupportAvailable => {
        remountUI({ isSupportAvailable });
      });
  const getPlatformUnsupportedReason = () => {
    if (typeof RTCDataChannelEvent === "undefined") {
      return "no_data_channels";
    }

    return null;
  };

  remountUI({ enterScene, exitScene });

  const platformUnsupportedReason = getPlatformUnsupportedReason();

  if (platformUnsupportedReason) {
    remountUI({ platformUnsupportedReason: platformUnsupportedReason });
    exitScene();
    return;
  }

  if (qs.get("required_version") && process.env.BUILD_VERSION) {
    const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)"
    if (qs.get("required_version") !== buildNumber) {
      remountUI({ roomUnavailableReason: "version_mismatch" });
      setTimeout(() => document.location.reload(), 5000);
      exitScene();
      return;
    }
  getAvailableVREntryTypes().then(availableVREntryTypes => {
    if (availableVREntryTypes.isInHMD) {
Greg Fodor's avatar
Greg Fodor committed
      remountUI({ availableVREntryTypes, forcedVREntryType: "vr" });
    } else if (availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) {
      remountUI({ availableVREntryTypes, forcedVREntryType: "gearvr" });
    } else {
      remountUI({ availableVREntryTypes });
    }
  });

  const environmentRoot = document.querySelector("#environment-root");

  const initialEnvironmentEl = document.createElement("a-entity");
  initialEnvironmentEl.addEventListener("bundleloaded", () => {
    remountUI({ initialEnvironmentLoaded: true });
Brian Peiris's avatar
Brian Peiris committed
    // We never want to stop the render loop when were running in "bot" mode.
    if (!isBotMode) {
Brian Peiris's avatar
Brian Peiris committed
      // Stop rendering while the UI is up. We restart the render loop in enterScene.
      // Wait a tick plus some margin so that the environments actually render.
      setTimeout(() => scene.renderer.setAnimationLoop(null), 100);
Brian Peiris's avatar
Brian Peiris committed
    } else {
Brian Peiris's avatar
Brian Peiris committed
      const noop = () => {};
      // Replace renderer with a noop renderer to reduce bot resource usage.
      scene.renderer = { setAnimationLoop: noop, render: noop };
  environmentRoot.appendChild(initialEnvironmentEl);

Greg Fodor's avatar
Greg Fodor committed
  const enterSceneWhenReady = hubId => {
    const enterSceneImmediately = () => enterScene(new MediaStream(), false, hubId);
    if (scene.hasLoaded) {
      enterSceneImmediately();
    } else {
      scene.addEventListener("loaded", enterSceneImmediately);
  // Connect to reticulum over phoenix channels to get hub info.
  const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0];
Greg Fodor's avatar
Greg Fodor committed
  console.log(`Hub ID: ${hubId}`);
  const socket = connectToReticulum(isDebug);
  const channel = socket.channel(`hub:${hubId}`, {});
  let loadedSceneUrl = null;
  // This routine needs to handle re-joins.
  const handleJoinedHubChannel = async data => {
    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}`);
    if (glbAsset || hasExtension) {
      if (loadedSceneUrl !== sceneUrl) {
        const resolved = await resolveMedia(sceneUrl, false, 0);
Greg Fodor's avatar
Greg Fodor committed
        const gltfEl = document.createElement("a-entity");
        gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true });
Greg Fodor's avatar
Greg Fodor committed
        gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded"));
        initialEnvironmentEl.appendChild(gltfEl);
        loadedSceneUrl = sceneUrl;
Greg Fodor's avatar
Greg Fodor committed
      }
      // TODO kill bundles
      initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
    }
Greg Fodor's avatar
Greg Fodor committed
    remountUI({ hubId: hub.hub_id, hubName: hub.name });
    hubChannel.setPhoenixChannel(channel);
Greg Fodor's avatar
Greg Fodor committed
    if (isBotMode) enterSceneWhenReady(hub.hub_id);

    if (NAF.connection.adapter) {
      // Send complete sync on phoenix re-join.
      NAF.connection.entities.completeSync(null, true);
    }
  };
  channel
    .join()
    .receive("ok", handleJoinedHubChannel)
    .receive("error", res => {
      if (res.reason === "closed") {
        exitScene();
        remountUI({ roomUnavailableReason: "closed" });
      }

      console.error(res);
    });
Greg Fodor's avatar
Greg Fodor committed

  channel.on("naf", data => {
    if (NAF.connection.adapter) {
      NAF.connection.adapter.onData(data);
  linkChannel.setSocket(socket);
};

document.addEventListener("DOMContentLoaded", onReady);