Skip to main content

Join a conference

In this lesson we will learn how to modify the starter_code and add changes to allow us to join a conference. During this journey we will perform the following tasks:

  • Download the PexRTC library dynamically.
  • Define the callbacks for some PexRTC events (onSetup, onConnect, onDisconnect and onError).
  • Update the connectionState and change the visible component depending on the current state (Preflight, Loading, Conference or Error).
  • Join a conference with the proper parameters.
  • Get the local stream (camera feed) and attach it to a HTML video element.
  • Get the remote stream (remote feed) and attach it to a HTML video element.
  • Disconnect from a conference gracefully and free the camera.
info

After finishing this lesson our app will be able to join a conference that is not protected by a PIN. If you want to use a PIN, you will have to make the changes that are explained in the next lesson.

You can download the starter code from Step.01-Exercise-Join-a-conference.

App component

We will start by modifying the file located in src/App.js. This file contains the main logic of our application and it's the first file that we need to change.

Download PexRTC dynamically

Instead of having a copy of the PexRTC library served from our application, we will retrieve it dynamically. This has two main advantages:

  • We always retrieve the latest version located in the Conferencing Node.
  • Our application will work with different versions of Pexip Infinity without further modifications.

The first step is to define a variable that will contain the pexRTC object. This object will be the main source of truth in of our application. It will store the current application state. We will use it to perform some action, such as mute the audio or share the screen.

let pexRTC;

Now we need to define the function that will retrieve the library from the Conferencing Node:

const getPexRTC = async nodeDomain => {
return new Promise((resolve, reject) => {
const pexRTCId = 'pexrtc-library';
// Remove a previous library if any
const pexRTCScript = document.getElementById(pexRTCId);
if (pexRTCScript) pexRTCScript.remove();
// Attach the new library
const script = document.createElement('script');
script.id = 'pexrtc-library';
script.type = 'text/javascript';
script.src = `https://${nodeDomain}/static/webrtc/js/pexrtc.js`;
script.onload = function () {
const pexRTC = new window.PexRTC();
pexRTC.onSetup = handleSetup;
pexRTC.onConnect = handleConnect;
pexRTC.onDisconnect = handleDisconnect;
pexRTC.onError = handleError;
resolve(pexRTC);
};
script.onerror = () => {
reject(new Error('Cannot get PexRTC'));
};
document.body.appendChild(script);
window.addEventListener('beforeunload', event => {
pexRTC.disconnect();
});
});
};

This function will perform the following tasks:

  • Remove any previously attached pexrtc.js libraries.
  • Create a script DOM node to retrieve pexrtc.js from nodeDomain. This last variable is one of the parameters that the user has to enter when they try to join a conference.
  • Attach the script node to the HTML body.
  • Once the script is loaded, we need to create a pexRTC object and assign all the callback functions. We will define what each of this functions do in a later step.
  • Finally, assign a listener to the event beforeunload. In this case, it will be used to disconnect the user from the conference when the user closes the window or tab that is running the app.

Define the possible app states

It's important to keep track of the app state. The app needs to know if we are in the middle of a call, trying to connect or if there is any kind of issue when trying to connect.

We will start by defining a constant that contains all the possible states:

const CONNECTION_STATE = {
DISCONNECTED: 'DISCONNECTED',
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
ERROR: 'ERROR',
};

Now we will use the React useState hook to define some state variables:

const [connectionState, setConnectionState] = useState(
CONNECTION_STATE.DISCONNECTED,
);
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [error, setError] = useState('');

With only these four variables we can define the whole app state:

  • connectionState: This state variable will store the current state for the connection. It will have one of the values that we have defined previously: Disconnected, Connecting, Connected and Error.
  • localStream: This variable will contain a MediaStream with the local video obtained directly from the user's webcam. We will attach this stream to a HTML Video element and also send it to Pexip Infinity.
  • remoteStream: In a similar way, we will have a MediaStream that contains the video of all the remote participants mixed in a single stream. This stream is obtained from the Infinity Conferencing Node and, in the same way that with the localStream, we will attach it to a HTML Video element.
  • error: This variable is used to save a string with the error that happened when the user tries to join a conference. If the call was successful, it will be an empty string.

Set the event handlers

Now we need to define all the callback functions for each one of the events:

const handleSetup = (localStream, pinStatus) => {
setLocalStream(localStream);
pexRTC.connect();
};

const handleConnect = remoteStream => {
setRemoteStream(remoteStream);
setConnectionState(CONNECTION_STATE.CONNECTED);
};

