Skip to main content
Build a custom control panel to replace or extend the default call controls. Hide the default layout and implement your own UI using the SDK’s action methods.

Hide Default Layout

Disable the default control panel to use your own:
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';

const callSettings = new CometChatCalls.CallSettingsBuilder()
  .enableDefaultLayout(false)
  .build();

Available Actions

Use these methods to control the call from your custom UI:
ActionMethodDescription
Mute AudioCometChatCalls.muteAudio()Mute local microphone
Unmute AudioCometChatCalls.unMuteAudio()Unmute local microphone
Pause VideoCometChatCalls.pauseVideo()Stop sending video
Resume VideoCometChatCalls.resumeVideo()Resume sending video
Switch CameraCometChatCalls.switchCamera()Toggle front/rear camera
Leave SessionCometChatCalls.leaveSession()End the call
Raise HandCometChatCalls.raiseHand()Raise hand
Lower HandCometChatCalls.lowerHand()Lower hand
Set LayoutCometChatCalls.setLayout(layout)Change call layout
Start RecordingCometChatCalls.startRecording()Start recording
Stop RecordingCometChatCalls.stopRecording()Stop recording
Start Screen ShareCometChatCalls.startScreenSharing()Share screen
Stop Screen ShareCometChatCalls.stopScreenSharing()Stop sharing
Enable PiPCometChatCalls.enablePictureInPictureLayout()Enter PiP mode
Disable PiPCometChatCalls.disablePictureInPictureLayout()Exit PiP mode

Complete Example

import React, { useState, useEffect } from 'react';
import { View, TouchableOpacity, Text, StyleSheet, SafeAreaView } from 'react-native';
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';

interface CustomControlPanelProps {
  isAudioOnly?: boolean;
}

