lazorfuzz / liowebrtc

An event-based WebRTC library that makes it easy to embed real-time peer to peer communication into UI components.
MIT License
164 stars 10 forks source link
electronjs javascript javascript-library liowebrtc p2p-chat p2p-cloud p2p-everything p2p-network p2p-transfer peer-communication react reactjs webrtc

LioWebRTC

NPM Downloads

A WebRTC library that makes it easy to embed scalable peer to peer communication into UI components.

LioWebRTC works standalone, but it is also compatible with React, Vue, Electron, etc. It can be configured for scalability using partial mesh networks, making it possible to emit data to thousands of peers in a room, while only needing to be connected to at least one other peer in the room.

Click here to see a chatroom demo built with LioWebRTC.

Click here to see a video conferencing demo app built with LioWebRTC.

Using LioWebRTC with React

React developers may want to take a look at react-liowebrtc.

Usage

Installation

yarn add liowebrtc
// Or
npm i liowebrtc

Import LioWebRTC

import LioWebRTC from 'liowebrtc';

Create LioWebRTC instance

By default, this enables video, audio, and data channels.

const webrtc = new LioWebRTC({
    localVideoEl: localVideoIdOrRef, // The local video element
    autoRequestMedia: true, // Immediately request camera and mic access upon initialization
    debug: true, // Displays events emitted by liowebrtc in the console
    url: 'https://your-signaling-server.com:443/' // The url for your signaling server. If no url is passed, liowebrtc uses the default demo signaling server. (The default server is for demo purposes only, and is not reliable. Plus, I'm the only one paying for it 🙁. Please use your own in production!)
});

Data channels only

Disable video/audio streaming, and only allow data channels.

const webrtc = new LioWebRTC({
    dataOnly: true
});

Audio and data channels only

Great for voice calls.

const webrtc = new LioWebRTC({
    autoRequestMedia: true,
    media: {
        video: false,
        audio: true
    }
});

Partial mesh network

Peers only form direct connections with a maximum of maxPeers and a minimum of minPeers. shout()ing still works because peers wil re-propagate messages to other peers. Note: partial mesh networks only work if you're only using dataOnly.

const webrtc = new LioWebRTC({
  dataOnly: true,
  network: {
    maxPeers: 8,
    minPeers: 4
  }
})

Join a room once it's ready

webrtc.on('ready', () => {
    // Joins a room if it exists, creates it if it doesn't
    webrtc.joinRoom('your room name');
});

Emitting to the hive

Sometimes a peer wants to let every other peer in the room to know about something. This can be accomplished with shout(messageType, payload)

webrtc.shout('event-label', { success: true, payload: '137' });

Now for the recipients, handle the peer event with a listener:

webrtc.on('receivedPeerData', (type, data, peer) => {
    if (type === 'event-label' && data.success) {
        console.log(`Peer ${peer.id} emitted ${data.payload}`);
    }
});

Communicating with a single peer

Sometimes a peer only wants to send data directly to another peer. This can be accomplished with whisper(peer, messageType, payload)

webrtc.whisper(peer, 'directMessage', { msg: 'Hello world!' });

Receiving the message is the same as handling a peer event:

webrtc.on('receivedPeerData', (type, data, peer) => {
    if (type === 'directMessage') console.log(`Peer ${peer.id} says: ${data.msg}`);
});

Live-syncing state

componentDidUpdate(prevProps, prevState) {
    if (this.state.position !== prevState.position) {
        this.webrtc.shout('stateUpdate', this.state);
    }
}
});

All communications via shout/whisper are sent over the default data channel and emitted by the LioWebRTC instance as events. You can create your own custom listeners suited for whatever purpose you'd like.

Attaching a peer's media stream to a video element

webrtc.on('peerStreamAdded', (stream, peer) => {
    webrtc.attachStream(stream, yourVideoElementOrRef);
});

Example

P2P Video Chat Component

import React, { Component } from 'react';
import LioWebRTC from 'liowebrtc';

class Party extends Component {
  constructor(props) {
    super(props);
    this.state = {
      nick: this.props.nick,
      roomID: `party-${this.props.roomName}`,
      muted: false,
      camPaused: false,
      peers: []
    };
    this.remoteVideos = {};
  }

