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
andonError
). - Update the
connectionState
and change the visible component depending on the current state (Preflight
,Loading
,Conference
orError
). - 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.
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 retrievepexrtc.js
fromnodeDomain
. 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
andError
. - localStream: This variable will contain a
MediaStream
with the local video obtained directly from the user's webcam. We will attach this stream to aHTML 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 thelocalStream
, we will attach it to aHTML 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 thelocalStream
that contains theMediaStream
obtained from the user's camera. The second parameter ispinStatus
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 theMediaStream
with all the remote participants in the parameterremoteStream
.handleDisconnect
: When the app receives the disconnect event, we need to change the connection status toDISCONNECTED
.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 toERROR
and define the string that contains theerror
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 callvideo.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 theVideo
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 theHTML Video
. In this case theMediaStream
is assigned to thesrcObject
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.