Source: core/connection-manager.js

import { Client, VRSPACE, Welcome } from '../client/vrspace.js';
import { WorldManager } from './world-manager.js';
import { MediaHelper } from './media-helper.js';
import { VRSPACEUI } from '../ui/vrspace-ui.js';
import { VRSpaceAPI } from '../client/rest-api.js';
import { MediaStreams } from './media-streams.js';

/**
 * Component responsible for setting up and mantaining connection to server.
 */
export class ConnectionManager {
  /** @param {WorldManager} worldManager */
  constructor(worldManager) {
    this.worldManager = worldManager;
    this.world = worldManager.world;
    this.api = VRSpaceAPI.getInstance(VRSPACEUI.contentBase);
  }

  /**
  Enter the world specified by world.name. If not already connected, 
  first connect to world.serverUrl and set own properties, then start the session.
  World and WorldListeners are notified by calling entered methods. 
  @param {Object} properties own properties to set before starting the session
  @return {Promise<Welcome>} promise resolved after enter
   */
  async enter(properties) {
    const errorListener = VRSPACE.addErrorListener((e) => {
      console.log("Server error:" + e);
      this.worldManager.error = e;
    });
    return new Promise((resolve, reject) => {
      // TODO most of this code needs to go into VRSpace client.
      // TODO it should use async rather than callback functions
      var afterEnter = (welcome) => {
        VRSPACE.removeWelcomeListener(afterEnter);
        this.entered(welcome);
        resolve(welcome);
      };
      var afterConnect = async (welcome) => {
        VRSPACE.removeWelcomeListener(afterConnect);
        this.configureSession(properties);
        // FIXME for the time being, Enter first, then Session
        if (this.world.name) {
          let anotherWelcomeListener = VRSPACE.addWelcomeListener(welcome => {
            VRSPACE.removeWelcomeListener(anotherWelcomeListener);
            VRSPACE.callCommand("Session", () => afterEnter(welcome));
            this.pubSub(welcome.client.User, VRSPACE.me.video);
          });
          VRSPACE.sendCommand("Enter", { world: this.world.name });
        } else {
          // Can't enter anywhere whithout world name, so we're in default world.
          // Untested scenario, not used anywhere in current code base.
          VRSPACE.callCommand("Session", () => {
            this.entered(welcome)
            resolve(welcome);
            // start session in default space
            this.pubSub(VRSPACE.me, VRSPACE.me.video);
          });
        }
      };
      if (!this.worldManager.isOnline()) {
        VRSPACE.addWelcomeListener(afterConnect);
        if (!VRSPACE.isConnected()) {
          // making sure reconnect is handled
          VRSPACE.connect(this.world.serverUrl);
        }
        const connectionListener = VRSPACE.addConnectionListener(async (connected, reconnecting) => {
          console.log('connected:' + connected);
          if (!connected) {
            if (!this.worldManager.isOnline()) {
              // initial connection failed
              reject(this);
            } else if (reconnecting) {
              this.trackProgress();
              // connection lost, reconnect in progress
              console.log("Reconnecting, user was authenticated: " + this.worldManager.authenticated);
            } else {
              console.log("connection lost and NOT reconnecting - return to login screen");
              this.closeProgress();
              window.location.reload();
            }
          } else if (this.worldManager.isOnline()) {
            // reconnect succeeded
            // TODO move this to dedicated cleanup method
            // clear the scene
            this.worldManager.removeAll();
            VRSPACE.removeErrorListener(errorListener);
            // clear audio/video session
            if (MediaStreams.instance) {
              MediaStreams.instance.close();
            }
            // ensure same workflow, sets online to false:
            this.worldManager.setSessionStatus(false);
            // this is going to be set up again
            VRSPACE.removeConnectionListener(connectionListener);
            // authenticated users may need to log in again
            if (this.worldManager.authenticated) {
              let authenticated = await this.api.getAuthenticated();
              console.log("Reconnecting, user was/is authenticated: " + this.worldManager.authenticated + "/" + authenticated);
              if (!authenticated) {
                // no automatic reconnect for authenticated users once authentication expires
                await this.api.oauth2login(this.worldManager.oauth2providerId, properties.name, properties.mesh);
              }
            }
            // restart enter procedure
            this.enter(properties).then(() => {
              this.worldManager.publishState();
              this.closeProgress();
            });
          }
        });
      } else if (this.world.name) {
        VRSPACE.addWelcomeListener(afterEnter);
        VRSPACE.sendCommand("Enter", { world: this.world.name });
      }
    });
  }

  trackProgress() {
    if (VRSPACEUI.indicator) {
      VRSPACEUI.indicator.add("Reconnect")
      VRSPACEUI.indicator.animate();
    }
  }
  closeProgress() {
    if (VRSPACEUI.indicator) {
      VRSPACEUI.indicator.remove("Reconnect")
    }
  }
  /** Called after user enters a world, calls world and world listener entered() methods wrapped in try/catch */
  entered(welcome) {
    try {
      this.world.entered(welcome);
    } catch (err) {
      console.log("Error in world entered", err);
    }
    this.world.worldListeners.forEach(listener => {
      try {
        if (listener.entered) {
          listener.entered(welcome);
        }
      } catch (error) {
        console.log("Error in world listener", error);
      }
    });
  }

  /**
   * Set up session based on properties and current state.
   * Called after connection is established, and before enter/start session.
   */
  configureSession(properties) {
    // CHECKME SoC
    if (this.worldManager.remoteLogging) {
      this.worldManager.enableRemoteLogging();
    }
    if (this.worldManager.tokens) {
      for (let token in this.worldManager.tokens) {
        VRSPACE.setToken(token, this.worldManager.tokens[token]);
      }
    }
    if (properties) {
      for (var prop in properties) {
        // publish own properties
        VRSPACE.sendMy(prop, properties[prop]);
        // and also set their values locally
        VRSPACE.me[prop] = properties[prop];
      }
    }
    // DO NOT start publishing after connect, but after enter
    // i.e. first make sure user is in the right space
    //this.pubSub(VRSPACE.me, VRSPACE.me.video);
  }

  /** 
   * Publish and subscribe audio/video. Expects user object to contain a valid token.
   * @param {Client} user Client object of the local user
   * @param {boolean} autoPublishVideo should webcam video be published as soon as possible
   */
  async pubSub(user, autoPublishVideo) {
    console.log("PubSub autoPublishVideo:" + autoPublishVideo, user);
    // CHECKME: should it be OpenVidu or general streaming service name?
    if (MediaStreams.instance && user.tokens && user.tokens.OpenViduMain) {
      console.log("Subscribing as User " + user.id + " with token " + user.tokens.OpenViduMain);
      // ask for webcam access permissions, but NOT while in XR
      if (!this.worldManager.world.inXR() && await MediaHelper.checkVideoPermissions()) {
        MediaStreams.instance.videoSource = undefined;
        MediaStreams.instance.startVideo = autoPublishVideo;
      }
      if (await MediaHelper.checkAudioPermissions()) {
        MediaStreams.instance.audioSource = undefined;
      } else {
        MediaStreams.instance.audioSource = false;
      }

      try {
        await MediaStreams.instance.connect(user.tokens.OpenViduMain)
        if (MediaStreams.instance.audioSource == undefined || MediaStreams.instance.videoSource == undefined) {
          // otherwise error
          MediaStreams.instance.publish();
        }
      } catch (exception) {
        console.error("Streaming connection failure", exception);
      }
    }
  }


}