Skip to main content

Audio & Video Calls

Ermis Chat React provides a complete, built-in Direct Call system for 1-on-1 audio and video calls. It integrates seamlessly with the chat UI through the ChatProvider, ErmisCallProvider, and ErmisCallUI components.


Prerequisites

Before using the call feature, you must run the initialization command to copy the required WebAssembly and audio files into your application's public/ directory.

npx ermis-init-call

This copies the following files from the SDK into your project's public/ folder:

FilePurpose
ermis_call_node_wasm_bg.wasmWebRTC WASM engine (required)
call_incoming.mp3Ringtone for incoming calls (optional)
call_outgoing.mp3Ringtone for outgoing calls (optional)
caution

If you skip this step, call initialization will fail at runtime because the WASM module cannot be loaded. Always run npx ermis-init-call after installing or updating the SDK.

tip

For non-standard setups (e.g., assets served from a CDN), you can manually place the files wherever you want and override paths via callWasmPath, incomingCallAudioPath, and outgoingCallAudioPath props on ChatProvider.


Quick Start

The fastest way to enable calls is through ChatProvider:

import { ChatProvider, ChannelList, Channel, VirtualMessageList, MessageInput } from '@ermis-network/ermis-chat-react';
import '@ermis-network/ermis-chat-react/dist/css/index.css';

function App() {
return (
<ChatProvider
client={chatClient}
enableCall={true}
callSessionId="unique-session-id"
onCallEnd={(duration) => console.log(`Call ended after ${duration}s`)}
>
<ChannelList />
<Channel>
{/* ChannelHeader automatically shows call buttons */}
<VirtualMessageList />
<MessageInput />
</Channel>
</ChatProvider>
);
}

That's it! The ChannelHeader component will automatically display audio and video call buttons, and the ErmisCallUI modal handles the entire call lifecycle.


ChatProvider Call Props

When enableCall is set to true, ChatProvider internally mounts ErmisCallProvider and ErmisCallUI. You can configure both through these props:

Configuration

PropTypeDefaultDescription
enableCallbooleanfalseEnables the call feature.
callSessionIdstringRequired. Unique session ID for the call node.
callWasmPathstring'/ermis_call_node_wasm_bg.wasm'Path to the WASM module in your public directory.
callRelayUrlstring'https://iroh-relay.ermis.network:8443'Relay server URL for NAT traversal.
CallUIComponentReact.ComponentTypeErmisCallUIReplace the entire default call UI.
incomingCallAudioPathstring'/call_incoming.mp3'Audio file for incoming call ringtone.
outgoingCallAudioPathstring'/call_outgoing.mp3'Audio file for outgoing call ringtone.

Lifecycle Callbacks

PropTypeDescription
onCallStart(callType: 'audio' | 'video', cid: string) => voidCalled when the local user initiates a call.
onCallEnd(duration: number) => voidCalled when a call ends (duration in seconds).
onCallError(error: string) => voidCalled when a call error occurs.
onIncomingCall(callerInfo: UserCallInfo) => voidCalled when an incoming call is received.
onCallAccepted() => voidCalled when the local user accepts a call.
onCallRejected() => voidCalled when the local user rejects a call.

Example: Analytics & Notifications

<ChatProvider
client={chatClient}
enableCall={true}
callSessionId={sessionId}
onCallStart={(type, cid) => {
analytics.track('call_started', { type, channelId: cid });
}}
onCallEnd={(duration) => {
analytics.track('call_ended', { duration });
}}
onCallError={(error) => {
toast.error(`Call failed: ${error}`);
}}
onIncomingCall={(caller) => {
// Show a system notification
new Notification(`${caller.name} is calling you`);
}}
>
{children}
</ChatProvider>

<ErmisCallUI />

The default call UI component that renders the full call experience — ringing screen, active call (audio/video), and error states.