function CustomControlPanel({ isAudioOnly = false }: CustomControlPanelProps) {
  const [isAudioMuted, setIsAudioMuted] = useState(false);
  const [isVideoMuted, setIsVideoMuted] = useState(false);
  const [isHandRaised, setIsHandRaised] = useState(false);
  const [isRecording, setIsRecording] = useState(false);
  const [currentLayout, setCurrentLayout] = useState<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'>('TILE');

  useEffect(() => {
    // Listen for media events
    const unsubscribeAudioMuted = CometChatCalls.addEventListener(
      'onAudioMuted',
      () => setIsAudioMuted(true)
    );
    const unsubscribeAudioUnmuted = CometChatCalls.addEventListener(
      'onAudioUnMuted',
      () => setIsAudioMuted(false)
    );
    const unsubscribeVideoPaused = CometChatCalls.addEventListener(
      'onVideoPaused',
      () => setIsVideoMuted(true)
    );
    const unsubscribeVideoResumed = CometChatCalls.addEventListener(
      'onVideoResumed',
      () => setIsVideoMuted(false)
    );
    const unsubscribeRecordingStarted = CometChatCalls.addEventListener(
      'onRecordingStarted',
      () => setIsRecording(true)
    );
    const unsubscribeRecordingStopped = CometChatCalls.addEventListener(
      'onRecordingStopped',
      () => setIsRecording(false)
    );
    const unsubscribeLayoutChanged = CometChatCalls.addEventListener(
      'onCallLayoutChanged',
      (layout: 'TILE' | 'SIDEBAR' | 'SPOTLIGHT') => setCurrentLayout(layout)
    );

    return () => {
      unsubscribeAudioMuted();
      unsubscribeAudioUnmuted();
      unsubscribeVideoPaused();
      unsubscribeVideoResumed();
      unsubscribeRecordingStarted();
      unsubscribeRecordingStopped();
      unsubscribeLayoutChanged();
    };
  }, []);

  const toggleAudio = () => {
    if (isAudioMuted) {
      CometChatCalls.unMuteAudio();
    } else {
      CometChatCalls.muteAudio();
    }
  };

  const toggleVideo = () => {
    if (isVideoMuted) {
      CometChatCalls.resumeVideo();
    } else {
      CometChatCalls.pauseVideo();
    }
  };

  const toggleHand = () => {
    if (isHandRaised) {
      CometChatCalls.lowerHand();
      setIsHandRaised(false);
    } else {
      CometChatCalls.raiseHand();
      setIsHandRaised(true);
    }
  };

  const toggleRecording = () => {
    if (isRecording) {
      CometChatCalls.stopRecording();
    } else {
      CometChatCalls.startRecording();
    }
  };

  const cycleLayout = () => {
    const layouts: Array<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'> = ['TILE', 'SIDEBAR', 'SPOTLIGHT'];
    const currentIndex = layouts.indexOf(currentLayout);
    const nextLayout = layouts[(currentIndex + 1) % layouts.length];
    CometChatCalls.setLayout(nextLayout);
  };

  const handleEndCall = () => {
    CometChatCalls.leaveSession();
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.controlRow}>
        {/* Audio Toggle */}
        <TouchableOpacity
          style={[styles.controlButton, isAudioMuted && styles.activeButton]}
          onPress={toggleAudio}
        >
          <Text style={styles.buttonIcon}>{isAudioMuted ? '🔇' : '🎤'}</Text>
          <Text style={styles.buttonLabel}>
            {isAudioMuted ? 'Unmute' : 'Mute'}
          </Text>
        </TouchableOpacity>

        {/* Video Toggle (only for video calls) */}
        {!isAudioOnly && (
          <TouchableOpacity
            style={[styles.controlButton, isVideoMuted && styles.activeButton]}
            onPress={toggleVideo}
          >
            <Text style={styles.buttonIcon}>{isVideoMuted ? '📷' : '🎥'}</Text>
            <Text style={styles.buttonLabel}>
              {isVideoMuted ? 'Show' : 'Hide'}
            </Text>
          </TouchableOpacity>
        )}

        {/* Switch Camera (only for video calls) */}
        {!isAudioOnly && (
          <TouchableOpacity
            style={styles.controlButton}
            onPress={() => CometChatCalls.switchCamera()}
          >
            <Text style={styles.buttonIcon}>🔄</Text>
            <Text style={styles.buttonLabel}>Flip</Text>
          </TouchableOpacity>
        )}

        {/* Raise Hand */}
        <TouchableOpacity
          style={[styles.controlButton, isHandRaised && styles.handRaisedButton]}
          onPress={toggleHand}
        >
          <Text style={styles.buttonIcon}>{isHandRaised ? '✋' : '🤚'}</Text>
          <Text style={styles.buttonLabel}>
            {isHandRaised ? 'Lower' : 'Raise'}
          </Text>
        </TouchableOpacity>
      </View>

      <View style={styles.controlRow}>
        {/* Layout Toggle */}
        <TouchableOpacity style={styles.controlButton} onPress={cycleLayout}>
          <Text style={styles.buttonIcon}>📐</Text>
          <Text style={styles.buttonLabel}>{currentLayout}</Text>
        </TouchableOpacity>

        {/* Recording */}
        <TouchableOpacity
          style={[styles.controlButton, isRecording && styles.recordingButton]}
          onPress={toggleRecording}
        >
          <Text style={styles.buttonIcon}>{isRecording ? '⏹️' : '⏺️'}</Text>
          <Text style={styles.buttonLabel}>
            {isRecording ? 'Stop' : 'Record'}
          </Text>
        </TouchableOpacity>

        {/* End Call */}
        <TouchableOpacity
          style={[styles.controlButton, styles.endCallButton]}
          onPress={handleEndCall}
        >
          <Text style={styles.buttonIcon}>📞</Text>
          <Text style={styles.buttonLabel}>End</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.8)',
    paddingVertical: 16,
    paddingHorizontal: 8,
  },
  controlRow: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 12,
    gap: 12,
  },
  controlButton: {
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#333',
    width: 64,
    height: 64,
    borderRadius: 32,
  },
  activeButton: {
    backgroundColor: '#6851D6',
  },
  handRaisedButton: {
    backgroundColor: '#f59e0b',
  },
  recordingButton: {
    backgroundColor: '#ef4444',
  },
  endCallButton: {
    backgroundColor: '#dc2626',
  },
  buttonIcon: {
    fontSize: 24,
  },
  buttonLabel: {
    color: '#fff',
    fontSize: 10,
    marginTop: 4,
  },
});

export default CustomControlPanel;

Using with Call Component

Combine the custom control panel with the call component:
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
import CustomControlPanel from './CustomControlPanel';

interface CallScreenProps {
  callToken: string;
  isAudioOnly?: boolean;
}

function CallScreen({ callToken, isAudioOnly = false }: CallScreenProps) {
  const callSettings = new CometChatCalls.CallSettingsBuilder()
    .enableDefaultLayout(false) // Hide default controls
    .setIsAudioOnlyCall(isAudioOnly)
    .build();

  return (
    <View style={styles.container}>
      <CometChatCalls.Component
        callToken={callToken}
        callSettings={callSettings}
      />
      <CustomControlPanel isAudioOnly={isAudioOnly} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
});

export default CallScreen;