CreateIT
CreateIT
BLOG

SendBird Calls – example in React

Ninja peeking from behind a smartphone

SendBird Calls – example in React

SHARE

Challenge:
use peer-to-peer video and voice calls in React
Solution:
utilize the sendbird-calls NPM package

In 2020, SendBird added video and voice capabilities to their API. SendBird Calls is a toolbox that provides methods to make or receive a call. You can also mute your microphone or disable the camera. Regarding pricing, all calls are billed per minute.

We’re going to implement Video/Voice calls in our Application using a Peer-to-peer connection (Direct Call). One user will be able to call another user. Regarding video quality, it offers 24 FPS (Frames Per Second) and 1280 x 720, standard HD resolution. This article was inspired by a Community post published here: https://community.sendbird.com/t/sendbird-calls-react-js-single-component-audio-only/91

Sendbird-calls

Sendbird Calls enables real-time calls between users within a Sendbird application. To make a direct voice or video call, the caller specifies user ID and dials. Upon dialing, all of the callee’s authenticated devices will receive notifications about an incoming call. The callee can then choose to accept or decline the call from any of the devices. When the call is accepted, a connection is established between the devices of the caller and the callee.

Video call app window with a black screen and buttons above and below

SBCalls in React

Our main dependency will be sendbird-calls, for styling we will add the MUI Material:

npm install sendbird-calls
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material

Make sure to change the APP_ID variable to your own SendBird Application ID.

// src/App.jsx
import React, {useState} from 'react';
import './App.css';
import SBCall from "./Calls";
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
const APP_ID = "ABC";
function App() {
    const [initAction, setInitAction] = useState(null);
    const queryParams = new URLSearchParams(window.location.search);
    const USER_ID = queryParams.get('user_id');
    let button1Color = "inherit";
    let button2Color = "inherit";
    if (initAction === 'initVideo') {
        button1Color = "primary";
    }
    if (initAction === 'initVoice') {
        button2Color = "primary";
    }
    return (
        <div className="App">
            <p>MY user ID: {USER_ID}</p>
            <Box
                display="flex"
                justifyContent="center"
                alignItems="center"
            >
                <Button sx={{margin: 2}}
                        onClick={() => setInitAction("initVideo")}
                        variant="contained"
                        color={button1Color}>Init Video Call</Button>
                <Button sx={{margin: 2}}
                        onClick={() => setInitAction("initVoice")}
                        variant="contained"
                        color={button2Color}>Init Voice Call</Button>
            </Box>
            {initAction === "initVideo" ? <SBCall appId={APP_ID} userId={USER_ID} initAction="initVideo"/> : ''}
            {initAction === "initVoice" ? <SBCall appId={APP_ID} userId={USER_ID} initAction="initVoice"/> : ''}
        </div>
    );
}
export default App;

And some basic styles for the container:

/**
src/App.css
 */
body {
  padding:0 20px;
}
.App {
  text-align: center;
  max-width: 500px;
  border: 5px solid orangered;
  padding: 3rem;
  margin: 20px auto;
}

The Main React Component, responsible for authentication, establishing a direct call (video or audio):