If you use ChatProvider with enableCall, this component is mounted automatically. You only need to render it manually if you're building a custom integration with ErmisCallProvider directly.

Core Configs

PropTypeDefaultDescription
classNamestringAdditional CSS class name on the root element.
suppressIncomingCallsbooleanfalseIf true, hides the incoming call UI. Useful for "Do Not Disturb" mode.
onCallDurationChange(seconds: number) => voidCalled every second with the current call duration.
incomingCallAudioPathstring'/call_incoming.mp3'Audio file for incoming call ringtone.
outgoingCallAudioPathstring'/call_outgoing.mp3'Audio file for outgoing call ringtone.

Icon Overrides

Every icon in the call UI can be replaced with a custom component:

PropTypeDescription
AvatarComponentReact.ComponentType<AvatarProps>Avatar shown during ringing and audio call.
MicIconReact.ComponentTypeMicrophone on icon.
MicOffIconReact.ComponentTypeMicrophone muted icon.
VideoIconReact.ComponentTypeCamera on icon.
VideoOffIconReact.ComponentTypeCamera off icon.
PhoneIconReact.ComponentTypePhone icon (accept/reject/end).
ScreenShareIconReact.ComponentTypeScreen sharing active icon.
ScreenShareOffIconReact.ComponentTypeScreen sharing inactive icon.
FullscreenIconReact.ComponentTypeEnter fullscreen icon.
ExitFullscreenIconReact.ComponentTypeExit fullscreen icon.
UpgradeCallIconReact.ComponentTypeUpgrade call (audio → video) button icon.

Localization (I18n)

All text labels in the call UI can be overridden for internationalization:

PropTypeDefault
incomingCallTitle(callType: string) => string(type) => `Incoming ${type} call`
outgoingCallTitle(callType: string) => string(type) => `Outgoing ${type} call`
ongoingCallTitle(callType: string) => string(type) => `Ongoing ${type} Call`
isCallingYouLabelstring'is calling you...'
ringingLabelstring'Ringing...'
rejectCallLabelstring'Reject'
acceptCallLabelstring'Accept'
endCallLabelstring'End Call'
cancelLabelstring'Cancel'
toggleMicTitlestring'Toggle Mic'
toggleVideoTitlestring'Toggle Video'
shareScreenTitlestring'Share Screen'
stopScreenShareTitlestring'Stop Sharing'
connectedLabelstring'Connected'
audioCallBadgeLabelstring'Audio Call'
videoCallBadgeLabelstring'Video Call'
fullscreenTitlestring'Fullscreen'
exitFullscreenTitlestring'Exit Fullscreen'
upgradeCallTitlestring'Request Video Upgrade'

Component Slots

For maximum flexibility, you can replace entire sections of the call UI:

PropTypeDescription
RingingComponentReact.ComponentType<ErmisCallRingingProps>Replace the ringing/incoming call view.
ConnectedAudioComponentReact.ComponentType<ErmisCallConnectedAudioProps>Replace the active audio call view.
ConnectedVideoComponentReact.ComponentType<ErmisCallConnectedVideoProps>Replace the active video call view.
ErrorComponentReact.ComponentType<ErmisCallErrorProps>Replace the error state view.
ControlsBarComponentReact.ComponentType<ErmisCallControlsBarProps>Replace the controls bar (mic, video, screen share, etc).

Example: Custom Ringing Screen

import { ErmisCallUI } from '@ermis-network/ermis-chat-react';
import type { ErmisCallRingingProps } from '@ermis-network/ermis-chat-react';

const MyRingingScreen = ({ peerInfo, callType, isIncoming, acceptCall, rejectCall }: ErmisCallRingingProps) => (
<div className="my-ringing-screen">
<img src={peerInfo?.avatar} alt={peerInfo?.name} />
<h2>{peerInfo?.name}</h2>
<p>{isIncoming ? 'Incoming' : 'Outgoing'} {callType} call</p>
{isIncoming && (
<div>
<button onClick={acceptCall}>Accept</button>
<button onClick={rejectCall}>Decline</button>
</div>
)}
</div>
);

