Home Reference Source

src/Remon.js

// import "webrtc-adapter";
import platform from "platform";
import Config from "./Configure";
import Context from "./Context";
import Media from "./Media";
import EventManager from "./EventManager";
import signalingStates from "./SignalingStates";
import bindPeerConnectionEvents from "./PeerConnectionHandler";
import SignalingConnection from "./SignalingConnection";
import bindSignalingConnectionEvents from "./SignalingConnectionHandler";
import RemonRecorder from "./RemonRecorder";
import util from "./Util";
import l from "./Logger";
import adapter from "webrtc-adapter";

/**
 * Most important class for using RemoteMonster API. It can be use to P2P communication and broadcast. You can receive callback events from listener.
 */
class Remon {
  /**
   * create Remon object with config object and listener object.
   * example: var v = new Remon({config: rtcConfig, listener: rtcListener});
   */
  constructor({ config, listener }) {
    this.version = "2.3.0-beta.1";

    this.context = new Context();
    this.context.sdkVersion = this.version;
    this.context.logServer =
      config.logServer && config.logServer.url
        ? config.logServer
        : Config.logServer;
    this.context.eventManager = EventManager();
    this.config = config;
    this.context.simulcast =
      this.config.rtc && this.config.rtc.simulcast
        ? this.config.rtc.simulcast
        : Config.rtc.simulcast;
    this.context.logLevel =
      this.config.dev && this.config.dev.logLevel
        ? this.config.dev.logLevel
        : "INFO";
    util.validateConfig(this.context, this.config);
    this.media = new Media(this.context);
    this.context.mediaManager = this.media;
    this.uri = Config.appServer.url;
    this.key = this.config.credential.key;
    this.serviceId = this.config.credential.serviceId;
    this.context.key = this.key;
    this.context.serviceId = this.serviceId;
    this.context.state = "INIT";

    l.init(this.context);
    if (listener) {
      Object.keys(listener).forEach(type => {
        const listenerItem = listener[type];
        this.context.eventManager.addEventListener({ type, listenerItem });
      });
    }
    if (!this.config.rtc) this.config.rtc = Config.rtc;
    if (!this.config.media)
      this.config.media = { audio: true, video: true, record: false };
    if (this.config.media.record) {
      this.context.useRecord = this.config.media.record;
      if (this.config.media.recordUrl)
        this.context.recordUrl = this.config.media.recordUrl;
      else
        this.context.recordUrl = "https://demo.remotemonster.com/rest/record";
    }
    Config.media = this.config.media;
    Config.view = this.config.view;

    if (this.config.media.video.codec)
      this.context.videoCodec = this.config.media.video.codec;
    if (this.config.media.audio.codec)
      this.context.audioCodec = this.config.media.audio.codec;
    if (this.config.media.video === false) this.context.useVideo = false;
    if (this.config.media.audio === false) this.context.useAudio = false;
    if (this.config.media.video.maxBandwidth)
      this.context.videoBandwidth = this.config.media.video.maxBandwidth;
    if (this.config.media.audio.maxBandwidth)
      this.context.audioBandwidth = this.config.media.audio.maxBandwidth;
    if (this.config.credential.resturl) {
      this.config.credential.resturl = this.config.credential.resturl.replace(
        "/init",
        ""
      );
      Config.appServer.url = this.config.credential.resturl;
      this.uri = Config.appServer.url;
    }
    if (this.config.credential.wsurl)
      Config.signalingServer.url = this.config.credential.wsurl;
    //this.init();
  }

