Skip to main content

Call Controls

Developers building apps in browsers have to face the dilemma of adapting their layouts to high and small resolutions. In the desktop environment there is a more generous amount of space than on mobile devices. Many times, there may not be one component that would fit all the constraints. Therefore, the React SDK provides the flexibility in assembling the call controls layout. We can pick any combination of buttons bundled with the SDK. Each button controls its own area of responsibility. Our task, as integrators is to create a component that puts these buttons together as we wish. In this example we intend to show, how to do just that.

note

The React SDK exports a pre-built component CallControls. If it does not meet all the requirements, we encourage everybody to assemble their own CallControls component.

Assembling own CallControls component

Currently, the SDK exports the following call controls components:

The default CallControls implementation makes use of some of these buttons only. All the buttons access the call related data with hooks instead of props. Therefore, the custom CallControls component just renders selected button components and orders them in any order that meets the customisation requirements. An example follows:

import {
CancelCallButton,
SpeakingWhileMutedNotification,
ToggleAudioPublishingButton,
ToggleVideoPublishingButton,
} from '@stream-io/video-react-sdk';

import type { CallControlsProps } from '@stream-io/video-react-sdk';

export const CallControls = ({ onLeave }: CallControlsProps) => (
<div className="str-video__call-controls">
<SpeakingWhileMutedNotification>
<ToggleAudioPublishingButton />
</SpeakingWhileMutedNotification>
<ToggleVideoPublishingButton />
<CancelCallButton onLeave={onLeave} />
</div>
);

Building custom control buttons

It may as well be the case, that the default call controls buttons look does not meet our design requirements. It is very easy to build custom buttons making use of the hooks provided by the SDK. In the next few sections, we will demonstrate how custom call controls buttons can be built.

note

Implementing call controls buttons will often be in reality associated with handling permissions to perform the given action. To learn about permission handling, take a look at our permissions and moderation guide.

Button to accept a call

We will need a call accept button when building app that makes use of ring call workflow. To accept a call we just invoke call.join(). So the minimal call accept button could look like this:

import { useCall } from '@stream-io/video-react-sdk';

export const CustomAcceptCallButton = () => {
const call = useCall();
return (
<button onClick={() => call?.join()}>
<span className="my-icon" />
</button>
);
};

Button to cancel a call

To cancel an outgoing call in ring call scenario or to leave an already joined call, we just invoke call.leave(). To reject a call in ring call scenario, invoke call.leave({reject: true}).

import { useCall } from '@stream-io/video-react-sdk';

type CustomCancelCallButtonProps = {
reject?: boolean;
};

export const CustomCancelCallButton = ({
reject,
}: CustomCancelCallButtonProps) => {
const call = useCall();
return (
<button onClick={() => call?.leave({ reject })}>
<span className="my-icon" />
</button>
);
};

Toggling audio

Toggling microphone in an active call turns around publishing audio input streams and enabling the audio state. The bare-bones button to toggle audio in an active call could look like the following:

import { useCallStateHooks } from '@stream-io/video-react-sdk';

export const CustomToggleAudioPublishingButton = () => {
const { useMicrophoneState } = useCallStateHooks();
const { microphone, isMute } = useMicrophoneState();
return (
<button onClick={() => microphone.toggle()}>
{isMute ? (
<span className="my-icon-disabled" />
) : (
<span className="my-icon-enabled" />
)}
</button>
);
};

To toggle audio before joining a call (for example in a call lobby or on pending call panel), we can use the same API. The state is kept on a call level, so if in the preview the audio was disabled, then it will remain disabled after joining the call.

Toggling video

To toggle video input, the approach is analogous to that of audio input.

import { useCallStateHooks } from '@stream-io/video-react-sdk';

export const CustomToggleVideoPublishingButton = () => {
const { useCameraState } = useCallStateHooks();
const { camera, isMute } = useCameraState();
return (
<button onClick={() => camera.toggle()}>
{isMute ? (
<span className="my-icon-disabled" />
) : (
<span className="my-icon-enabled" />
)}
</button>
);
};

To toggle video before joining a call (for example in a call lobby or on pending call panel), we can use the same API. The state is kept on a call level, so if in the preview the video was disabled, then it will remain disabled after joining the call.

Toggling screen sharing

To toggle Screen Sharing, you can utilize the following API:

import { useCallStateHooks } from '@stream-io/video-react-sdk';

export const CustomScreenShareButton = () => {
const { useScreenShareState, useHasOngoingScreenShare } = useCallStateHooks();
const { screenShare, isMute: isScreenSharing } = useScreenShareState();

// determine, whether somebody else is sharing their screen
const isSomeoneScreenSharing = useHasOngoingScreenShare();
return (
<button
// disable the button in case I'm not the one sharing the screen
disabled={!isScreenSharing && isSomeoneScreenSharing}
onClick={() => screenShare.toggle()}
>
{isScreenSharing ? (
<span className="my-icon-enabled" />
) : (
<span className="my-icon-disabled" />
)}
</button>
);
};

Recording calls

To start recording a call, we invoke call.startRecording() and to stop it call.stopRecording(). To determine, whether the recording already began, use the hook useIsCallRecordingInProgress().

import { useCallback, useEffect, useState } from 'react';
import {
LoadingIndicator,
useCall,
useCallStateHooks,
} from '@stream-io/video-react-sdk';

export const CustomRecordCallButton = () => {
const call = useCall();
const { useIsCallRecordingInProgress } = useCallStateHooks();

const isCallRecordingInProgress = useIsCallRecordingInProgress();
const [isAwaitingResponse, setIsAwaitingResponse] = useState(false);

useEffect(() => {
// we wait until call.recording_started/stopped event to flips the
// `isCallRecordingInProgress` state variable.
// Once the flip happens, we remove the loading indicator
setIsAwaitingResponse((isAwaiting) => {
if (isAwaiting) return false;
return isAwaiting;
});
}, [isCallRecordingInProgress]);

const toggleRecording = useCallback(async () => {
try {
setIsAwaitingResponse(true);
if (isCallRecordingInProgress) {
await call?.stopRecording();
} else {
await call?.startRecording();
}
} catch (e) {
console.error(`Failed start recording`, e);
}
}, [call, isCallRecordingInProgress]);

return (
<>
{isAwaitingResponse ? (
<LoadingIndicator
tooltip={
isCallRecordingInProgress
? 'Waiting for recording to stop... '
: 'Waiting for recording to start...'
}
/>
) : (
<button disabled={!call} title="Record call" onClick={toggleRecording}>
{isCallRecordingInProgress ? (
<span className="my-icon-enabled" />
) : (
<span className="my-icon-disabled" />
)}
</button>
)}
</>
);
};

Did you find this page helpful?