Newer
Older
import qsTruthy from "./utils/qs_truthy";
import screenfull from "screenfull";
import pinnedEntityToGltf from "./utils/pinned-entity-to-gltf";
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();
}
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");
init = () => {
this.whenSceneLoaded(() => {
this.cursorController.components["cursor-controller"].enabled = false;
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", {});
}
this._setupMedia(mediaStream);
if (qsTruthy("offline")) return;
this.scene.setAttribute("motion-capture-replayer", "enabled", false);
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;
// 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 => {
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);
};
_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 });
}
};
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 });
this.scene.emit("username-changed", { username: displayName });
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
});
});
};
this.scene.addEventListener("add_media", e => {
const contentOrigin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL;
spawnMediaInfrontOfPlayer(e.detail, contentOrigin);
});
const networkId = el.components.networked.data.networkId;
const gltfNode = pinnedEntityToGltf(el);
el.setAttribute("networked", { persistent: true });
const networked = components.networked;
el.setAttribute("networked", { persistent: false });
this.scene.addEventListener("object_spawned", e => {
this.hubChannel.sendObjectSpawnedEvent(e.detail.objectType);
});
document.addEventListener("paste", e => {
if (e.target.matches("input, textarea") && document.activeElement === e.target) return;
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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;
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
const videoTracks = newStream ? newStream.getVideoTracks() : [];
if (videoTracks.length > 0) {
newStream.getVideoTracks().forEach(track => mediaStream.addTrack(track));
NAF.connection.adapter.setLocalMediaStream(mediaStream);
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");
this.scene.emit("share_video_enabled", { source: constraints.video.mediaSource });
};
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
}
});
this.scene.addEventListener("action_end_video_sharing", () => {
if (isHandlingVideoShare) return;
isHandlingVideoShare = true;
if (currentVideoShareEntity && currentVideoShareEntity.parentNode) {
currentVideoShareEntity.parentNode.removeChild(currentVideoShareEntity);
}
for (const track of mediaStream.getVideoTracks()) {
mediaStream.removeTrack(track);
}
NAF.connection.adapter.setLocalMediaStream(mediaStream);
currentVideoShareEntity = null;
this.scene.emit("share_video_disabled");
isHandlingVideoShare = false;
});
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");
});
this.playerRig.setAttribute("networked", "template: #remote-avatar-template; attachTemplateToLocal: false;");
this.playerRig.setAttribute("networked-avatar", "");
this.playerRig.emit("entered");
};
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]);
audioEl.play();
};
}