diff --git a/src/hub.html b/src/hub.html index a4992f194beb69b09b59426394c7ae76623381c5..0abecc4a51f959ecaa182097812518e3bcd3dbf0 100644 --- a/src/hub.html +++ b/src/hub.html @@ -456,7 +456,9 @@ id="environment-root" nav-mesh-helper static-body="shape: none;" - ></a-entity> + > + <a-entity id="environment-scene"/> + </a-entity> <a-entity super-spawner=" diff --git a/src/hub.js b/src/hub.js index 3b648d6aa5570818ff1ad4531f37b836549d95f2..df3083d3ea843c09b2fbfb77c4bb111044f11a1f 100644 --- a/src/hub.js +++ b/src/hub.js @@ -153,7 +153,46 @@ concurrentLoadDetector.start(); store.init(); -function mountUI(scene, props = {}) { +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.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"); +} + +let uiProps = {}; + +function mountUI(props = {}) { + const scene = document.querySelector("a-scene"); const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.get("vr_entry_type"); const enableScreenSharing = qsTruthy("enable_screen_sharing"); @@ -177,66 +216,138 @@ function mountUI(scene, props = {}) { ); } -const onReady = async () => { - const scene = document.querySelector("a-scene"); - const hubChannel = new HubChannel(store); - const linkChannel = new LinkChannel(store); +function remountUI(props) { + uiProps = { ...uiProps, ...props }; + mountUI(uiProps); +} - window.APP.scene = scene; +async function handleHubChannelJoined(entryManager, data) { + const scene = entryManager.scene; + const hubChannel = entryManager.hubChannel; - registerNetworkSchemas(); + if (NAF.connection.isConnected()) { + // Send complete sync on phoenix re-join. + NAF.connection.entities.completeSync(null, true); + return; + } - let uiProps = { hubChannel, linkChannel }; + 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"); + + if (glbAsset || hasExtension) { + const resolved = await resolveMedia(sceneUrl, false, 0); + const gltfEl = document.createElement("a-entity"); + gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true }); + gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded")); + environmentScene.appendChild(gltfEl); + } else { + // TODO kill bundles + environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`); + } - mountUI(scene); + remountUI({ hubId: hub.hub_id, hubName: hub.name }); - const remountUI = props => { - uiProps = { ...uiProps, ...props }; - mountUI(scene, uiProps); - }; + scene.setAttribute("networked-scene", { + room: hub.hub_id, + serverURL: process.env.JANUS_SERVER, + debug: !!isDebug + }); - const pollForSupportAvailability = callback => { - let isSupportAvailable = null; - const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability"); + if (isBotMode) { + entryManager.enterSceneWhenLoaded(new MediaStream(), false); + } - const updateIfChanged = () => { - fetch(availabilityUrl).then(({ ok }) => { - if (isSupportAvailable !== ok) { - isSupportAvailable = ok; - callback(isSupportAvailable); - } - }); - }; + const sendHubDataMessage = function(clientId, dataType, data, reliable) { + const event = "naf"; + const payload = { dataType, data }; - updateIfChanged(); - setInterval(updateIfChanged, 30000); - }; + if (clientId != null) { + payload.clientId = clientId; + } - pollForSupportAvailability(isSupportAvailable => { - remountUI({ isSupportAvailable }); - }); + 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); + } + }; - const getPlatformUnsupportedReason = () => { - if (typeof RTCDataChannelEvent === "undefined") { - return "no_data_channels"; + const connectWhenNetworkedSceneReady = () => { + if (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) { + setTimeout(connectWhenNetworkedSceneReady, 0); + return; } - return null; + scene.components["networked-scene"] + .connect() + .then(() => { + NAF.connection.adapter.reliableTransport = (clientId, dataType, data) => + sendHubDataMessage(clientId, dataType, data, true); + }) + .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(); + + return; + }); }; + connectWhenNetworkedSceneReady(); +} + +document.addEventListener("DOMContentLoaded", () => { + const scene = document.querySelector("a-scene"); + const hubChannel = new HubChannel(store); const entryManager = new SceneEntryManager(hubChannel); - remountUI({ enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); + const linkChannel = new LinkChannel(store); + + window.APP.scene = scene; + + registerNetworkSchemas(); + mountUI({}); + remountUI({ hubChannel, linkChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); + + pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable })); + + document.body.addEventListener("connected", () => + remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 }) + ); + + document.body.addEventListener("clientConnected", () => + remountUI({ + occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 + }) + ); + + document.body.addEventListener("clientDisconnected", () => + remountUI({ + occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 + }) + ); const platformUnsupportedReason = getPlatformUnsupportedReason(); if (platformUnsupportedReason) { - remountUI({ platformUnsupportedReason: platformUnsupportedReason }); + remountUI({ platformUnsupportedReason }); entryManager.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); @@ -253,25 +364,10 @@ const onReady = async () => { } }); - const environmentRoot = document.querySelector("#environment-root"); + document.querySelector("#environment-scene").addEventListener("bundleloaded", () => { + remountUI({ environmentSceneLoaded: true }); - const initialEnvironmentEl = document.createElement("a-entity"); - initialEnvironmentEl.addEventListener("bundleloaded", () => { - remountUI({ initialEnvironmentLoaded: true }); - - const camera = document.querySelector("#player-camera"); - const previewCamera = initialEnvironmentEl.object3D.getObjectByName("scene-preview-camera"); - - if (previewCamera) { - camera.object3D.position.copy(previewCamera.position); - camera.object3D.rotation.copy(previewCamera.rotation); - 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"); + setupLobbyCamera(); // Replace renderer with a noop renderer to reduce bot resource usage. if (isBotMode) { @@ -279,16 +375,6 @@ const onReady = async () => { scene.renderer = { setAnimationLoop: noop, render: noop }; } }); - environmentRoot.appendChild(initialEnvironmentEl); - - const enterSceneWhenReady = hubId => { - const enterSceneImmediately = () => entryManager.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]; @@ -296,114 +382,13 @@ const onReady = async () => { 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); - const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true }); - gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded")); - initialEnvironmentEl.appendChild(gltfEl); - loadedSceneUrl = sceneUrl; - } - } else { - // TODO kill bundles - initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${sceneUrl}`); - } - - remountUI({ hubId: hub.hub_id, hubName: hub.name }); - hubChannel.setPhoenixChannel(channel); - - scene.setAttribute("networked-scene", { - room: hub.hub_id, - serverURL: process.env.JANUS_SERVER - }); - - if (isDebug) { - scene.setAttribute("networked-scene", { debug: true }); - } - - if (isBotMode) enterSceneWhenReady(hub.hub_id); - - if (NAF.connection.adapter) { - // Send complete sync on phoenix re-join. - NAF.connection.entities.completeSync(null, true); - } - - document.body.addEventListener("connected", () => { - remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 }); - }); - - document.body.addEventListener("clientConnected", () => { - remountUI({ - occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 - }); - }); - - document.body.addEventListener("clientDisconnected", () => { - remountUI({ - occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 - }); - }); - - 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); - } - }; - - const connectWhenNetworkedSceneReady = () => { - if (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) { - setTimeout(connectWhenNetworkedSceneReady, 0); - return; - } - - scene.components["networked-scene"] - .connect() - .then(() => { - NAF.connection.adapter.reliableTransport = (clientId, dataType, data) => - sendHubDataMessage(clientId, dataType, data, true); - }) - .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(); - - return; - }); - }; - - connectWhenNetworkedSceneReady(); - }; channel .join() - .receive("ok", handleJoinedHubChannel) + .receive("ok", async data => { + hubChannel.setPhoenixChannel(channel); + await handleHubChannelJoined(entryManager, data); + }) .receive("error", res => { if (res.reason === "closed") { entryManager.exitScene(); @@ -414,12 +399,9 @@ const onReady = async () => { }); channel.on("naf", data => { - if (NAF.connection.adapter) { - NAF.connection.adapter.onData(data); - } + if (!NAF.connection.adapter) return; + NAF.connection.adapter.onData(data); }); linkChannel.setSocket(socket); -}; - -document.addEventListener("DOMContentLoaded", onReady); +}); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index ddc6608fe643df4a34a95d86d081ae0e5e7ca89f..c56ed0b0c663a5961c8bf2631d9fabc196453f9a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -75,7 +75,7 @@ class UIRoot extends Component { linkChannel: PropTypes.object, showProfileEntry: PropTypes.bool, availableVREntryTypes: PropTypes.object, - initialEnvironmentLoaded: PropTypes.bool, + environmentSceneLoaded: PropTypes.bool, roomUnavailableReason: PropTypes.string, platformUnsupportedReason: PropTypes.string, hubId: PropTypes.string, @@ -639,7 +639,7 @@ class UIRoot extends Component { ); } - if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.hubId) { + if (!this.props.environmentSceneLoaded || !this.props.availableVREntryTypes || !this.props.hubId) { return ( <IntlProvider locale={lang} messages={messages}> <div className="loading-panel"> diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index b82fd1deef23bfae83faf4f77f8653a9a37f0d5e..ce67c4820f2e47eb3317ee25e4916d8eb73b4cf1 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -75,6 +75,16 @@ export default class SceneEntryManager { this.spawnAvatar(); }; + enterSceneWhenLoaded = (mediaStream, enterInVR) => { + const enterSceneImmediately = () => this.enterScene(mediaStream, enterInVR); + + if (this.scene.hasLoaded) { + enterSceneImmediately(); + } else { + this.scene.addEventListener("loaded", enterSceneImmediately); + } + }; + exitScene = () => { if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());