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:

  • Define the callbacks for some Infinity events.
  • 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 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.
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.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 and onRemotePresentationStream. These signals will contain a MediaStream with the video source that we can attach to a HTML 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 and Error.
  • 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-view 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 localVideoStream, 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.
info

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 the Loading component.
  • Call the getUserMedia() method to get a MediaStream. This is a method from the browser API to request access to the microphone and/or camera. Thanks to this method, we can get a MediaStream for audio and another for the video.
  • Assign the previous MediaStream to the localAudioStream and localVideoStream 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:

Error

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 to Error and define the string that contains the error itself in another variable.
  • Change the state to Disconnected if the app receives the onDisconnect 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 the Loading component.
  • Connected: If the app is in the Connected state, it will show the Conference component.
  • Error: If the app is in the Error state, it will show the Error component.
  • Disconnected: If the app is in the Disconnected state, it will show the Preflight 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.

Join a conference