// src/Calls/index.js
import './index.css';
import React from "react";
import SendBirdCall from "sendbird-calls";
import {callStates} from "./settings";
import {
  IconButton as MIconButton
} from "@mui/material";
import { Call as CallIcon } from "@mui/icons-material";
import { CallEnd as CallEndIcon } from "@mui/icons-material";
import { VolumeOff as VolumeOffIcon } from "@mui/icons-material";
import { VolumeUp as VolumeUpIcon } from "@mui/icons-material";
import { Videocam as VideocamIcon } from "@mui/icons-material";
import { VideocamOff as VideocamOffIcon } from "@mui/icons-material";
class SBCall extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      appId: props.appId,
      userId: props.userId,
      targetUserId: '',
      info: "Waiting...",
      call: "",
      displayPickup: false,
      displayEnd: false,
      displayCall: true,
      listenerId: 'ct-listener-1',
      errorMsg: '',
      initAction: props.initAction,
      isMuted: false,
      videoHidden: false
    };
  }
  componentDidMount() {
    SendBirdCall.init(this.state.appId);
    this.authenticate()
        .then(() => this.connect())
        .then(() => this.addIncomingListener())
        .catch(err => {
          console.log(err)
        });
  }
  connect() {
    return new Promise((resolve, reject) => {
      SendBirdCall.connectWebSocket()
          .then(() => {
            resolve("Connected");
          })
          .catch(() => {
            reject("Websocket Failed");
          });
    });
  }
  authenticate() {
    return new Promise((resolve, reject) => {
      SendBirdCall.authenticate({
        userId: this.state.userId,
        accessToken: undefined
      }, (result, error) => {
        !!error ? reject(error) : resolve(result);
      });
    });
  }
  acceptCall() {
    let callOption = this.getCallOptions();
    this.state.call.accept({
          callOption
        }
    );
  }
  getCallOptions() {
    let callOption = {
      remoteMediaView: document.getElementById('remote_element_id'),
      audioEnabled: true,
      videoEnabled: false
    }
    if (this.isVideoCall()) {
      callOption.localMediaView = document.getElementById('local_video_element_id');
      callOption.videoEnabled = true;
      callOption.audioEnabled = true;
    }
    return callOption;
  }
  isVideoCall() {
    if (this.state.initAction === 'initVideo') {
      return true;
    }
    return false;
  }
  endCall() {
    this.state.call.end();
    this.clearState();
  }
  clearState(){
    this.setState({isMuted: false});
    this.setState({videoHidden: false});
  }
  muteCall(){
    this.setState({isMuted: !this.state.isMuted},() => {
      if(this.state.isMuted) {
        this.state.call.muteMicrophone();
      } else {
        this.state.call.unmuteMicrophone();
      }
    });
  }
  toggleVideo(){
    this.setState({videoHidden: !this.state.videoHidden},() => {
      if(this.state.videoHidden) {
        this.state.call.stopVideo();
      } else {
        this.state.call.startVideo();
      }
    });
  }
  makeCall() {
    let callOption = this.getCallOptions();
    const dialParams = {
      userId: this.state.targetUserId,
      isVideoCall: this.isVideoCall(),
      callOption
    };
    try {
      const call = SendBirdCall.dial(dialParams, (call, error) => {
        if (error) {
          console.log(error);
          this.setState({errorMsg: error.toString()})
        } else {
          this.setState({errorMsg: ''})
          this.addDialOutListener(call);
        }
      });
    } catch (e) {
      this.setState({errorMsg: e.message})
    }
  }
  addDialOutListener(call) {
    call.onEstablished = (call) => this.setState({call, ...callStates.established});
    call.onConnected = (call) => this.setState(callStates.connected);
    call.onEnded = (call) => {
      let _this = this;
      this.setState(callStates.ended);
      setTimeout(() => _this.setState({info: "Waiting..."}), 1000);
    };
  }
  addIncomingListener() {
    console.log("Initizalized & ready...");
    SendBirdCall.addListener(this.state.listenerId, {
      onRinging: (call) => {
        console.log(call);
        this.setState({call, ...callStates.ringing});
        call.onEstablished = (call) => this.setState(callStates.established);
        call.onConnected = (call) => this.setState(callStates.connected);
        call.onEnded = (call) => this.setState(callStates.ended);
      }
    });
  }
  componentWillUnmount() {
    this.clearState();
    SendBirdCall.removeListener(this.state.listenerId);
    SendBirdCall.deauthenticate();
  }
  videoWorkaround = (id) => {
    let mutedParam = '';
    return (
        <div dangerouslySetInnerHTML={{
          __html: `
          <video
            ${mutedParam}
            autoplay
            playsinline      
            id="${id}"
          />`
        }}
        />
    );
  };
  /**
   * for debugging
   */
  callUserId = () => {
    return (
            <div className="mb1 mt1">
              <button onClick = {() => this.makeCall()}>Call User ID:</button>
              <input value={this.state.targetUserId} onChange={e => this.setState({ targetUserId: e.target.value })} id="targetUserId" type="text" placeholder="Target UserID" />
            </div>
    );
  }
  componentDidUpdate = () =>{
    // console.log(this.state);
  }
  render() {
    let button;
    let button2;
    let button3;
    if (this.state.displayPickup) {
      button = (
      <MIconButton
          className="btn--pickUp u-m2"
          aria-label="Pick Up!"
          onClick={() => this.acceptCall()}
      >
        <CallIcon />
      </MIconButton>
      )
    }
    if (this.state.displayEnd) {
      button = (
          <MIconButton
              className="btn--hangUp u-m2"
              aria-label="Hang Up!"
              onClick={() => this.endCall()}
          >
            <CallEndIcon />
          </MIconButton>
      )
    }
    if (this.state.displayEnd) {
      button2 = (
          <MIconButton
              className="btn--control u-m2"
              aria-label="Control"
              onClick={() => this.muteCall()}
          >
            {this.state.isMuted ? <VolumeOffIcon/> : <VolumeUpIcon/>}
          </MIconButton>
      )
    }
    if (this.state.displayEnd && this.isVideoCall()) {
      button3 = (
          <MIconButton
              className="btn--dark u-m2"
              aria-label="Control"
              onClick={() => this.toggleVideo()}
          >
            {this.state.videoHidden ? <VideocamOffIcon/> : <VideocamIcon/>}
          </MIconButton>
      )
    }
    let mediaView = (<audio id = "remote_element_id" controls autoPlay/>);
    if(this.isVideoCall()){
       mediaView = (
          <div className="videosContainer">
            <div className="tinyVideo">
              {this.videoWorkaround("local_video_element_id")}
            </div>
            {this.videoWorkaround("remote_element_id")}
          </div>
      );
    }
    let button8_debug;
    if (this.state.displayCall) {
      button8_debug = this.callUserId();
    }
    return ( <div className="videoLayer">
          {mediaView}
          <div className="videoInfoMsg"> { this.state.info } </div>
            { button8_debug }
            { button }
            { button2 }
            { button3 }
      { this.state.errorMsg ? <p className="sbError">{this.state.errorMsg}</p> : '' }
      </div>
    );
  }
}
export default SBCall;