  componentDidMount() {
    this.webrtc = new LioWebRTC({
      // The url for your signaling server. Use your own in production!
      url: 'https://sm1.lio.app:443/',
      // The local video ref set within your render function
      localVideoEl: this.localVid,
      // Immediately request camera access
      autoRequestMedia: true,
      // Optional: nickname
      nick: this.state.nick,
      debug: true
    });

    this.webrtc.on('peerStreamAdded', this.addVideo);
    this.webrtc.on('removedPeer', this.removeVideo);
    this.webrtc.on('ready', this.readyToJoin);
    this.webrtc.on('iceFailed', this.handleConnectionError);
    this.webrtc.on('connectivityError', this.handleConnectionError);
  }

  addVideo = (stream, peer) => {
    this.setState({ peers: [...this.state.peers, peer] }, () => {
      this.webrtc.attachStream(stream, this.remoteVideos[peer.id]);
    });
  }

  removeVideo = (peer) => {
    this.setState({
      peers: this.state.peers.filter(p => !p.closed)
    });
  }

  handleConnectionError = (peer) => {
    const pc = peer.pc;
    console.log('had local relay candidate', pc.hadLocalRelayCandidate);
    console.log('had remote relay candidate', pc.hadRemoteRelayCandidate);
  }

  readyToJoin = () => {
    // Starts the process of joining a room.
    this.webrtc.joinRoom(this.state.roomID, (err, desc) => {
    });
  }

  // Show fellow peers in the room
  generateRemotes = () => this.state.peers.map((p) => (
    <div key={p.id}>
      <div id={/* The video container needs a special id */ `${this.webrtc.getContainerId(p)}`}>
        <video
          // Important: The video element needs both an id and ref
          id={this.webrtc.getDomId(p)}
          ref={(v) => this.remoteVideos[p.id] = v}
          />
      </div>
        <p>{p.nick}</p>
    </div>
    ));

  disconnect = () => {
    this.webrtc.quit();
  }

  componentWillUnmount() {
    this.disconnect();
  }

  render() {
    return (
      <div>
        <div>
            <video
              // Important: The local video element needs to have a ref
              ref={(vid) => { this.localVid = vid; }}
            />
            <p>{this.state.nick}</p>
        </div>
        {this.generateRemotes()}
      </div>
    );
  }
}

export default Party;

API

Constructor Options

new LioWebRTC(options)

Fields

connection - the socket signaling connection

webrtc - the underlying WebRTC session manager

Events

To set up event listeners, use the LioWebRTC instance created with the constructor. Example:

// Emitted when a peer's media stream becomes available
this.webrtc.on('peerStreamAdded', (stream, peer) => {
    // Attach the MediaStream to a video element
    // this.webrtc.attachStream(stream, this.remoteVideos[peer.id]);
});
// Emitted when we receive data from a peer via the data channel
this.webrtc.on('receivedPeerData', (type, payload, peer) => {
    // Find something to do with the data
});

'channelOpen', RTCDataChannel, peer - emitted when a new channel is established with a peer.

'connectionReady', sessionId - emitted when the signaling connection emits the connect event, with the unique id for the session.

'createdPeer', peer - this will be emitted when:

'joinedRoom', roomName - emitted after successfully joining a room.

'leftRoom', roomName - emitted after successfully leaving the current room, ending all peers, and stopping local stream

'mute', data - emitted when a peer mutes their video or audioOn

'removedPeer', peer - emitted when a peer loses connection or exits the room

'ready', sessionId - emitted when liowebrtc is ready to join a room

'receivedPeerData', type, payload, peer - emitted when data is received from a peer that sent the data with shout or whisper

'receivedSignalData', type, payload, peer - emitted when data is received from a peer that sent the data via the socket.io signaling server with broadcast or transmit

'stunservers', [...args] - emitted when the signaling server emits a list of stun servers.

'turnservers', [...args] - emitted when the signaling server emits a list of turn servers.

'unmute', data - emitted when a peer mutes their video or audioOn

'peerStreamAdded', stream, peer - emitted when a peer's MediaStream becomes available

