Get a free advice now!

    Pick the topic

    Developer OutsourcingWeb developingApp developingDigital MarketingeCommerce systemseEntertainment systems

    Thank you for your message. It has been sent.

    Tags

    SendBird Calls – example in React

    SendBird Calls – example in React

    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 preview with black video screen, call buttons and text

    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.

    Animation of a voice call in the browser window

    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 windows

    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.

    Comments
    0 response

    Add comment

    Your email address will not be published. Required fields are marked *

    Popular news

    eCommerce growth – is your business ready?
    • Services
    • Trends

    eCommerce growth – is your business ready?

    April 8, 2024 by createIT
    Digital marketing without third-party cookies – new rules
    • Technology
    • Trends

    Digital marketing without third-party cookies – new rules

    February 21, 2024 by createIT
    eCommerce healthcheck
    • Services
    • Trends

    eCommerce healthcheck

    January 24, 2024 by createIT
    Live Visitor Count in WooCommerce with SSE
    • Dev Tips and Tricks

    Live Visitor Count in WooCommerce with SSE

    December 12, 2023 by createIT
    Calculate shipping costs programmatically in WooCommerce
    • Dev Tips and Tricks

    Calculate shipping costs programmatically in WooCommerce

    December 11, 2023 by createIT
    Designing a cookie consent modal certified by TCF IAB
    • Dev Tips and Tricks

    Designing a cookie consent modal certified by TCF IAB

    December 7, 2023 by createIT
    Understanding the IAB’s Global Vendor List (GVL)
    • Dev Tips and Tricks

    Understanding the IAB’s Global Vendor List (GVL)

    December 6, 2023 by createIT

    Support – Tips and Tricks
    All tips in one place, and the database keeps growing. Stay up to date and optimize your work!

    Contact us