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';

/**
 * 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;
    // probably not instantiated at the moment of creation
    this.mediaStreams = worldManager.mediaStreams
    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));
          });
          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);
          });
        }    
      };
      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 ( this.mediaStreams ) {
              this.mediaStreams.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];
      }
    }
    // start publishing video only for video avatar currently displaying video
    //this.pubSub(welcome.client.User, VRSPACE.me.video);
    this.pubSub(VRSPACE.me, VRSPACE.me.video);
  }
  
  /** 
   * Publish and subscribe
   * @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) {
    //this.mediaStreams = this.worldManager.mediaStreams // may not be initialized
    console.log("PubSub autoPublishVideo:"+autoPublishVideo, user);
    // CHECKME: should it be OpenVidu or general streaming service name?
    if (this.mediaStreams && user.tokens && user.tokens.OpenViduMain) {
      console.log("Subscribing as User " + user.id + " with token " + user.tokens.OpenViduMain);
      // ask for webcam access permissions
      if ( await MediaHelper.checkVideoPermissions() ) {
        this.mediaStreams.videoSource = undefined;
        this.mediaStreams.startVideo = autoPublishVideo;
      }
      if (await MediaHelper.checkAudioPermissions()) {
        this.mediaStreams.audioSource = undefined;
      } else {
        this.mediaStreams.audioSource = false;        
      }
      
      try {
        await this.mediaStreams.connect(user.tokens.OpenViduMain)
        // TODO use static instance instead
        this.worldManager.avatarLoader.mediaStreams = this.mediaStreams;
        this.worldManager.meshLoader.mediaStreams = this.mediaStreams;
        // we may need to pause/unpause audio publishing during speech input
        // TODO figure out how to use instance
        VRSPACEUI.hud.speechInput.constructor.mediaStreams = this.mediaStreams;
        if ( this.mediaStreams.audioSource == undefined || this.mediaStreams.videoSource == undefined ) {
          // otherwise error
          this.mediaStreams.publish();
        }
      } catch ( exception ) {
        console.error("Streaming connection failure", exception);
      }
    }
  }

  
}