Skip to content
Snippets Groups Projects
scene-entry-manager.js 12.2 KiB
Newer Older
import qsTruthy from "./utils/qs_truthy";
import screenfull from "screenfull";
import nextTick from "./utils/next-tick";
Greg Fodor's avatar
Greg Fodor committed
import pinnedEntityToGltf from "./utils/pinned-entity-to-gltf";
Greg Fodor's avatar
Greg Fodor committed
const playerHeight = 1.6;
const isBotMode = qsTruthy("bot");
const isMobile = AFRAME.utils.device.isMobile();
const isDebug = qsTruthy("debug");
const qs = new URLSearchParams(location.search);
const aframeInspectorUrl = require("file-loader?name=assets/js/[name]-[hash].[ext]!aframe-inspector/dist/aframe-inspector.min.js");

import { addMedia, proxiedUrlFor } from "./utils/media-utils";
import { ObjectContentOrigins } from "./object-types";

function requestFullscreen() {
  if (screenfull.enabled && !screenfull.isFullscreen) screenfull.request();
}

Greg Fodor's avatar
Greg Fodor committed
export default class SceneEntryManager {
  constructor(hubChannel) {
    this.hubChannel = hubChannel;
    this.store = window.APP.store;
    this.scene = document.querySelector("a-scene");
    this.cursorController = document.querySelector("#cursor-controller");
    this.playerRig = document.querySelector("#player-rig");
Greg Fodor's avatar
Greg Fodor committed
    this._entered = false;
  init = () => {
    this.whenSceneLoaded(() => {
      this.cursorController.components["cursor-controller"].enabled = false;
Greg Fodor's avatar
Greg Fodor committed
  hasEntered = () => {
Greg Fodor's avatar
Greg Fodor committed
    return this._entered;
  };

  enterScene = async (mediaStream, enterInVR) => {
    const playerCamera = document.querySelector("#player-camera");
    playerCamera.removeAttribute("scene-preview-camera");
    playerCamera.object3D.position.set(0, playerHeight, 0);

    // Get aframe inspector url using the webpack file-loader.
    // Set the aframe-inspector url to our hosted copy.
    this.scene.setAttribute("inspector", { url: aframeInspectorUrl });

    if (isDebug) {
      NAF.connection.adapter.session.options.verbose = true;
    }

    if (enterInVR) {
      // HACK - A-Frame calls getVRDisplays at module load, we want to do it here to
      // force gamepads to become live.
      navigator.getVRDisplays();
      this.scene.enterVR();
    } else if (AFRAME.utils.device.isMobile()) {
      document.body.addEventListener("touchend", requestFullscreen);
    }

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

Greg Fodor's avatar
Greg Fodor committed
    this._setupPlayerRig();
    this._setupBlocking();
    this._setupMedia(mediaStream);
Greg Fodor's avatar
Greg Fodor committed
    this._setupCamera();

    if (qsTruthy("offline")) return;

Greg Fodor's avatar
Greg Fodor committed
    this._spawnAvatar();
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
      this._runBot(mediaStream);
Greg Fodor's avatar
Greg Fodor committed
    this.scene.setAttribute("motion-capture-replayer", "enabled", false);

Greg Fodor's avatar
Greg Fodor committed
    if (mediaStream) {
      NAF.connection.adapter.setLocalMediaStream(mediaStream);
    }

    this.scene.classList.remove("hand-cursor");
    this.scene.classList.add("no-cursor");

    this.cursorController.components["cursor-controller"].enabled = true;
Greg Fodor's avatar
Greg Fodor committed
    this._entered = true;
    // Delay sending entry event telemetry until VR display is presenting.
    (async () => {
      while (enterInVR && !(await navigator.getVRDisplays()).find(d => d.isPresenting)) {
        await nextTick();
      }

      this.hubChannel.sendEntryEvent().then(() => {
        this.store.update({ activity: { lastEnteredAt: new Date().toISOString() } });
      });

    this.scene.addState("entered");
  whenSceneLoaded = callback => {
Greg Fodor's avatar
Greg Fodor committed
    if (this.scene.hasLoaded) {
Greg Fodor's avatar
Greg Fodor committed
    } else {
      this.scene.addEventListener("loaded", callback);
  enterSceneWhenLoaded = (mediaStream, enterInVR) => {
    this.whenSceneLoaded(() => this.enterScene(mediaStream, enterInVR));
  };

  exitScene = () => {
    if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
      NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
    }
    if (this.hubChannel) {
      this.hubChannel.disconnect();
    }
    if (this.scene.renderer) {
      this.scene.renderer.setAnimationLoop(null); // Stop animation loop, TODO A-Frame should do this
    }
    document.body.removeChild(this.scene);
    document.body.removeEventListener("touchend", requestFullscreen);
  };

Greg Fodor's avatar
Greg Fodor committed
  _setupPlayerRig = () => {
    this._updatePlayerRigWithProfile();
    this.store.addEventListener("statechanged", this._updatePlayerRigWithProfile);

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

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

Greg Fodor's avatar
Greg Fodor committed
  _updatePlayerRigWithProfile = () => {
    const { avatarId, displayName } = this.store.state.profile;
    this.playerRig.setAttribute("player-info", {
      displayName,
      avatarSrc: avatarId && avatarId.startsWith("http") ? proxiedUrlFor(avatarId) : `#${avatarId || "botdefault"}`
    });
    const hudController = this.playerRig.querySelector("[hud-controller]");
    hudController.setAttribute("hud-controller", { showTip: !this.store.state.activity.hasFoundFreeze });
Greg Fodor's avatar
Greg Fodor committed
    this.scene.emit("username-changed", { username: displayName });
Greg Fodor's avatar
Greg Fodor committed
  _setupBlocking = () => {
    document.body.addEventListener("blocked", ev => {
      NAF.connection.entities.removeEntitiesOfClient(ev.detail.clientId);
    });

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

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

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

      return entity;
    };

    this.scene.addEventListener("add_media", e => {
      const contentOrigin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL;

      spawnMediaInfrontOfPlayer(e.detail, contentOrigin);
    });

Greg Fodor's avatar
Greg Fodor committed
    this.scene.addEventListener("pinned", e => {
      const el = e.detail.el;
Greg Fodor's avatar
Greg Fodor committed
      const networkId = el.components.networked.data.networkId;
      const gltfNode = pinnedEntityToGltf(el);
      if (!gltfNode) return;

      el.setAttribute("networked", { persistent: true });
Greg Fodor's avatar
Greg Fodor committed
      this.hubChannel.pin(networkId, gltfNode);
Greg Fodor's avatar
Greg Fodor committed
    this.scene.addEventListener("unpinned", e => {
      const el = e.detail.el;
Greg Fodor's avatar
Greg Fodor committed
      const components = el.components;
      const networked = components.networked;

Greg Fodor's avatar
Greg Fodor committed
      if (!networked || !networked.data || !NAF.utils.isMine(el)) return;
Greg Fodor's avatar
Greg Fodor committed
      const networkId = components.networked.data.networkId;
      el.setAttribute("networked", { persistent: false });
Greg Fodor's avatar
Greg Fodor committed

      this.hubChannel.unpin(networkId);
    this.scene.addEventListener("object_spawned", e => {
      this.hubChannel.sendObjectSpawnedEvent(e.detail.objectType);
    });

    document.addEventListener("paste", e => {
Greg Fodor's avatar
Greg Fodor committed
      if (e.target.matches("input, textarea") && document.activeElement === e.target) 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());

    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);
        }
      }
    });
    let currentVideoShareEntity;
    let isHandlingVideoShare = false;

    const shareVideoMediaStream = async constraints => {
      if (isHandlingVideoShare) return;
      isHandlingVideoShare = true;

Greg Fodor's avatar
Greg Fodor committed
      const newStream = await navigator.mediaDevices.getUserMedia(constraints);
      const videoTracks = newStream ? newStream.getVideoTracks() : [];
Greg Fodor's avatar
Greg Fodor committed
      if (videoTracks.length > 0) {
        newStream.getVideoTracks().forEach(track => mediaStream.addTrack(track));
        NAF.connection.adapter.setLocalMediaStream(mediaStream);
Greg Fodor's avatar
Greg Fodor committed
        currentVideoShareEntity = spawnMediaInfrontOfPlayer(mediaStream, undefined);

        // Wire up custom removal event which will stop the stream.
        currentVideoShareEntity.setAttribute("emit-scene-event-on-remove", "event:action_end_video_sharing");
Greg Fodor's avatar
Greg Fodor committed
      this.scene.emit("share_video_enabled", { source: constraints.video.mediaSource });
Greg Fodor's avatar
Greg Fodor committed
      isHandlingVideoShare = false;
    };

    this.scene.addEventListener("action_share_camera", () => {
      shareVideoMediaStream({
          mediaSource: "camera",
          width: 720,
          frameRate: 30
        }
      });
    });

    this.scene.addEventListener("action_share_window", () => {
      shareVideoMediaStream({
        video: {
          mediaSource: "window",
          // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything
          // other than your current monitor that has a different aspect ratio.
          width: 720 * (screen.width / screen.height),
          height: 720,
          frameRate: 30
        }
    this.scene.addEventListener("action_share_screen", () => {
      shareVideoMediaStream({
        video: {
          mediaSource: "screen",
          // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything
          // other than your current monitor that has a different aspect ratio.
          width: 720 * (screen.width / screen.height),
          height: 720,
          frameRate: 30
        }
      });
Greg Fodor's avatar
Greg Fodor committed

    this.scene.addEventListener("action_end_video_sharing", () => {
      if (isHandlingVideoShare) return;
      isHandlingVideoShare = true;
Greg Fodor's avatar
Greg Fodor committed
      if (currentVideoShareEntity && currentVideoShareEntity.parentNode) {
        currentVideoShareEntity.parentNode.removeChild(currentVideoShareEntity);
      }
Greg Fodor's avatar
Greg Fodor committed

      for (const track of mediaStream.getVideoTracks()) {
        mediaStream.removeTrack(track);
      }

      NAF.connection.adapter.setLocalMediaStream(mediaStream);
      currentVideoShareEntity = null;

      this.scene.emit("share_video_disabled");
      isHandlingVideoShare = false;
    });
Greg Fodor's avatar
Greg Fodor committed
  _setupCamera = () => {
    this.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 }
      });
      this.scene.appendChild(entity);
    });

    this.scene.addEventListener("photo_taken", e => {
      this.hubChannel.sendMessage({ src: e.detail }, "spawn");
    });
Greg Fodor's avatar
Greg Fodor committed
  _spawnAvatar = () => {
    this.playerRig.setAttribute("networked", "template: #remote-avatar-template; attachTemplateToLocal: false;");
    this.playerRig.setAttribute("networked-avatar", "");
    this.playerRig.emit("entered");
  };

Greg Fodor's avatar
Greg Fodor committed
  _runBot = async mediaStream => {
    console.log("Running bot");

    this.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]);
      this.playerRig.setAttribute("avatar-replay", { recordingUrl: url });
    };
    await new Promise(resolve => audioEl.addEventListener("canplay", resolve));
    mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]);
Greg Fodor's avatar
Greg Fodor committed
    NAF.connection.adapter.setLocalMediaStream(mediaStream);