
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
react-native-webrtc-call
Advanced tools
A complete WebRTC calling solution for React Native with UI components, Context API integration, and signaling support
A complete WebRTC calling solution for React Native with UI components, Context API integration, and signaling support. This package provides everything you need to implement audio and video calling in your React Native application.
npm install react-native-webrtc-call
# or
yarn add react-native-webrtc-call
Make sure you have these peer dependencies installed:
npm install react-native-webrtc react-native-incall-manager socket.io-client react-native-flash-message @react-native-async-storage/async-storage react-native-vector-icons
Note: react-native-vector-icons is used for UI icons. If you're using Expo, you can use @expo/vector-icons instead (which is included by default in Expo projects).
This package ships both the original TypeScript sources under src/ and the compiled JavaScript output under lib/. Consumers rely on the lib/ bundle when the package is published, while contributors typically edit the files in src/ and then run the build to regenerate lib/. Seeing duplicate filenames in both folders is expected—avoid editing only one side or your changes may be lost the next time the build runs.
For iOS, you may need to add permissions to Info.plist:
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for video calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for audio calls</string>
For Android, add permissions to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Before initializing the WebRTC service, you need to set up a signaling server. Here's a complete Express/Socket.io signaling server implementation:
Create a new file server.js in your project:
const express = require("express");
const http = require("http");
const socketIo = require("socket.io");
const cors = require("cors");
require("dotenv").config();
const twilio = require("twilio");
const app = express();
const server = http.createServer(app);
app.use(cors());
app.use(express.json());
const io = socketIo(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
// Store connected users
const users = new Map();
const activeRooms = new Map();
// ICE configuration cache
let cachedIceConfig = null;
let cachedIceExpiresAt = 0;
function isTwilioConfigured() {
return (
Boolean(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) ||
Boolean(process.env.TWILIO_API_KEY_SID && process.env.TWILIO_API_KEY_SECRET)
);
}
function createTwilioClient() {
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const apiKeySid = process.env.TWILIO_API_KEY_SID;
const apiKeySecret = process.env.TWILIO_API_KEY_SECRET;
if (apiKeySid && apiKeySecret && accountSid) {
return twilio(apiKeySid, apiKeySecret, { accountSid });
}
if (apiKeySid && apiKeySecret) {
return twilio(apiKeySid, apiKeySecret);
}
if (accountSid && authToken) {
return twilio(accountSid, authToken);
}
return null;
}
async function fetchTwilioIceServers() {
const ttlSeconds = Number(process.env.ICE_TTL_SECONDS || 3600);
const client = createTwilioClient();
if (!client) return null;
const token = await client.tokens.create({ ttl: ttlSeconds });
return {
iceServers: token.iceServers || [],
ttl: ttlSeconds,
provider: "twilio",
};
}
async function getIceConfig() {
const now = Date.now();
// Refresh 30 seconds before expiry
const refreshBufferMs = 30 * 1000;
if (cachedIceConfig && now < cachedIceExpiresAt - refreshBufferMs) {
return cachedIceConfig;
}
if (isTwilioConfigured()) {
try {
const twilioConfig = await fetchTwilioIceServers();
if (
twilioConfig &&
twilioConfig.iceServers &&
twilioConfig.iceServers.length
) {
cachedIceConfig = twilioConfig;
cachedIceExpiresAt = now + (twilioConfig.ttl || 3600) * 1000;
return cachedIceConfig;
}
} catch (err) {
console.error("❌ Failed to fetch Twilio ICE servers:", err.message);
}
}
// Fallback to public Twilio STUN only (no TURN)
cachedIceConfig = {
iceServers: [{ urls: ["stun:global.stun.twilio.com:3478"] }],
ttl: 3600,
provider: "static-stun",
note: "Using public STUN only. Configure TWILIO_* env vars to enable TURN for NAT traversal.",
};
cachedIceExpiresAt = now + cachedIceConfig.ttl * 1000;
return cachedIceConfig;
}
// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: "ok",
connectedUsers: users.size,
activeRooms: activeRooms.size,
timestamp: new Date().toISOString(),
iceProvider: cachedIceConfig
? cachedIceConfig.provider
: isTwilioConfigured()
? "twilio"
: "static-stun",
});
});
// Endpoint to retrieve ICE configuration for clients
app.get("/ice", async (req, res) => {
try {
const config = await getIceConfig();
res.json({
...config,
fetchedAt: new Date().toISOString(),
expiresAt: new Date(cachedIceExpiresAt).toISOString(),
});
} catch (error) {
console.error("❌ /ice error:", error);
res.status(500).json({ error: "Failed to load ICE configuration" });
}
});
// Get connected users (for debugging)
app.get("/users", (req, res) => {
const userList = Array.from(users.entries()).map(([userId, userData]) => ({
userId,
userType: userData.userType,
connected: true,
}));
res.json(userList);
});
io.on("connection", (socket) => {
console.log("🔌 User connected:", socket.id);
// Register user with their profile information
socket.on("register", (userData) => {
try {
users.set(userData.userId, {
socketId: socket.id,
userType: userData.userType,
connectedAt: new Date().toISOString(),
...userData,
});
console.log("👤 User registered:", {
userId: userData.userId,
userType: userData.userType,
socketId: socket.id,
});
// Confirm registration with ICE configuration
getIceConfig()
.then((iceConfig) => {
socket.emit("registered", {
success: true,
userId: userData.userId,
connectedUsers: users.size,
iceServers: iceConfig.iceServers,
iceProvider: iceConfig.provider,
});
})
.catch(() => {
socket.emit("registered", {
success: true,
userId: userData.userId,
connectedUsers: users.size,
iceServers: [{ urls: ["stun:global.stun.twilio.com:3478"] }],
iceProvider: "static-stun",
});
});
} catch (error) {
console.error("❌ Registration error:", error);
socket.emit("registered", { success: false, error: error.message });
}
});
// Handle all signaling messages
socket.on("signaling-message", (message) => {
try {
const senderId = getUserIdBySocketId(socket.id);
const recipient = users.get(message.to);
console.log("📡 Signaling message:", {
type: message.type,
from: senderId,
to: message.to,
callId: message.callId,
recipientFound: !!recipient,
});
if (recipient) {
// Forward the message to the recipient
io.to(recipient.socketId).emit("signaling-message", {
...message,
from: senderId,
timestamp: new Date().toISOString(),
});
// Handle room management for calls
if (message.type === "call-request") {
activeRooms.set(message.callId, {
caller: senderId,
callee: message.to,
startTime: new Date().toISOString(),
status: "ringing",
});
} else if (message.type === "call-accept") {
const room = activeRooms.get(message.callId);
if (room) {
room.status = "active";
room.acceptTime = new Date().toISOString();
}
} else if (
message.type === "call-end" ||
message.type === "call-reject"
) {
const room = activeRooms.get(message.callId);
if (room) {
room.status = message.type === "call-end" ? "ended" : "rejected";
room.endTime = new Date().toISOString();
// Clean up room after 5 minutes
setTimeout(() => {
activeRooms.delete(message.callId);
}, 5 * 60 * 1000);
}
}
console.log("✅ Message forwarded successfully");
} else {
console.log("❌ Recipient not found:", message.to);
// Notify sender that recipient is not available
socket.emit("signaling-error", {
type: "recipient-not-found",
message: "The person you are trying to call is not available",
originalMessage: message,
});
}
} catch (error) {
console.error("❌ Signaling error:", error);
socket.emit("signaling-error", {
type: "server-error",
message: "An error occurred while processing your request",
error: error.message,
});
}
});
// Handle user status updates
socket.on("update-status", (status) => {
const userId = getUserIdBySocketId(socket.id);
if (userId && users.has(userId)) {
const userData = users.get(userId);
userData.status = status;
userData.lastStatusUpdate = new Date().toISOString();
users.set(userId, userData);
console.log("📊 Status updated:", { userId, status });
}
});
// Handle ping/pong for connection health
socket.on("ping", () => {
socket.emit("pong", { timestamp: new Date().toISOString() });
});
// Handle disconnect
socket.on("disconnect", (reason) => {
const userId = getUserIdBySocketId(socket.id);
if (userId) {
// Clean up any active calls
for (const [callId, room] of activeRooms.entries()) {
if (room.caller === userId || room.callee === userId) {
room.status = "disconnected";
room.endTime = new Date().toISOString();
// Notify the other party
const otherUserId =
room.caller === userId ? room.callee : room.caller;
const otherUser = users.get(otherUserId);
if (otherUser) {
io.to(otherUser.socketId).emit("signaling-message", {
type: "call-end",
callId: callId,
from: userId,
reason: "peer-disconnected",
});
}
}
}
users.delete(userId);
console.log("👋 User disconnected:", {
userId,
socketId: socket.id,
reason,
remainingUsers: users.size,
});
} else {
console.log("👋 Unknown user disconnected:", socket.id);
}
});
// Handle errors
socket.on("error", (error) => {
console.error("🔥 Socket error:", error);
});
});
// Helper function to get user ID by socket ID
function getUserIdBySocketId(socketId) {
for (const [userId, userData] of users.entries()) {
if (userData.socketId === socketId) {
return userId;
}
}
return null;
}
// Cleanup inactive rooms periodically
setInterval(() => {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
for (const [callId, room] of activeRooms.entries()) {
const roomTime = new Date(room.endTime || room.startTime);
if (roomTime < oneHourAgo) {
activeRooms.delete(callId);
}
}
}, 10 * 60 * 1000); // Run every 10 minutes
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("🛑 SIGTERM received, shutting down gracefully");
server.close(() => {
console.log("✅ Server closed");
process.exit(0);
});
});
process.on("SIGINT", () => {
console.log("🛑 SIGINT received, shutting down gracefully");
server.close(() => {
console.log("✅ Server closed");
process.exit(0);
});
});
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
console.log(`🚀 PlugEx Signaling Server running on port ${PORT}`);
console.log(`📊 Health check available at http://localhost:${PORT}/health`);
console.log(`👥 Users endpoint available at http://localhost:${PORT}/users`);
});
Install the required dependencies:
npm install express socket.io cors dotenv twilio
# or
yarn add express socket.io cors dotenv twilio
Create a .env file in your server directory:
PORT=3001
ICE_TTL_SECONDS=3600
# Optional: Twilio Configuration (for TURN servers)
# Option 1: Using Account SID and Auth Token
TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
# Option 2: Using API Key (recommended)
TWILIO_API_KEY_SID=your_api_key_sid
TWILIO_API_KEY_SECRET=your_api_key_secret
node server.js
The server will start on port 3001 (or the port specified in your .env file).
Wrap your app (or the part that needs calling functionality) with WebRTCCallManagerWrapper and pass the configuration as props:
import React from "react";
import { WebRTCCallManagerWrapper } from "react-native-webrtc-call";
import AsyncStorage from "@react-native-async-storage/async-storage";
const App = () => {
return (
<WebRTCCallManagerWrapper
config={{
// Use http:// or https:// (not ws:// or wss://)
// The service will automatically convert to WebSocket protocol
signalingServerUrl: "http://localhost:3001", // For development
// signalingServerUrl: 'https://your-signaling-server.com', // For production
// Optional: If your server provides ICE servers via /ice endpoint
turnEndpoint: "http://localhost:3001/ice", // Optional
icePolicy: "all", // or 'relay'
defaultCallType: "p2p", // Default to P2P, can be 'group'
// Optional: Provide custom token retrieval
getUserToken: async () => {
return await AsyncStorage.getItem("userToken");
},
// Optional: Provide custom user profile
getUserProfile: () => {
return {
_id: "user123",
firstName: "John",
lastName: "Doe",
userType: "user",
profilePicture: "https://...",
};
},
}}
userId="user123" // Required for user registration
userName="John Doe" // Optional - for display purposes
userAvatar="https://..." // Optional - for display purposes
onEvents={(event) => {
// Optional: Handle background events
console.log("WebRTC Event:", event);
}}
>
{/* Your app components */}
</WebRTCCallManagerWrapper>
);
};
export default App;
Important Notes:
http:// or https:// for signalingServerUrl (not ws:// or wss://)http://localhost:3001https://userId is required for user registration with the signaling servergetUserProfile is not provided, it will be built from userId, userName, and userAvatar propsWhen using WebRTCCallManagerWrapper, the service is created internally. For components that need to initiate calls, you have a few options:
If you need direct access to the service methods, create a separate service instance with the same configuration:
// services/webRTCService.ts
import { WebRTCService, WebRTCConfig } from "react-native-webrtc-call";
import AsyncStorage from "@react-native-async-storage/async-storage";
const config: WebRTCConfig = {
signalingServerUrl: "http://localhost:3001",
turnEndpoint: "http://localhost:3001/ice",
icePolicy: "all",
defaultCallType: "p2p",
getUserToken: async () => {
return await AsyncStorage.getItem("userToken");
},
getUserProfile: () => {
return {
_id: "user123",
firstName: "John",
lastName: "Doe",
userType: "user",
};
},
};
export const webRTCService = new WebRTCService(config);
Then use it in your components:
import { useWebRTC } from "react-native-webrtc-call";
import { webRTCService } from "./services/webRTCService";
const MyComponent = () => {
const { initiateCall } = useWebRTC();
const handleCall = async () => {
try {
// P2P call (default)
const callId = await webRTCService.initiateCall(
"recipient-id",
false // video: false for audio, true for video
);
initiateCall(
{
id: "recipient-id",
name: "Recipient Name",
avatar: "https://...",
userType: "user",
},
callId
);
} catch (error) {
console.error("Call failed:", error);
}
};
return <Button onPress={handleCall} title="Call" />;
};
import { CallButton } from "react-native-webrtc-call";
import { webRTCService } from "./services/webRTCService";
const MyComponent = () => {
return (
<CallButton
recipientId="recipient-id"
recipientName="Recipient Name"
webRTCService={webRTCService}
onInitiateCall={(callId) => {
// Optional: Handle call initiation
console.log("Call initiated:", callId);
}}
/>
);
};
import { useWebRTC } from "react-native-webrtc-call";
import { webRTCService } from "./services/webRTCService";
const MyComponent = () => {
const { initiateCall, setCallType } = useWebRTC();
const handleGroupCall = async () => {
try {
const participantIds = ["user1", "user2", "user3"];
// Group call
const callId = await webRTCService.initiateGroupCall(
participantIds,
true // video: true for video, false for audio
);
setCallType("group");
initiateCall(
{
id: "group",
name: "Group Call",
},
callId
);
} catch (error) {
console.error("Group call failed:", error);
}
};
return <Button onPress={handleGroupCall} title="Start Group Call" />;
};
The WebRTCCallManagerWrapper automatically displays the IncomingCallScreen when there's an incoming call. You can also use the IncomingCallScreen component directly:
import { IncomingCallScreen } from "react-native-webrtc-call";
import { webRTCService } from "./services/webRTCService";
const MyApp = () => {
return <IncomingCallScreen webRTCService={webRTCService} />;
};
Or handle it manually:
import { useWebRTC } from "react-native-webrtc-call";
import { webRTCService } from "./services/webRTCService";
const IncomingCallHandler = () => {
const { state, acceptCall, rejectCall } = useWebRTC();
const { isIncomingCall, caller } = state;
const handleAccept = async () => {
acceptCall();
await webRTCService.acceptCall(false); // false for audio, true for video
};
const handleReject = () => {
rejectCall();
webRTCService.rejectCall();
};
if (!isIncomingCall) return null;
return (
<View>
<Text>Incoming call from {caller?.name}</Text>
<Button onPress={handleAccept} title="Accept" />
<Button onPress={handleReject} title="Reject" />
</View>
);
};
import { webRTCService } from "./services/webRTCService";
const CallControls = () => {
const handleMute = () => {
webRTCService.toggleMute();
};
const handleVideo = () => {
webRTCService.toggleVideo();
};
const handleSpeaker = () => {
webRTCService.toggleSpeaker();
};
const handleEndCall = () => {
webRTCService.endCall();
};
return (
<View>
<Button onPress={handleMute} title="Mute" />
<Button onPress={handleVideo} title="Video" />
<Button onPress={handleSpeaker} title="Speaker" />
<Button onPress={handleEndCall} title="End Call" />
</View>
);
};
initiateCall(calleeId: string, video?: boolean, callIdOverride?: string): Promise<string>
initiateP2PCall(calleeId: string, video?: boolean, callIdOverride?: string): Promise<string>
initiateGroupCall(participantIds: string[], video?: boolean, callIdOverride?: string): Promise<string>
acceptCall(video?: boolean): Promise<void>
rejectCall(): void
endCall(reason?: string): void
toggleMute(): void
toggleVideo(): Promise<void>
toggleSpeaker(): void
getFormattedCallDuration(seconds: number): string
cleanup(): void
getState(): Partial<CallState>
The package provides a useWebRTC hook that gives you access to:
import { useWebRTC } from "react-native-webrtc-call";
const MyComponent = () => {
const {
state,
initiateCall,
acceptCall,
rejectCall,
endCall,
toggleMute,
toggleVideo,
toggleSpeaker,
} = useWebRTC();
// Access state
const { isCallActive, caller, callee } = state;
// Use actions
const handleCall = () => {
initiateCall(callee, callId);
};
};
interface CallState {
isCallActive: boolean;
isIncomingCall: boolean;
isOutgoingCall: boolean;
callId: string | null;
callType: "p2p" | "group"; // Call type
localStream: any;
remoteStream: any; // For P2P calls
remoteParticipants: RemoteParticipant[]; // For group calls
isMuted: boolean;
isVideoEnabled: boolean;
isSpeakerEnabled: boolean;
callStartTime: number | null;
callDuration: number;
caller: CallParticipant | null;
callee: CallParticipant | null;
participants: CallParticipant[]; // All participants (for group calls)
connectionState: "connecting" | "connected" | "disconnected" | "failed";
isCallMinimized: boolean;
incomingWantsVideo: boolean | null;
}
Your signaling server should handle the following Socket.io events:
register - Register user with signaling server
{
userId: string;
userType: string;
token?: string;
}
signaling-message - Send signaling messages
{
type: "offer" | "answer" | "ice-candidate" | "call-request" | "call-accept" | "call-reject" | "call-end";
data?: any;
callId: string;
to: string;
}
registered - Registration confirmation with ICE servers
{
success: boolean;
iceServers: RTCIceServer[];
iceProvider?: string;
}
signaling-message - Receive signaling messages
The package supports both STUN and TURN servers. TURN servers are required for calls to work across different networks (NAT traversal).
You can provide ICE servers in two ways:
turnEndpoint in the config that returns ICE serversExample TURN endpoint response:
{
"iceServers": [
{
"urls": "turn:your-turn-server.com:3478",
"username": "username",
"credential": "password"
}
]
}
You can test your signaling server setup using the following methods:
Run the included test script to verify all endpoints:
npm run test:server
# or with custom URL
node test/signaling-server-test.js http://localhost:3001
This will test:
/health)/ice)/users)Test if your server is running:
curl http://localhost:3001/health
Expected response:
{
"status": "ok",
"connectedUsers": 0,
"activeRooms": 0,
"timestamp": "2024-01-01T00:00:00.000Z",
"iceProvider": "static-stun"
}
Test ICE server configuration:
curl http://localhost:3001/ice
Expected response:
{
"iceServers": [
{
"urls": ["stun:global.stun.twilio.com:3478"]
}
],
"ttl": 3600,
"provider": "static-stun",
"fetchedAt": "2024-01-01T00:00:00.000Z",
"expiresAt": "2024-01-01T01:00:00.000Z"
}
Check currently connected users:
curl http://localhost:3001/users
Add this test function to verify your signaling server connection:
import { WebRTCService } from "react-native-webrtc-call";
async function testSignalingServer() {
const testConfig = {
signalingServerUrl: "http://localhost:3001", // Your server URL
getUserProfile: () => ({
_id: "test-user",
firstName: "Test",
lastName: "User",
userType: "user",
}),
onError: (error) => {
console.error("Test Error:", error);
},
onCallStateChange: (state) => {
console.log("Test State:", state);
},
};
const service = new WebRTCService(testConfig);
try {
// Register user (this will connect to signaling server)
await service.registerUser();
console.log("✅ Successfully connected to signaling server");
// Check connection state
const state = service.getState();
console.log("Connection state:", state);
// Cleanup
service.cleanup();
} catch (error) {
console.error("❌ Failed to connect to signaling server:", error);
}
}
// Call the test function
testSignalingServer();
For testing on physical devices or emulators:
http://localhost:3001 if testing on the same machinehttp://10.0.2.2:3001 (Android emulator's localhost)http://192.168.1.100:3001)Find your local IP:
# macOS/Linux
ifconfig | grep "inet " | grep -v 127.0.0.1
# Windows
ipconfig
curl http://localhost:3001/healthsignalingServerUrl is correctly configured
http:// or https:// (not ws:// or wss://)http://10.0.2.2:3001/ice endpoint returns valid ICE serversuseWebRTC hookConnection refused errors
node server.jsWebSocket connection fails
http:// or https:// in signalingServerUrlRegistration fails
getUserProfile() returns valid user datauseWebRTC hookMIT
Contributions are welcome! Please feel free to submit a Pull Request.
FAQs
A complete WebRTC calling solution for React Native with UI components, Context API integration, and signaling support
We found that react-native-webrtc-call demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.