Skip to content
Snippets Groups Projects
liowebrtc.js 11.64 KiB
import WildEmitter from 'wildemitter';
import attachMediaStream from 'attachmediastream';
import mockconsole from 'mockconsole';
import WebRTC from './webrtc';
import webrtcSupport from './webrtcsupport';
import SocketIoConnection from './socketioconnection';

class LioWebRTC extends WildEmitter {
  constructor(opts) {
    super();
    const self = this;
    const options = opts || {};
    const config = this.config = {
      url: 'https://sandbox.simplewebrtc.com:443/',
      socketio: { forceNew: true },
      connection: null,
      debug: false,
      localVideoEl: '',
      remoteVideosEl: '',
      enableDataChannels: true,
      autoRequestMedia: false,
      autoRemoveVideos: true,
      adjustPeerVolume: true,
      peerVolumeWhenSpeaking: 0.25,
      media: {
        video: true,
        audio: true
      },
      receiveMedia: {
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1
      },
      localVideo: {
        autoplay: true,
        mirror: false,
        muted: true
      }
    };

    let connection;
    this.logger = ((() => {
          // we assume that if you're in debug mode and you didn't
          // pass in a logger, you actually want to log as much as
          // possible.
      if (opts.debug) {
        return opts.logger || console;
      }
      return opts.logger || mockconsole;
    })());

      // set our config from options
    Object.keys(options).forEach(o => {
      this.config[o] = options[o];
    });

      // attach detected support for convenience
    this.capabilities = webrtcSupport;

      // call WildEmitter constructor
    WildEmitter.call(this);

      // create default SocketIoConnection if it's not passed in
    if (this.config.connection === null) {
      connection = this.connection = new SocketIoConnection(this.config);
    } else {
      connection = this.connection = this.config.connection;
    }

    connection.on('connect', () => {
      self.emit('connectionReady', connection.getSessionid());
      self.sessionReady = true;
      self.testReadiness();
    });

    connection.on('message', (message) => {
      const peers = self.webrtc.getPeers(message.from, message.roomType);
      let peer;

      if (message.type === 'offer') {
        if (peers.length) {
          peers.forEach((p) => {
            if (p.sid === message.sid) peer = p;
          });
                  // if (!peer) peer = peers[0]; // fallback for old protocol versions
        }
        if (!peer) {
          peer = self.webrtc.createPeer({
            id: message.from,
            sid: message.sid,
            type: message.roomType,
            enableDataChannels: self.config.enableDataChannels && message.roomType !== 'screen',
            sharemyscreen: message.roomType === 'screen' && !message.broadcaster,
            broadcaster: message.roomType === 'screen' && !message.broadcaster ? self.connection.getSessionid() : null
          });
          self.emit('createdPeer', peer);
        }
        peer.handleMessage(message);
      } else if (peers.length) {
        peers.forEach((peer) => {
          if (message.sid) {
            if (peer.sid === message.sid) {
              peer.handleMessage(message);
            }
          } else {
            peer.handleMessage(message);
          }
        });
      }
    });

    connection.on('remove', (room) => {
      if (room.id !== self.connection.getSessionid()) {
        self.webrtc.removePeers(room.id, room.type);
      }
    });

      // instantiate our main WebRTC helper
      // using same logger from logic here
    opts.logger = this.logger;
    opts.debug = false;
    this.webrtc = new WebRTC(opts);

      // attach a few methods from underlying lib to simple.
    ['mute', 'unmute', 'pauseVideo', 'resumeVideo', 'pause', 'resume', 'sendToAll', 'sendDirectlyToAll', 'getPeers'].forEach((method) => {
      self[method] = self.webrtc[method].bind(self.webrtc);
    });

      // proxy events from WebRTC
    this.webrtc.on('*', function () {
      self.emit(...arguments);
    });

      // log all events in debug mode
    if (config.debug) {
      this.on('*', this.logger.log.bind(this.logger, 'LioWebRTC event:'));
    }

      // check for readiness
    this.webrtc.on('localStream', () => {
      self.testReadiness();
    });

    this.webrtc.on('message', (payload) => {
      self.connection.emit('message', payload);
    });

    this.webrtc.on('peerStreamAdded', this.handlePeerStreamAdded.bind(this));
    this.webrtc.on('peerStreamRemoved', this.handlePeerStreamRemoved.bind(this));

      // echo cancellation attempts
    if (this.config.adjustPeerVolume) {
      this.webrtc.on('speaking', this.setVolumeForAll.bind(this, this.config.peerVolumeWhenSpeaking));
      this.webrtc.on('stoppedSpeaking', this.setVolumeForAll.bind(this, 1));
    }

    connection.on('stunservers', (args) => {
          // resets/overrides the config
      self.webrtc.config.peerConnectionConfig.iceServers = args;
      self.emit('stunservers', args);
    });
    connection.on('turnservers', (args) => {
          // appends to the config
      self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args);
      self.emit('turnservers', args);
    });

    this.webrtc.on('iceFailed', (peer) => {
          // local ice failure
    });
    this.webrtc.on('connectivityError', (peer) => {
          // remote ice failure
    });


      // sending mute/unmute to all peers
    this.webrtc.on('audioOn', () => {
      self.webrtc.sendToAll('unmute', { name: 'audio' });
    });
    this.webrtc.on('audioOff', () => {
      self.webrtc.sendToAll('mute', { name: 'audio' });
    });
    this.webrtc.on('videoOn', () => {
      self.webrtc.sendToAll('unmute', { name: 'video' });
    });
    this.webrtc.on('videoOff', () => {
      self.webrtc.sendToAll('mute', { name: 'video' });
    });

