npm install @stream-io/video-react-sdk
Ringing Call
When building an application that relies on ring call workflow
, you’ll often want to build your own incoming or outgoing call panels. The React SDK already comes with a pre-built RingingCall
component. However, in this article we will build a new one from scratch.
The component will render:
- Call controls buttons to toggle audio and video enablement and accept resp. reject call buttons (the component will be called
CallControls
) - Avatars of called users or the current user’s video preview (the component will be called
CallMembers
) - Call calling state indicator (the component will be called
CallCallingStateLabel
)
Project setup and prerequisites
Make sure you have the following prerequisites checked:
- Registered Stream account
- Have an app created in the Stream’s dashboard to obtain app API key and secret.
- Initiate the project (you can follow our introductory tutorial setup guide)
- Have installed the Stream video and chat SDKs in the project:
yarn add @stream-io/video-react-sdk
App boilerplate
In order we can start performing calls we need a StreamVideoClient
instance connected to the Stream’s server network with app credentials. For more information on how to do that, please take a look at our authentication guide.
Ringing call panel implementation
The component will rely on data accessed via our helper hooks:
useCall
useCallCallingState
useCallCreatedBy
useCallMembers
User preview
The panel will render VideoPreview
so that the user can see the camera input or avatars if video preview is disabled. The avatar display logic is as follows:
- show all the called users’ avatars if the call is outgoing (who am I calling)
- show the avatar of the person who initiated the call in case of an incoming call (who is calling me)
import {
useCall,
useCallStateHooks,
VideoPreview,
UserResponse,
} from "@stream-io/video-react-sdk";
import { CallCallingStateLabel } from "./CallCallingStateLabel";
import { CallControls } from "./CallControls";
import { CallMembers } from "./CallMembers";
import { useEffect } from "react";
type RingingCallProps = {
showMemberCount?: number;
};
export const CustomRingingCall = ({
showMemberCount = 3,
}: RingingCallProps) => {
const call = useCall();
const { useCallMembers, useCallCreatedBy, useCameraState } =
useCallStateHooks();
const members = useCallMembers();
const creator = useCallCreatedBy();
const { camera, isMute: isCameraMute } = useCameraState();
useEffect(() => {
// enable the camera by default for all ring calls
camera.enable();
}, [camera]);
if (!call) return null;
const caller = creator;
// show the caller if this is an incoming call or show all the users I am calling to
let membersToShow: UserResponse[] = [];
if (call.isCreatedByMe) {
membersToShow =
members
?.slice(0, showMemberCount)
.map(({ user }) => user)
.filter((u) => !!u) || [];
} else if (caller) {
membersToShow = [caller];
}
return (
<div>
{isCameraMute ? (
<CallMembers members={membersToShow} />
) : (
<VideoPreview />
)}
<CallCallingStateLabel />
<CallControls />
</div>
);
};
Incoming call panel
In the snippet below, the CallPanel
component is expected to render our CustomRingingCall
.
import {
CallingState,
StreamCall,
useCall,
useCallStateHooks,
useCalls,
} from "@stream-io/video-react-sdk";
import { CustomActiveCallPanel } from "./CustomActiveCallPanel";
import { CustomRingingCall } from "./CustomRingingCall";
export const Video = () => {
const calls = useCalls();
return (
<>
{calls.map((call) => (
<StreamCall call={call} key={call.cid}>
<CallPanel />
</StreamCall>
))}
</>
);
};
// custom component that renders ringing as well as active call UI
const CallPanel = () => {
const call = useCall();
const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();
if (!call) return null;
if (callingState === CallingState.JOINED) {
return <CustomActiveCallPanel />;
} else if (
[CallingState.RINGING, CallingState.JOINING].includes(callingState)
) {
return <CustomRingingCall />;
}
return null;
};
User avatars
We display relevant users’ avatars with CallMembers
component.
import { Avatar } from "@stream-io/video-react-sdk";
import type { UserResponse } from "@stream-io/video-react-sdk";
type CallMembersProps = {
members: UserResponse[];
};
const CallMembers = ({ members }: CallMembersProps) => {
return (
<div>
{members.map((member) => (
<div key={member.id}>
<Avatar name={member.name} imageSrc={member.image} />
{member.name && (
<div>
<span>{member.name}</span>
</div>
)}
</div>
))}
</div>
);
};
Call calling state label
To show to our users the state of call connection, we implement CallCallingStateLabel
. The calling state is provided by useCallCallingState
hook:
In this component we are using translation service that comes bundled with the SDK. The service API is consumed with useI18n
hook. Learn more about using the i18n service in the dedicated documentation.
import {
CallingState,
useCallStateHooks,
useI18n,
} from "@stream-io/video-react-sdk";
const CALLING_STATE_TO_LABEL: Record<CallingState, string> = {
[CallingState.JOINING]: "Joining",
[CallingState.RINGING]: "Ringing",
[CallingState.RECONNECTING]: "Re-connecting",
[CallingState.RECONNECTING_FAILED]: "Failed",
[CallingState.OFFLINE]: "No internet connection",
[CallingState.IDLE]: "",
[CallingState.UNKNOWN]: "",
[CallingState.JOINED]: "Joined",
[CallingState.LEFT]: "Left call",
};
const CallCallingStateLabel = () => {
const { t } = useI18n();
const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();
const callingStateLabel = CALLING_STATE_TO_LABEL[callingState];
return callingStateLabel ? <div>{t(callingStateLabel)}</div> : null;
};
Call controls
Finally, we build the call control buttons. We include buttons to toggle camera and microphone and buttons to accept and reject the call.
import {
AcceptCallButton,
CallingState,
CancelCallButton,
IconButton,
useCall,
useCallStateHooks,
} from "@stream-io/video-react-sdk";
const CallControls = () => {
const call = useCall();
const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();
if (!call) return null;
// show ringing call panel components only before the call is joined
if (![CallingState.RINGING, CallingState.JOINING].includes(callingState))
return null;
// prevent users from rejecting call when already accepted
const buttonsDisabled = callingState === CallingState.JOINING;
return (
<div>
<ToggleAudioButton />
<ToggleVideoButton />
{call.isCreatedByMe ? (
<CancelCallButton call={call} disabled={buttonsDisabled} />
) : (
<>
<AcceptCallButton call={call} disabled={buttonsDisabled} />
<CancelCallButton
onClick={() => {
const reason = call.isCreatedByMe ? "cancel" : "decline";
call.leave({ reject: true, reason });
}}
disabled={buttonsDisabled}
/>
</>
)}
</div>
);
};
const ToggleAudioButton = () => {
const { useMicrophoneState } = useCallStateHooks();
const { microphone, isMute } = useMicrophoneState();
return (
<IconButton
icon={isMute ? "mic-off" : "mic"}
onClick={() => microphone.toggle()}
/>
);
};
const ToggleVideoButton = () => {
const { useCameraState } = useCallStateHooks();
const { camera, isMute } = useCameraState();
return (
<IconButton
icon={isMute ? "camera-off" : "camera"}
onClick={() => camera.toggle()}
/>
);
};