The definition of states that are used to control button visibility and displaying the status message:

// src/Calls/settings.js
export const callStates = {
    ringing: {
        info: "Ringing... Pick up!",
          displayPickup: true,
          displayCall: false,
          displayCallEnd: false
    },
    established : {
        info: "Call established",
        displayPickup: false,
        displayCall: false,
        displayCallEnd: true
      },
    connected: {
        info: "Call connected",
        displayPickup: false,
        displayCall: false,
        displayEnd: true
      },
    ended: {
        info: "Call ended",
        displayPickup: false,
        displayEnd: false,
        displayCall: true
    }
}

Styles for video container, the pick-up and hang up icon:

/**
src/Calls/index.css
 */
.videosContainer{
  background:#333;
  position:relative;
}
.videosContainer:before {
    display: block;
    content: "";
    padding-top: 56.25%;
}
.videosContainer #remote_element_id {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    width: 100%;
    height: 100%;
}
.tinyVideo {
    position: absolute;
    width: 100px;
    top: 10px;
    left: 10px;
    background: #000;
    border:1px solid #333;
    z-index:1;
    overflow:hidden;
}
.tinyVideo:before {
    display: block;
    content: "";
    padding-top: 56.25%;
}
#local_video_element_id {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    width: 100%;
    height: 100%;
}
.btn--pickUp {
    background:green !important;
    color:#fff !important;
}
.btn--hangUp {
    background:red !important;
    color:#fff !important;
}
.btn--control {
    background:dodgerblue !important;
    color:#fff !important;
}
.btn--dark {
    background:#333 !important;
    color:#fff !important;
}
.videoInfoMsg {
    background: #fff;
    padding: 10px;
    margin: 10px;
    border: 1px solid #ccc;
}
.sbError{
    color:red;
    margin:2px;
}
.u-m2 {
    margin:1rem !important;
}

Running demo

The demo needs to authenticate a particular sendbird user by checking ?user_id url GET param. For testing purposes, you can open 2 browser windows:

http://localhost:3001/?user_id=3_8e81bbd3-3aeb-4bf7-be5a-a3d91b73f739

http://localhost:3001/?user_id=3_b68af04f-72af-49ff-8cd7-f54b43091d3a

and initiate a call from window1 and a pickup in window2. The user has the ability to mute his microphone and disable the video from the camera. The red icon is used to hang up the conversation.

Voice Call in SendBird

To initiate a voice call, we’re using the same functions, but with videoEnabled: false. The participants of the call can mute their microphones or end a call using the red button.

browser window with two tabs and voice call options

 

SendBird tools

You can also use the SendBird tools available in the Main panel: Direct calls / 1-to-1 call / Make a Call widget. It has the ability to initiate a call to a particular user id and test if your application is working correctly.

video call app window with buttons and a side panel

Integrate Calls with Chat

Beside calls, SendBird offers Chat API and Chat SDKs. The React UIKit includes a predefined set of components that allow to build a custom chat with advanced analytics. ‘Calls integration to Chat’ enables the option to call anyone in a channel. More info is available in sendbird documentation: https://sendbird.com/docs/calls/v1/javascript/tutorials/calls-integration-to-chat

That’s it for today’s tutorial. Make sure to follow us for other tips and guidelines, and don’t forget to subscribe to our newsletter.

 

 

 

 

 

Need help?

  • Looking for support from experienced programmers?

  • Need to fix a bug in the code?

  • Want to customize your webste/application?

ADD COMMENT

Your email address will not be published.

createIT Contact