// Usage
<ErmisCallUI RingingComponent={MyRingingScreen} />

Example: Do Not Disturb Mode

const [isDND, setIsDND] = useState(false);

<ErmisCallUI suppressIncomingCalls={isDND} />

useCallContext Hook

Access the full call state and actions from anywhere within the ErmisCallProvider tree.

import { useCallContext } from '@ermis-network/ermis-chat-react';

Returns: CallContextValue

PropertyTypeDescription
callStatusCallStatus | ''Current status: 'ringing', 'connected', or ''.
callTypestringCall type: 'audio' or 'video'.
callDurationnumberDuration in seconds (ticks every second while connected).
callerInfoUserCallInfo | undefinedInfo about the caller (incoming calls).
receiverInfoUserCallInfo | undefinedInfo about the receiver (outgoing calls).
isIncomingbooleanWhether the current call is incoming.
localStreamMediaStream | nullThe local user's media stream.
remoteStreamMediaStream | nullThe remote peer's media stream.
isMicMutedbooleanWhether the local microphone is muted.
isVideoMutedbooleanWhether the local camera is off.
isScreenSharingbooleanWhether screen sharing is active.
isRemoteMicMutedbooleanWhether the remote peer's mic is muted.
isRemoteVideoMutedbooleanWhether the remote peer's camera is off.
errorMessagestring | nullCurrent error message, if any.
audioDevicesMediaDeviceInfo[]Available audio input devices.
videoDevicesMediaDeviceInfo[]Available video input devices.
selectedAudioDeviceIdstringCurrently selected mic device ID.
selectedVideoDeviceIdstringCurrently selected camera device ID.

Actions

MethodSignatureDescription
createCall(type: 'audio' | 'video', cid: string) => Promise<void>Initiate an outgoing call.
acceptCall() => Promise<void>Accept an incoming call.
rejectCall() => Promise<void>Reject an incoming call.
endCall() => Promise<void>End the current call.
toggleMic() => voidToggle microphone on/off.
toggleVideo() => voidToggle camera on/off.
toggleScreenShare() => Promise<void>Toggle screen sharing.
upgradeCall() => Promise<void>Upgrade audio call to video.
switchAudioDevice(deviceId: string) => Promise<void>Switch to a different microphone.
switchVideoDevice(deviceId: string) => Promise<void>Switch to a different camera.
clearError() => voidClear the current error message.

Example: Custom Call Header Badge

import { useCallContext } from '@ermis-network/ermis-chat-react';

const CallDurationBadge = () => {
const { callStatus, callDuration, callType } = useCallContext();

if (callStatus !== 'connected') return null;

const mins = Math.floor(callDuration / 60).toString().padStart(2, '0');
const secs = (callDuration % 60).toString().padStart(2, '0');

return (
<div className="call-badge">
<span className="call-badge__dot" />
{callType === 'video' ? '📹' : '📞'} {mins}:{secs}
</div>
);
};

Example: Building a Completely Custom Call UI

import { useCallContext } from '@ermis-network/ermis-chat-react';

const MyCallUI = () => {
const {
callStatus, callType, isIncoming,
callerInfo, receiverInfo,
acceptCall, rejectCall, endCall,
toggleMic, isMicMuted,
toggleVideo, isVideoMuted,
callDuration,
} = useCallContext();

if (!callStatus) return null;
const peer = isIncoming ? callerInfo : receiverInfo;

return (
<div className="my-call-ui">
<h2>{peer?.name}</h2>
<p>Status: {callStatus} | Duration: {callDuration}s</p>

{callStatus === 'ringing' && isIncoming && (
<div>
<button onClick={acceptCall}>Answer</button>
<button onClick={rejectCall}>Decline</button>
</div>
)}

{callStatus === 'connected' && (
<div>
<button onClick={toggleMic}>{isMicMuted ? 'Unmute' : 'Mute'}</button>
<button onClick={toggleVideo}>{isVideoMuted ? 'Camera On' : 'Camera Off'}</button>
<button onClick={endCall}>Hang Up</button>
</div>
)}
</div>
);
};
tip