  async init() {
    l.d("init is called");
    var that = this;
    var ctx = this.context;
    var cfg = this.config;
    var messageBody = {
      credential: { key: this.key, serviceId: this.serviceId },
      env: {
        os: platform.os.family,
        osVersion: platform.os.version || "0",
        device: platform.name,
        deviceVersion: platform.version || "0",
        networkType: Navigator.connection,
        sdkVersion: this.version
      }
    };
    if (this.config.sdk && this.config.sdk.country)
      messageBody.env.country = this.config.sdk.country;
    if (this.config.media.roomid) messageBody.id = this.config.media.roomid;
    var message = {
      method: "POST",
      headers: {
        Accept: "application/json, text/plain, */*",
        "Content-Type": "application/json"
      },
      body: JSON.stringify(messageBody)
    };
    try {
      var response = await fetch(this.uri + "/init", message);
      var responseJson = await response.json();
    } catch (e) {
      if (ctx.eventManager.hasEventListener("onError")) {
        ctx.eventManager.dispatchEvent("onError", "WebSocketFailedError");
      }
      l.e("Init: failed:", error);
      l.errorEvt(ctx, "1004", "init failed:" + error);
    }
    l.d("-> Message:", responseJson);

    Object.keys(responseJson).forEach(responseJsonKey => {
      switch (responseJsonKey) {
        case "iceServers": {
          responseJson[responseJsonKey].forEach(x =>
            Config.rtc.iceServers.push(x)
          );
          break;
        }
        case "token": {
          ctx.token = responseJson[responseJsonKey];
          break;
        }
        case "key": {
          ctx.channel.id = responseJson[responseJsonKey];
          break;
        }
        case "name": {
          ctx.channel.name = responseJson[responseJsonKey];
          break;
        }
        default: {
          // l.e("Init: Unknown property:" + responseJsonKey);
        }
      }
    });
    var eventMsg = {
      topic: "log",
      messages: {
        log: "Peer Id is created : " + ctx.token,
        logLevel: "info",
        os: platform.os.family,
        osVersion: platform.os.version || "0",
        device: platform.name,
        deviceVersion: platform.version || "0",
        networkType: Navigator.connection,
        sdkVersion: this.version,
        svcId: ctx.serviceId,
        pId: ctx.token,
        status: "INIT"
      }
    };
    l.evt(JSON.stringify(eventMsg));
    ctx.signalingConnection = new SignalingConnection({
      url: Config.signalingServer.url,
      context: ctx
    });
    ctx.signalingConnection.connect();
    ctx.signalingConnection.on("reconnect", () => {
      this.onReconnectSignalConnection();
    });
    ctx.signalingConnection.on("disconnect", () => {
      this.onDisconnectSignalConnection();
    });
    window.addEventListener(
      "offline",
      () => {
        l.i("Browser: offline");
        // if (this.context.eventManager.hasEventListener("onError")) {
        //   this.context.eventManager.dispatchEvent("onError", "disconnected");
        // }
        // this.close("UNKNOWN");
        this.context.signalingConnection.onOffline();
      },
      false
    );

    if (cfg.rtc.audioType === "music") {
      cfg.opt = {
        mandatory: {
          googHighpassFilter: false,
          googEchoCancellation: false,
          googNoiseSuppression: false
        },
        optional: [{ googCpuOveruseDetection: false }]
      };
    }
    ctx.peerConnection = new RTCPeerConnection(Config.rtc, cfg.opt);
    ctx.hasAddTrack = ctx.peerConnection.addTrack !== undefined;
    bindSignalingConnectionEvents({
      context: ctx,
      media: that.media,
      config: cfg
    });
    bindPeerConnectionEvents({ context: ctx, media: that.media });
    if (cfg.view && typeof cfg.view.local !== "undefined")
      Config.rtc.localVideo = document.querySelector(`${cfg.view.local}`);
    if (cfg.view && typeof cfg.view.remote !== "undefined") {
      ctx.remoteVideo = document.querySelector(`${cfg.view.remote}`);
    }
    if (cfg.media.recvonly) {
      ctx.remoteVideo = document.querySelector(`${cfg.view.remote}`);
    }

    const MAX_RETRIES = 11;
    for (let i = 5; i <= MAX_RETRIES; i++) {
      // ctx.signalingConnection connection state와 localmedia 들어왔는지, ctx.peerConnection이 제대로 생성되었는지 체크 후 return
      if (
        ctx.signalingConnection.isOpened()
        // ctx.mediaManager.isLocalPrepared()
      ) {
        return;
      } else {
        const timeout = Math.pow(2, i);
        l.v("wating for init %i", i);
        await this.wait(timeout);
      }
    }
    // if (ctx.eventManager.hasEventListener("onError")) {
    //   ctx.eventManager.dispatchEvent("onError", "Error is successfully dispatched");
    // }
    try {
      ctx.devices = await navigator.mediaDevices.enumerateDevices();
      ctx.currentVideoDeviceId = ctx.devices[0].deviceId;
    } catch (e) {
      console.log(e);
      l.errorEvt(ctx, "1007", "failed to get media devices: " + e);
    }
  }