      // screensharing events
    this.webrtc.on('localScreen', (stream) => {
      let item;
      let el = document.createElement('video');
      let container = self.getRemoteVideoContainer();

      el.oncontextmenu = () => false;
      el.id = 'localScreen';
      attachMediaStream(stream, el);
      if (container) {
        container.appendChild(el);
      }

      self.emit('localScreenAdded', el);
      self.connection.emit('shareScreen');

      self.webrtc.peers.forEach((existingPeer) => {
        let peer;
        if (existingPeer.type === 'video') {
          peer = self.webrtc.createPeer({
            id: existingPeer.id,
            type: 'screen',
            sharemyscreen: true,
            enableDataChannels: false,
            receiveMedia: {
              offerToReceiveAudio: 0,
              offerToReceiveVideo: 0
            },
            broadcaster: self.connection.getSessionid(),
          });
          self.emit('createdPeer', peer);
          peer.start();
        }
      });
    });
    this.webrtc.on('localScreenStopped', (stream) => {
      if (self.getLocalScreen()) {
        self.stopScreenShare();
      }
          /*
          self.connection.emit('unshareScreen');
          self.webrtc.peers.forEach(function (peer) {
              if (peer.sharemyscreen) {
                  peer.end();
              }
          });
          */
    });

    this.webrtc.on('channelMessage', (peer, label, data) => {
      if (data.type === 'volume') {
        self.emit('remoteVolumeChange', data.payload, peer);
      } else {
        self.emit(data.type, data.payload, peer);
      }
    });

    if (this.config.autoRequestMedia) this.startLocalVideo();
  }

  leaveRoom() {
    if (this.roomName) {
      this.connection.emit('leave');
      while (this.webrtc.peers.length) {
        this.webrtc.peers[0].end();
      }
      if (this.getLocalScreen()) {
        this.stopScreenShare();
      }
      this.emit('leftRoom', this.roomName);
      this.roomName = undefined;
    }
  }

  disconnect() {
    this.connection.disconnect();
    delete this.connection;
  }

  handlePeerStreamAdded(peer) {
    const self = this;

    this.emit('videoAdded', peer.stream, peer);

      // send our mute status to new peer if we're muted
      // currently called with a small delay because it arrives before
      // the video element is created otherwise (which happens after
      // the async setRemoteDescription-createAnswer)
    setTimeout(() => {
      if (!self.webrtc.isAudioEnabled()) {
        peer.send('mute', { name: 'audio' });
      }
      if (!self.webrtc.isVideoEnabled()) {
        peer.send('mute', { name: 'video' });
      }
    }, 250);
  }

  handlePeerStreamRemoved(peer) {
    this.emit('videoRemoved', peer);
  }

  getDomId(peer) {
    return [peer.id, peer.type, peer.broadcaster ? 'broadcasting' : 'incoming'].join('_');
  }

  // set volume on video tag for all peers takse a value between 0 and 1
  setVolumeForAll(volume) {
    this.webrtc.peers.forEach((peer) => {
      if (peer.videoEl) peer.videoEl.volume = volume;
    });
  }

  joinRoom(name, cb) {
    const self = this;
    this.roomName = name;
    this.connection.emit('join', name, (err, roomDescription) => {
      if (err) {
        self.emit('error', err);
      } else {
        let id;
        let client;
        let type;
        let peer;

        for (id in roomDescription.clients) {
          client = roomDescription.clients[id];
          for (type in client) {
            if (client[type]) {
              peer = self.webrtc.createPeer({
                id,
                type,
                enableDataChannels: self.config.enableDataChannels && type !== 'screen',
                receiveMedia: {
                  offerToReceiveAudio: type !== 'screen' && self.config.receiveMedia.offerToReceiveAudio ? 1 : 0,
                  offerToReceiveVideo: self.config.receiveMedia.offerToReceiveVideo
                }
              });
              self.emit('createdPeer', peer);
              peer.start();
            }
          }
        }
      }

      if (cb) cb(err, roomDescription);
      self.emit('joinedRoom', name);
    });
  }

  startLocalVideo() {
    const self = this;
    this.webrtc.start(this.config.media, (err, stream) => {
      if (err) {
        self.emit('localMediaError', err);
      } else {
        attachMediaStream(stream, this.config.localVideoEl, { muted: true });
      }
    });
  }

  stopLocalVideo() {
    this.webrtc.stop();
  }

  shareScreen(cb) {
    this.webrtc.startScreenShare(cb);
  }

  getLocalScreen() {
    return this.webrtc.localScreens && this.webrtc.localScreens[0];
  }

  stopScreenShare() {
    this.connection.emit('unshareScreen');
    const videoEl = document.getElementById('localScreen');
    const container = this.getRemoteVideoContainer();

    if (this.config.autoRemoveVideos && container && videoEl) {
      container.removeChild(videoEl);
    }

      // a hack to emit the event the removes the video
      // element that we want
    if (videoEl) {
      this.emit('videoRemoved', videoEl);
    }
    if (this.getLocalScreen()) {
      this.webrtc.stopScreenShare();
    }
    this.webrtc.peers.forEach((peer) => {
      if (peer.broadcaster) {
        peer.end();
      }
    });
  }

  testReadiness() {
    const self = this;
    if (this.sessionReady) {
      if (!this.config.media.video && !this.config.media.audio) {
        self.emit('readyToCall', self.connection.getSessionid());
      } else if (this.webrtc.localStreams.length > 0) {
        self.emit('readyToCall', self.connection.getSessionid());
      }
    }
  }

  createRoom(name, cb) {
    this.roomName = name;
    if (arguments.length === 2) {
      this.connection.emit('create', name, cb);
    } else {
      this.connection.emit('create', name);
    }
  }

  sendFile() {
    if (!webrtcSupport.dataChannel) {
      return this.emit('error', new Error('DataChannelNotSupported'));
    }
  }
}

export default LioWebRTC;