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>
);Call Controls
The React SDK provides flexibility in assembling call controls layouts. Pick any combination of bundled buttons - each controls its own area of responsibility.
Best Practices
- Use individual control buttons for custom layouts instead of modifying
CallControls. - Wrap audio toggle with
SpeakingWhileMutedNotificationto alert muted speakers. - Handle permissions - buttons automatically hide when users lack permission.
- Use the same hooks (
useMicrophoneState,useCameraState) for consistent state.
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:
AcceptCallButtonCancelCallButtonToggleAudioPreviewButtonToggleAudioPublishingButtonToggleAudioOutputButtonToggleVideoPreviewButtonToggleVideoPublishingButtonScreenShareButtonRecordCallButton
The default CallControls implementation uses only some of these buttons. The buttons access call-related data via hooks, not props. A custom CallControls component typically renders the buttons you want in your preferred order. Example:
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.
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, reason: 'cancel' }).
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, reason: reject ? "cancel" : undefined })
}
>
<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>
);
};Toggling Noise Cancellation
Before we start working on a toggle button, Noise Cancellation should be integrated and enabled in your application. Check our Noise Cancellation guide.
import { useNoiseCancellation } from "@stream-io/video-react-sdk";
export const ToggleNoiseCancellationButton = () => {
const { isSupported, isEnabled, setEnabled } = useNoiseCancellation();
if (!isSupported) return null;
return (
<button
className={isEnabled ? "btn-toggle-nc-active" : "btn-toggle-nc"}
type="button"
onClick={() => setEnabled((enabled) => !enabled)}
>
Toggle Noise Cancellation
</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>
)}
</>
);
};