'peerStreamRemoved', peer - emitted when a peer stream is removed

Methods

attachStream(stream, el, opts) - attaches a media stream to a video or audio element

broadcast(messageType, payload) - broadcasts a message to all peers in the room via the signaling server (similar to shout, but not p2p). Listen for peers' broadcasts on the receivedSignalData event.

createRoom(name, callback) - emits the create event and optionally invokes callback on response

disconnect() - calls disconnect on the signaling connection and deletes it. Peers will still be available

emit(eventLabel, ...args) - emit arbitrary event (Emits locally. To emit stuff other peers, use shout)

getClients((err, clients)) - asks the socket.io signaling server for a list of peers currently in the room.

getContainerId(peer) - get the DOM id associated with a peer's media element. In JSX, you will need to set the id of the container element to this value

getMyId() - get your own peer ID

getDomId(peer) - get the DOM id associated with a peer's media stream. In JSX, you will need to set the id of the peer's media element to this value.

getPeerById(id) - returns a peer with a given id

getPeerByNick(nick) - returns a peer with a given nick

getPeers(sessionId) - returns all peers by sessionId

joinRoom(name, callback) - joins the room name. Callback is invoked with callback(err, roomDescription) where roomDescription is yielded by the connection on the join event. See SignalBuddy for more info.

leaveRoom() - leaves the currently joined room and stops local streams

mute() - mutes the local audio stream to your peers (stops sending audio in the WebRTC audio channel)

on(ev, fn) - creates an event listener for event ev handled by fn

pause() - pauses both video and audio streams to your peers

pauseVideo() - pauses the video stream to your peers (stops sending video in the WebRTC video channel)

quit() - stops the local video, leaves the currently joined room, and disconnects from the signaling server

resume() - resumes sending video and audio to your peers

resumeVideo() - resumes the video stream to your peers (resumes sending video in the WebRTC video channel)

sendDirectlyToAll(messageType, payload, channel) - sends a message to all peers in the room via a data channel (same as shout, except you can specify your own data channel. Use this if you need to set up a new data channel, e.g. a dedicated file-sharing channel, etc.)

setVolumeForAll(volume) - set the volume level for all peers

shout(messageType, payload) - sends a message to all peers in the room via the default p2p data channel. Listen for peers' shouts on the receivedPeerData event.

startLocalVideo() - starts the local video or audio streams with the media options provided in the config. Use this if autoRequestMedia is set to false

stopLocalVideo() - stops all local media streams

transmit(peer, messageType, payload) - sends a message to a single peer in the room via the signaling server (similar to whisper, but not p2p). Listen for peers' transmissions on the receivedSignalData event.

unmute() - unmutes the audio stream to your peers (resumes sending audio in the WebRTC audio channel)

whisper(peer, messageType, payload) - sends a message to a single peer in the room via the default p2p data channel. Listen for peers' whispers on the receivedPeerData event.

Signaling

WebRTC needs to be facilitated with signaling; a service that acts as a matchmaker for peers before they establish direct video/audio/data channels. Signaling can be done in any way, e.g. via good old fashioned carrier pigeons. Signaling services only need to fulfill the absolute minimal role of matchmaking peers.

SignalBuddy is a scalable socket.io signaling server, and is very easy to set up. socket.io enables real-time, bidirectional communication between a client and server via web sockets. It also allows us to easily segment peers into rooms.

For emitting data to peers, LioWebRTC provides a unified, event-based API that enables peers to seamlessly switch between shouting (p2p data channels) or broadcasting (socket.io) to all the peers in a room. It's up to you to decide which protocol to use, but socket.io should ideally only be used for transmitting things like metadata, one-off events, etc. Both protocols are real-time, bidirectional, and event-based.

Connection

LioWebRTC wraps socketio-client and returns a connection object. This the connection to the signaling server. The connection object comes with the following methods:

Signaling Server Url

LioWebRTC uses SignalBuddy to facilitate signaling. LioWebRTC works out of the box with a demo SignalBuddy server that was intended for testing purposes. However, for production purposes, IT IS NOT RELIABLE. In production, you will need to set up your own SignalBuddy server (or any other socket.io solution that implements matchmaking).