const handleDisconnect = reason => {
setConnectionState(CONNECTION_STATE.DISCONNECTED);
};

const handleError = error => {
setConnectionState(CONNECTION_STATE.ERROR);
setError(error);
};

We have defined four callback functions an this is what each one of them do:

  • handleSetup: This is the first callback that is triggered when we try to join a conference. It receives two parameters. The first one is the localStream that contains the MediaStream obtained from the user's camera. The second parameter is pinStatus that indicates if a PIN is required. We will ignore this parameter for now, but it will be a key element in the next lesson.

  • handleConnect: This callback function is call once the user is connected to the conference. It contains the MediaStream with all the remote participants in the parameter remoteStream.

  • handleDisconnect: When the app receives the disconnect event, we need to change the connection status to DISCONNECTED.

  • handleError: The last callback function is triggered in case there is any trouble joining the conference. When this function is triggered, we change the connection state to ERROR and define the string that contains the error itself in another variable. This way the application will display a panel with the problem.

Return component based on state

Now we will center our focus in the application rendering. We will choose what component we should show to the user depending on the application state:

let component;
switch (connectionState) {
case CONNECTION_STATE.CONNECTING:
component = <Loading />;
break;
case CONNECTION_STATE.CONNECTED:
component = (
<Conference
localStream={localStream}
remoteStream={remoteStream}
pexRTC={pexRTC}
/>
);
break;
case CONNECTION_STATE.ERROR:
component = (
<Error
message={error}
onClose={() => setConnectionState(CONNECTION_STATE.DISCONNECTED)}
/>
);
break;
default:
component = <Preflight onSubmit={handleStartConference} />;
break;
}

The last step is to return the component that was selected in the switch statement:

return (
<div className="App" data-testid="app">
{component}
</div>
);

Conference component

This component is located in src/components/Conference/Conference.js and it will be displayed once the user joins a conference.

The first thing that we will do is to create a new component called Video. This component will add an abstraction layer and simplify the creation of the conference component.

const Video = React.memo(props => {
return (
<video
className={props.className}
autoPlay
playsInline
muted={props.muted}
onClick={props.onClick}
ref={element => {
if (element) element.srcObject = props.mediaStream;
}}
/>
);
});

Some things that we need to consider regarding the Video component:

  • We need to use React.memo to prevent to re-render the component when we don't have changes in the props. If we didn't use this, we would notice flickering each time the video is re-rendered.
  • We need to use autoPlay to start the video automatically. If we didn't use it, we would have to call video.play() manually which is not a good practice.
  • If we want to support iOS, we need to include playsInline. If we forget to add this parameter, the video will be played in fullscreen and it will break the whole experience.
  • We need to define the muted prop for this component. This way we can define easily if the Video should have audio or not. For example, the local video shouldn't have audio, meanwhile the remote video with the rest of the participants should have it. If we forgot to add this parameter, we would experience a small issue as the user will hear themself when they were talking.
  • The last thing to mention is how the MediaStream is assigned to the HTML Video. In this case the MediaStream is assigned to the srcObject video property.

Now we will define what to render for the conference component:

function Conference(props) {
return (
<div className="Conference">
<Video className="remote-video" mediaStream={props.remoteStream} />
<Video
className="local-video"
mediaStream={props.localStream}
muted={true}
/>
<Toolbar className="toolbar" pexRTC={props.pexRTC} />
</div>
);
}

This component consists in three sub-components:

  • remote-video: The video with the rest of the participants and their audio.
  • local-video: The video with the webcam feed of the current user. In this case the audio is muted.
  • toolbar: This component is the one that contains all the buttons to make actions. In the next section we will discuss how it works.

Toolbar component

The last file that we will modify in this lesson is src/components/Conference/Toolbar/Toolbar.js. This is the toolbar that allows us to interact with the conference. This time we will implement a new button. This button will be used to disconnect the user from the conference on demand.

The first thing that we will do is to define the function that will be called when the button is pushed. In this case the function is quite simple, since it only calls to the disconnect function that is provided by the pexRTC object, and also triggers the onDisconnect() function that we have defined in a previous step.

const handleHangUp = () => {
props.pexRTC.disconnect();
props.pexRTC.onDisconnect();
};

The final step is to add the button itself to our interface:

return (
<div className={className}>
<Button onClick={handleHangUp} icon={<CallEndIcon />} />
</div>
);

Run the app

You have everything in place to launch your videoconferencing application. Run the command npm start and you will see the app in the browser.

You can compare your code with the solution in Step.01-Solution-Join-a-conference. You can also check the differences with the previous lesson in the git diff.

Join a conference