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:
- Define the callbacks for some Infinity events.
- 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 streams (microphone and camera feeds) attach the video to a
HTML video
element and send the streams to Pexip Infinity. - Get the remote stream (remote feed) and attach it to a
HTML video
element. - Disconnect from a conference gracefully and free the microphone and 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.tsx
. This file will
contain the main logic of our application and it's the first file that we need
to change.
At the top of the file we define all the dependencies that we will use:
import {useEffect, useState} from 'react';
import {
ClientCallType,
createCallSignals,
createInfinityClient,
createInfinityClientSignals,
type InfinityClient,
} from '@pexip/infinity';
import {Loading} from './components/Loading/Loading';
import {Conference} from './components/Conference/Conference';
import {Error} from './components/Error/Error';
import {Preflight} from './components/Preflight/Preflight';
Create the Infinity Client
We will start by creating the Infinity Client. The Infinity Client is the object that we'll use to connect to the Pexip Infinity Conferencing Node and exchange the proper messages to manage a conference.
The first step is to define the signals that our application could receive from Pexip Infinity:
const infinityClientSignals = createInfinityClientSignals([]);
const callSignals = createCallSignals([]);
We will use these objects to get different types of signals:
- infinityClientSignals: Thanks to this object, we will be able to detect when the call is connected, a participant joins the conference, the layout is changed or somebody raises their hand, but more signals are available.
- callSignals: These signals contain the basic elements for the calls to
work. An example of these signals are
onRemoteStream
andonRemotePresentationStream
. These signals will contain aMediaStream
with the video source that we can attach to aHTML video
element.
Now we will define the variable that will contain the instance of the Infinity Client:
let infinityClient: InfinityClient;
Define the possible app states
It's import 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 was any kind of issue.
We will start by defining a constant that contains all the possible states:
enum ConnectionState {
Disconnected,
Connecting,
Connected,
Error,
}
Now we will use the React useState
hook to define some state variables:
const [connectionState, setConnectionState] = useState<ConnectionState>(
ConnectionState.Disconnected,
);
const [localAudioStream, setLocalAudioStream] = useState<MediaStream>();
const [localVideoStream, setLocalVideoStream] = useState<MediaStream>();
const [remoteStream, setRemoteStream] = useState<MediaStream>();
const [error, setError] = useState('');
With only these 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
. - localAudioStream: This variable will contain a
MediaStream
with the local audio obtained directly from the user's microphone. We will send it to Pexip Infinity. - localVideoStream: This variable will contain a
MediaStream
with the local video obtained directly from the user's webcam. We will attach this stream to a self-viewHTML 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 thelocalVideoStream
, 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.
In this example we use a MediaStream
for audio (localAudioStream
) and
another MediaStream
for video (localVideoStream
). However, it's possible to
use a single MediaStream
for both: audio and video. We decided to do it in a
separate stream to simplify the process to mute/unmute the different devices.
Set the event handlers
Now we need to define all the callback functions for each one of the events. We will start by defining a function that will be called each time the user tries to join a conference:
const handleStartConference = async (
nodeDomain: string,
conferenceAlias: string,
displayName: string,
): Promise<void> => {
setConnectionState(ConnectionState.Connecting);
const localAudioStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
const localVideoStream = await navigator.mediaDevices.getUserMedia({
video: true,
});
setLocalAudioStream(localAudioStream);
setLocalVideoStream(localVideoStream);
const response = await infinityClient.call({
callType: ClientCallType.AudioVideo,
node: nodeDomain,
conferenceAlias,
displayName,
bandwidth: 0,
mediaStream: new MediaStream([
...localAudioStream.getTracks(),
...localVideoStream.getTracks(),
]),
});
if (response != null) {
if (response.status !== 200) {
localAudioStream.getTracks().forEach(track => {
track.stop();
});
localVideoStream.getTracks().forEach(track => {
track.stop();
});
setLocalAudioStream(undefined);
setLocalVideoStream(undefined);
}
switch (response.status) {
case 200:
setConnectionState(ConnectionState.Connected);
break;
case 403: {
setConnectionState(ConnectionState.Error);
setError('The conference is protected by PIN.');
break;
}
case 404: {
setConnectionState(ConnectionState.Error);
setError("The conference doesn't exist.");
break;
}
default: {
setConnectionState(ConnectionState.Error);
setError('Internal error.');
break;
}
}
} else {
setConnectionState(ConnectionState.Error);
setError("The server isn't available.");
}
};
You can see that the function has three input parameters: nodeDomain
,
conferenceAlias
and displayName
. These parameters are the ones that the user
will introduce in the Preflight
component:
- nodeDomain: This is the domain of the Conferencing Node that we want to use. This could be a domain or an IP address.
- conferenceAlias: This is the alias of the conference that we want to join.
- displayName: This is the username that the other participants will see for this user.
Now, since this function is a bit long, we will explain step by step what it does:
- First of all, it changes the connection state to
Connecting
, so the interface can display theLoading
component. - Call the
getUserMedia()
method to get aMediaStream
. This is a method from the browser API to request access to the microphone and/or camera. Thanks to this method, we can get aMediaStream
for audio and another for the video. - Assign the previous
MediaStream
to thelocalAudioStream
andlocalVideoStream
state variables. This way the local camera feed can be displayed in our interface. - Call
infinityClient.call()
with all the conference info that we want to start:- callType: This parameter defines how we want to join the meeting. In this case we define that we want audio and video, but we can define that we only want to send video, receive audio and so on.
- node: Through this parameter we can indicate which Conferencing Node we want to use. This parameter could be a domain or an IP address.
- displayName: This is the username that the other participants will see for this user.
- bandwidth: We can limit the bandwidth to use. In this case we set the
value to
0
indicating that we don't want to use the bandwidth limitation. - mediaStream: This is the stream with local audio and video that we will send to Pexip Infinity.
- Change the connection state accordingly to the call response.
In case we receive an error, we will display the proper message in the Error
component:
Now we define the disconnect handler:
const handleDisconnect = async (): Promise<void> => {
localAudioStream?.getTracks().forEach(track => {
track.stop();
});
localVideoStream?.getTracks().forEach(track => {
track.stop();
});
await infinityClient.disconnect({reason: 'User initiated disconnect'});
setConnectionState(ConnectionState.Disconnected);
};
In this case, the fist thing that this handler will do is to stop the
localAudioStream
and localVideoStream
. That means that the application will
release the microphone and camera, so they will be available for other
applications. You will also see that the camera LED will turn off.
Later on, it will disconnect from the meeting indicating the reason and change
the connection state to Disconnected
.
useEffect hooks
We will create an instance of the infinityClient
inside a useEffect
hook. We
also need to define the dependency (error
), this way the code will be run each
time the error
variable changes:
useEffect(() => {
infinityClient = createInfinityClient(infinityClientSignals, callSignals);
}, [error]);
Now we will use the useEffect
hook to specify the signals we want to listen
for and define what should happen when one of these signals are received:
useEffect(() => {
callSignals.onRemoteStream.add(stream => {
setRemoteStream(stream);
});
infinityClientSignals.onError.add(error => {
setConnectionState(ConnectionState.Error);
setError(error.error);
});
infinityClientSignals.onDisconnected.add(() => {
setConnectionState(ConnectionState.Disconnected);
});
const disconnectBrowserClosed = (): void => {
infinityClient.disconnect({reason: 'Browser closed'}).catch(console.error);
};
window.addEventListener('beforeunload', disconnectBrowserClosed);
return () => {
window.removeEventListener('beforeunload', disconnectBrowserClosed);
};
}, []);
This function will be triggered when the Conference
component is created and
it does the following:
- Set the remote stream when the
onRemoteStream
signal is received. - Trigger the proper handler when the app receives an error in the
onError
signal. In this case, it changes the connection state toError
and define the string that contains theerror
itself in another variable. - Change the state to
Disconnected
if the app receives theonDisconnect
signal. This happens if a host kick this user out of the conference. - Finally, assign a listener to the event
beforeunload
. This event is triggered when the user closes the window or tab that is running the app and, in this case, we will disconnect the user from the conference.
Return component based on state
Now we will center our focus in the application rendering. We will choose what component to show to the user depending on the application state:
let component
switch (connectionState) {
case ConnectionState.Connecting:
component = <Loading />
break
case ConnectionState.Connected:
component = (
<Conference
localVideoStream={localVideoStream}
remoteStream={remoteStream}
onDisconnect={handleDisconnect}
/>
)
break
case ConnectionState.Error:
component = (
<Error
message={error}
onClose={() => {
setConnectionState(ConnectionState.Disconnected)
}}
/>
)
break
default:
component = <Preflight onSubmit={handleStartConference} />
break
}
return <div className="App">{component}</div>
As you can see, we have defined a switch
statement that will choose between
the different components depending on the connectionState
:
- Connecting: If the app is in the
Connecting
state, it will show theLoading
component. - Connected: If the app is in the
Connected
state, it will show theConference
component. - Error: If the app is in the
Error
state, it will show theError
component. - Disconnected: If the app is in the
Disconnected
state, it will show thePreflight
component.
Conference component
This component is located in src/components/Conference/Conference.tsx
and it
will be displayed once the user joins a conference.
At the top of the file we import the React components that we will use:
import {Video} from '@pexip/components';
import {Toolbar} from './Toolbar/Toolbar';
The Video
component is a wrapper around the HTML video
element that
simplifies the process of attaching a MediaStream
to it. The Toolbar
component is a custom component that we will develop and it will contain all the
buttons that the user can use to interact with the conference.
We define first all the possible props that the component will accept:
interface ConferenceProps {
localVideoStream: MediaStream | undefined;
remoteStream: MediaStream | undefined;
onDisconnect: () => Promise<void>;
}
The localVideoStream
is an only video MediaStream
that contains the webcam
feed and we will use it to display the self-view. The remoteStream
is the
MediaStream
that contains the video and audio from all the remote
participants. Finally, onDisconnect
is a function that will be called when the
user wants to leave the conference.
Now we define those props as input values of the functional component and render the component itself:
export const Conference = (props: ConferenceProps): JSX.Element => {
return (
<div className="Conference">
<div className="VideoContainer">
<Video className="remote-video" srcObject={props.remoteStream} />
<Video
className="local-video"
srcObject={props.localVideoStream}
isMirrored={true}
/>
<Toolbar className="toolbar" onDisconnect={props.onDisconnect} />
</div>
</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 from the current user.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.tsx
. 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.
At the top of the file we will import all the Pexip components that we will use to build the button.
import {Button, Icon, IconTypes, Tooltip} from '@pexip/components';
We will start by defining the function that will be called when the button is
pressed. In this case the function is quite simple, since it only calls the
props.onDisconnect()
function that we have defined in a previous step.
First of all, we add onDisconnect
to the props list:
interface ToolbarProps {
className: string;
onDisconnect: () => Promise<void>;
}
Now we define the function that will be run when the user pushes the disconnect button:
const handleHangUp = async (): Promise<void> => {
await props.onDisconnect();
};
The final step is to add the button itself to our interface with a tooltip:
return (
<div className={className}>
<Tooltip text="Disconnect">
<Button
onClick={() => {
handleHangUp().catch(console.error)
}}
variant="danger"
modifier="square"
colorScheme="light"
>
<Icon source={IconTypes.IconPhoneHorisontal} />
</Button>
</Tooltip>
</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.