  async connectCall(...args) {
    l.d("connect is called");
    await this.connectChannel(...args);
  }

  /**
   * Create P2P channel, if there is no P2P channel with the id. Join the P2P channel, if there is P2P channel with the id.
   * example: remon.connectChannel("roomname1");
   */
  async connectChannel(...args) {
    l.d("createChannel is called");
    this.config.rtc.audioType = "voice";
    await this.init();
    return this.context.signalingConnection.connectChannel(...args);
  }

  /**
   * Create a broadcast room
   * @param (string) roomname name of broadcast room. it is no id but name. You can take a real room id from onCreateChannel event
   */
  async createCast(roomname) {
    l.d("createCast is called");
    this.config.rtc.audioType = "music";
    await this.init();
    this.context.signalingConnection.createBroadcastChannel(roomname);
  }

  /**
   * Join a room by room id.
   * @param (string) room id
   */
  async joinCast(roomid) {
    l.d("joinCast is called");
    this.config.rtc.audioType = "music";
    this.context.channel.type = "VIEWER";
    this.config.media.recvonly = true;
    await this.init();
    this.context.signalingConnection.createViewerChannel(roomid);
  }

  /**
   * retrieve current stream health information
   */
  getHealth() {
    return this.context.health.result;
  }
  /**
   * retrieve current remon state information
   */
  getState() {
    return this.context.state;
  }
  /**
   * retrieve current sdk version
   */
  getVersion() {
    return this.version;
  }
  /**
   * get channel id
   */
  getChannelId() {
    return this.context.channel.id;
  }
  /**
   * mute local video
   * @param (bool)
   */
  pauseLocalVideo(bool) {
    l.d("pauseLocalVideo is called");
    this.media
      .mediaStreamTrackSwitch(Config.rtc.localStream)
      .type("Video")
      .enabled(!!bool);
  }
  /**
   * mute remote video
   * @param (bool) bool
   */
  pauseRemoteVideo(bool) {
    l.d("pauseRemoteVideo is called");
    this.media
      .mediaStreamTrackSwitch(this.context.remoteStream)
      .type("Video")
      .enabled(!!bool);
  }

  /**
   * switch camera between fore and back
   */
  cameraSwitch() {
    l.d("cameraSwitch is called");
    this.media.setUserDevices(null, this.context.devices[1].deviceId);
  }
  /**
   * mute local audio and mic stream
   * @param {bool} bool
   */
  muteLocalAudio(bool) {
    this.media
      .mediaStreamTrackSwitch(Config.rtc.localStream)
      .type("Audio")
      .enabled(!!bool);
  }
  /**
   * mute remote audio stream
   * @param {*} bool
   */
  muteRemoteAudio(bool) {
    this.media
      .mediaStreamTrackSwitch(this.context.remoteStream)
      .type("Audio")
      .enabled(!!bool);
  }
  async fetchCalls(id) {
    return await this.search(id);
  }

  setVideoQulity(quility) {
    this.context.signalingConnection.setSimulcastPriority(quility);
  }
  /**
   * search P2P channels by id.
   * @param (string) id for search. It can be part of full id
   */
  search(id) {
    l.d("search by" + id);
    // const message = {
    //   command: "search", token: this.context.token,
    //   serviceId: this.context.serviceId, body: id
    // };
    // this.context.signalingConnection.send(JSON.stringify(message));
    const message = {
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    };
    return new Promise((resolve, reject) => {
      fetch(
        this.uri + "/call/" + this.config.credential.serviceId,
        message
      ).then(response => {
        response
          .json()
          .then(responseJson => {
            if (this.context.eventManager.hasEventListener("onSearch")) {
              this.context.eventManager.dispatchEvent("onSearch", responseJson);
            }
            resolve(responseJson);
          })
          .catch(err => {
            reject(err);
            l.errorEvt(this.context, "1008", "search is  failed:" + err);
          });
      });
    });
  }