When building a fully custom UI, set CallUIComponent on ChatProvider to your custom component so the default ErmisCallUI is not rendered.


Theming & CSS

The call UI uses CSS variables (tokens) for theming, defined in _tokens.css. Override these in your CSS to match your application's look and feel.

Call UI Tokens

TokenDark Mode DefaultLight Mode DefaultPurpose
--ermis-call-bglinear-gradient(135deg, #0f0f1a, #1a1a2e, #16213e)linear-gradient(135deg, #f8f9fa, #e9ecef, #dee2e6)Call modal background gradient.
--ermis-call-glassrgba(255,255,255,0.06)rgba(0,0,0,0.04)Glassmorphism control bar background.
--ermis-call-glass-borderrgba(255,255,255,0.1)rgba(0,0,0,0.08)Glassmorphism border.
--ermis-call-pulsergba(99,102,241,0.4)rgba(99,102,241,0.3)Ringing pulse animation color.

Signal Message Tokens

TokenDark Mode DefaultLight Mode DefaultPurpose
--ermis-signal-success#54D62C#229A16Completed call icon/text color.
--ermis-signal-missed#FF4842#B72136Missed/rejected call icon/text color.
--ermis-signal-bgrgba(255,255,255,0.04)rgba(0,0,0,0.03)Signal message background.
--ermis-signal-own-success#86EFAC#86EFACOwn completed call color.
--ermis-signal-own-missed#FCA5A5#FCA5A5Own missed call color.

Example: Custom Theme Override

.ermis-chat--dark {
--ermis-call-bg: linear-gradient(135deg, #1a0030, #2d0050);
--ermis-call-pulse: rgba(147, 51, 234, 0.5);
--ermis-signal-success: #10b981;
--ermis-signal-missed: #ef4444;
}

BEM Class Reference

All call UI elements follow BEM naming convention with the ermis-call-ui block:

ClassElement
.ermis-call-uiRoot container
.ermis-call-ui--fullscreenFullscreen modifier
.ermis-call-ui__ringingRinging state container
.ermis-call-ui__ringing-avatarAvatar with pulse rings
.ermis-call-ui__ringing-actionsAccept/reject buttons
.ermis-call-ui__activeConnected state container
.ermis-call-ui__video-containerVideo call layout
.ermis-call-ui__video-remoteRemote video stream
.ermis-call-ui__video-localLocal video PiP
.ermis-call-ui__audio-containerAudio call layout
.ermis-call-ui__controlsControls bar
.ermis-call-ui__control-btnIndividual control button
.ermis-call-ui__control-btn--mutedMuted state modifier
.ermis-call-ui__control-btn--dangerEnd call button
.ermis-call-ui__timerCall duration display
.ermis-call-ui__errorError state container

Signal Messages

When a call ends, the SDK automatically creates a signal message in the channel to record the call history. These messages display as inline call logs within the chat feed.

Signal messages are rendered by the SignalMessage component and show:

  • Call type icon (audio/video)
  • Call result text (e.g., "Audio call", "Missed audio call")
  • Call duration (if the call was answered)

Customizing Signal Messages

You can override the signal message renderer using the messageRenderers prop on VirtualMessageList:

import { VirtualMessageList } from '@ermis-network/ermis-chat-react';
import type { MessageRendererProps } from '@ermis-network/ermis-chat-react';

const MySignalMessage = ({ message }: MessageRendererProps) => (
<div className="my-call-log">
📞 {message.text}
</div>
);

<VirtualMessageList
messageRenderers={{
signal: MySignalMessage,
}}
/>