  async fetchCasts() {
    return await this.liveRooms();
  }
  /**
   * Retrieve all broadcast rooms information
   */
  liveRooms() {
    const message = {
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    };
    return new Promise((resolve, reject) => {
      fetch(
        this.uri + "/room/" + this.config.credential.serviceId,
        message
      ).then(response => {
        response
          .json()
          .then(responseJson => {
            resolve(responseJson);
          })
          .catch(err => {
            reject(err);
            l.errorEvt(this.context, "1008", "search is  failed:" + err);
          });
      });
    });
  }
  /**
   * It's only function for P2P communication. send message to peer
   * @param (string) userMessage message to peer
   */
  sendMessage(userMessage) {
    l.g("Signaling: Send user message");
    const message = this.context.signalingConnection.createMessage({
      command: "message",
      body: userMessage,
      code: ""
    });
    l.d("Message ->:", message);
    this.context.signalingConnection.send(JSON.stringify(message));
  }

  onReconnectSignalConnection() {
    console.log("event: onReconnectSignalConnection");
    this.context.signalingConnection.reconnectChannel();
  }

  onDisconnectSignalConnection() {
    console.log("event: onDisconnectSignalConnection");
    if (this.context.eventManager.hasEventListener("onStateChange")) {
      this.context.eventManager.dispatchEvent("onStateChange", "CLOSE");
    }
    this.close("UNKNOWN");
  }

  /**
   * close all Remon's resources
   */
  close(closeType) {
    l.i("Remon.close");

    if (this.context.useRecord && this.context.remoteRecorder) {
      this.context.remoteRecorder.stop();
      this.context.remoteRecorder = null;
    }
    if (this.context.useRecord && this.context.localRecorder) {
      this.context.localRecorder.stop();
      this.context.localRecorder = null;
      this.context.useRecord = false;
    }
    if (this.context.remoteVideo && this.context.remoteVideo.srcObject) {
      this.context.remoteVideo.srcObject
        .getTracks()
        .forEach(track => track.stop());
      this.context.remoteVideo.srcObject = null;
    }

    if (Config.rtc.localVideo && Config.rtc.localVideo.srcObject) {
      Config.rtc.localVideo.srcObject
        .getTracks()
        .forEach(track => track.stop());
    }
    if (!this.context.signalingConnection) return;
    //this.context.localVideo.srcObject = null;
    // FIXME: Chrome, adapter does not support addTrack.
    if (!this.context.peerConnection) return;
    if (this.context.health) this.context.health.stop();
    if (this.context.hasAddTrack) {
      this.context.peerConnection.ontrack = null;
    } else {
      this.context.peerConnection.onaddstream = null;
    }
    this.context.peerConnection.onremovestream = null;
    this.context.peerConnection.onicecandidate = null;
    this.context.peerConnection.oniceconnectionstatechange = null;
    this.context.peerConnection.onsignalingstatechange = null;
    this.context.peerConnection.onicegatheringstatechange = null;
    this.context.peerConnection.onnegotiationneeded = null;
    if (this.context.peerConnection.signalingState !== "closed") {
      this.context.peerConnection.close();
    }
    this.context.peerConnection = null;
    this.context.signalingConnection.close();

    if (closeType) {
      this.context.eventManager.dispatchEvent("onClose", {
        closeType
      });
    } else {
      this.context.eventManager.dispatchEvent("onClose", {
        closeType: "MINE"
      });
    }

    var eventMsg = {
      topic: "log",
      messages: {
        log: "remon is closed",
        logLevel: "info",
        sdkVersion: this.version,
        svcId: this.context.serviceId,
        pId: this.context.token,
        chId: this.context.channel.id,
        status: "CLOSE"
      }
    };
    l.evt(JSON.stringify(eventMsg));
  }

  wait(timeout) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, timeout);
    });
  }
}

export default Remon;