@sawport/peers-caller
Version:
WebRTC multi-peer video call library with mesh architecture supporting up to 4 participants
908 lines (726 loc) โข 24.7 kB
Markdown
# ๐ฅ PeersCaller
<div align="center">
[](https://badge.fury.io/js/@sawport%2Fpeers-caller)
[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
A modern, TypeScript-first WebRTC library for multi-peer mesh video calls supporting up to 4 participants. Built with developer experience in mind.
</div>
## โจ Features
- ๐ฅ **WebRTC-based P2P video calls** - Direct peer-to-peer communication
- ๏ฟฝ๏ธ **Mesh architecture** - Efficient network topology for up to 4 participants
- ๏ฟฝ **TypeScript-first** - Full type safety and excellent IntelliSense
- โก **Vite-powered** - Lightning-fast development and builds
- ๐ฏ **Zustand state management** - Predictable and reactive state
- ๐๏ธ **Media controls** - Audio/video toggle, screen sharing
- ๐น **Call recording** - Built-in recording capabilities
- ๐งช **Well-tested** - Comprehensive test suite with Vitest
- ๐จ **React hooks** - Ready-to-use React integration
- ๐ก **Real-time signaling** - WebSocket-based call coordination
## ๐ฆ Installation
```bash
npm install @sawport/peers-caller
# or
yarn add @sawport/peers-caller
# or
pnpm add @sawport/peers-caller
```
## ๐ Quick Start
### Basic Usage
```typescript
import { PeersCaller } from '@sawport/peers-caller';
// Initialize the caller
const peersCaller = new PeersCaller({
conversationId: 'unique-conversation-id',
userId: 'current-user-id',
token: 'jwt-auth-token',
socketUrl: 'https://your-signaling-server.com',
maxParticipants: 4,
mediaConfig: {
video: true,
audio: true
}
}, {
onParticipantJoined: (participant) => console.log('User joined:', participant.userId),
onParticipantLeft: (userId) => console.log('User left:', userId),
onStreamReceived: (userId, stream) => {
// Attach stream to video element
const videoElement = document.getElementById(`video-${userId}`);
if (videoElement) videoElement.srcObject = stream;
},
onError: (error, message) => console.error('Call error:', error, message)
});
// Start or join a call
async function startCall() {
try {
await peersCaller.initialize();
await peersCaller.startCall();
console.log('Call started successfully!');
} catch (error) {
console.error('Failed to start call:', error);
}
}
async function joinCall() {
try {
await peersCaller.initialize();
await peersCaller.joinCall();
console.log('Joined call successfully!');
} catch (error) {
console.error('Failed to join call:', error);
}
}
```
### React Integration
```tsx
import { useVideoCall } from '@sawport/peers-caller';
function VideoCallComponent() {
const {
startCall,
joinCall,
endCall,
toggleAudio,
toggleVideo,
startScreenShare,
stopScreenShare,
participants,
localParticipant,
isConnected,
error
} = useVideoCall({
conversationId: 'conversation-123',
userId: 'user-456',
token: 'your-jwt-token',
socketUrl: 'https://your-server.com',
callbacks: {
onStreamReceived: (userId, stream) => {
// Handle received video streams
console.log(`Received stream from ${userId}`);
}
}
});
return (
<div className="video-call">
<div className="controls">
<button onClick={() => startCall()}>Start Call</button>
<button onClick={() => joinCall()}>Join Call</button>
<button onClick={() => endCall()}>End Call</button>
<button onClick={() => toggleAudio(!localParticipant?.audioOn)}>
{localParticipant?.audioOn ? 'Mute' : 'Unmute'}
</button>
<button onClick={() => toggleVideo(!localParticipant?.videoOn)}>
{localParticipant?.videoOn ? 'Stop Video' : 'Start Video'}
</button>
<button onClick={() => startScreenShare()}>Share Screen</button>
</div>
<div className="participants">
{Object.values(participants).map(participant => (
<div key={participant.userId} className="participant">
<video
autoPlay
playsInline
ref={ref => {
if (ref && participant.stream) {
ref.srcObject = participant.stream;
}
}}
/>
<span>{participant.userId}</span>
</div>
))}
</div>
{error && <div className="error">Error: {error}</div>}
</div>
);
}
```
## ๐๏ธ Backend Signaling Requirements
PeersCaller requires a WebSocket signaling server to coordinate calls between peers. The server must implement the following Socket.IO events:
### ๐ก Client-to-Server Events (Outgoing)
```typescript
// Call Management
socket.emit('call.start', { conversationId: string });
socket.emit('call.join', { conversationId: string });
socket.emit('call.leave', { conversationId: string });
socket.emit('call.end', { conversationId: string, targetUserId?: string });
socket.emit('call.status', { conversationId: string });
// WebRTC Signaling
socket.emit('call.offer', {
to: string,
offer: RTCSessionDescriptionInit,
conversationId: string
});
socket.emit('call.answer', {
to: string,
answer: RTCSessionDescriptionInit,
conversationId: string
});
socket.emit('call.candidate', {
to: string,
candidate: RTCIceCandidateInit,
conversationId: string
});
// State Updates
socket.emit('call.state', {
to?: string,
state: Partial<CallParticipant>,
conversationId: string
});
// Recording & Transcription
socket.emit('call.recording.start', { conversationId: string, recordingId: string });
socket.emit('call.recording.chunk', { conversationId: string, recordingId: string, chunk: Blob });
socket.emit('call.recording.end', { conversationId: string, recordingId: string });
socket.emit('call.transcript', { conversationId: string, transcript: string, timestamp: number });
```
### ๐จ Server-to-Client Events (Incoming)
```typescript
// Call Management Responses
socket.on('call.started', (data: {
conversationId: string,
userId: string,
success: boolean,
participants: string[]
}) => {});
socket.on('call.participant.joined', (data: {
userId: string,
participants: string[],
conversationId: string
}) => {});
socket.on('call.participant.left', (data: {
userId: string,
participants: string[],
conversationId: string
}) => {});
socket.on('call.participants', (data: {
participants: string[],
conversationId: string
}) => {});
socket.on('call.left', (data: {
conversationId: string,
success: boolean
}) => {});
socket.on('call.ended', (data: {
conversationId: string,
endedBy: string,
reason: string
}) => {});
socket.on('call.error', (data: {
error: string,
message: string
}) => {});
// WebRTC Signaling Forwarding
socket.on('call.offer', (data: {
from: string,
offer: RTCSessionDescriptionInit,
conversationId: string
}) => {});
socket.on('call.answer', (data: {
from: string,
answer: RTCSessionDescriptionInit,
conversationId: string
}) => {});
socket.on('call.candidate', (data: {
from: string,
candidate: RTCIceCandidateInit,
conversationId: string
}) => {});
socket.on('call.state', (data: {
from: string,
state: Partial<CallParticipant>,
conversationId: string
}) => {});
// Call Status Updates
socket.on('call.status.changed', (data: {
conversationId: string,
hasActiveCall: boolean,
participantCount: number,
maxParticipants: number,
participants: string[],
startedAt: Date | null,
canJoin: boolean,
status: "no_call" | "active" | "full" | "ending"
}) => {});
// Recording Events
socket.on('call.recording.start', (data: { recordingId: string, conversationId: string }) => {});
socket.on('call.recording.chunk.received', (data: { recordingId: string, conversationId: string, chunkSize: number, timestamp: number }) => {});
socket.on('call.recording.end', (data: { recordingId: string, conversationId: string }) => {});
// Transcription Events
socket.on('call.transcript', (data: { userId: string, transcript: string, timestamp: number, conversationId: string }) => {});
```
### ๐ Authentication
The signaling server should authenticate connections using the provided JWT token:
```typescript
// Client connection with auth
io(serverUrl, {
path: '/apis/video-call',
auth: {
token: 'your-jwt-token'
}
});
```
### ๐ Server Implementation Requirements
1. **Room Management**: Track participants in conversation rooms
2. **Message Forwarding**: Route WebRTC signaling between specific participants
3. **Participant Limits**: Enforce maximum participant limits (default: 4)
4. **Authentication**: Validate JWT tokens and extract user information
5. **Error Handling**: Provide meaningful error messages and codes
6. **Graceful Cleanup**: Handle disconnections and cleanup resources
### ๐ Example Server Setup (Node.js + Socket.IO)
```typescript
import { Server } from 'socket.io';
import jwt from 'jsonwebtoken';
const io = new Server(server, {
path: '/apis/video-call',
cors: { origin: "*" }
});
// Authentication middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.userId;
next();
} catch (err) {
next(new Error('Authentication failed'));
}
});
io.on('connection', (socket) => {
console.log(`User ${socket.userId} connected`);
// Handle call start
socket.on('call.start', async ({ conversationId }) => {
try {
// Join room
await socket.join(conversationId);
// Get existing participants
const room = io.sockets.adapter.rooms.get(conversationId);
const participants = Array.from(room || []);
// Emit success response
socket.emit('call.started', {
conversationId,
userId: socket.userId,
success: true,
participants: participants.map(id => io.sockets.sockets.get(id)?.userId).filter(Boolean)
});
// Notify other participants
socket.to(conversationId).emit('call.participant.joined', {
userId: socket.userId,
participants: participants.map(id => io.sockets.sockets.get(id)?.userId).filter(Boolean),
conversationId
});
} catch (error) {
socket.emit('call.error', { error: 'CALL_START_FAILED', message: error.message });
}
});
// Handle WebRTC signaling
socket.on('call.offer', ({ to, offer, conversationId }) => {
const targetSocket = Array.from(io.sockets.sockets.values())
.find(s => s.userId === to);
if (targetSocket) {
targetSocket.emit('call.offer', {
from: socket.userId,
offer,
conversationId
});
}
});
// Handle disconnection
socket.on('disconnect', () => {
// Notify rooms about participant leaving
socket.rooms.forEach(room => {
if (room !== socket.id) {
socket.to(room).emit('call.participant.left', {
userId: socket.userId,
conversationId: room
});
}
});
});
});
```
## ๐ API Reference
### PeersCaller Class
The main orchestrator class for managing video calls.
#### Constructor
```typescript
new PeersCaller(config: PeersCallerConfig, callbacks?: PeersCallerCallbacks)
```
**Parameters:**
- `config`: Configuration object for the caller
- `callbacks`: Optional event callbacks
#### Methods
##### `initialize(): Promise<void>`
Initialize the PeersCaller and establish WebSocket connection.
##### `startCall(mediaConfig?: MediaStreamConfig): Promise<void>`
Start a new video call.
##### `joinCall(mediaConfig?: MediaStreamConfig): Promise<void>`
Join an existing video call.
##### `endCall(): Promise<void>`
End the call for all participants.
##### `leaveCall(): Promise<void>`
Leave the call gracefully.
##### `toggleAudio(enabled: boolean): void`
Enable or disable local audio.
##### `toggleVideo(enabled: boolean): void`
Enable or disable local video.
##### `startScreenShare(): Promise<void>`
Start sharing screen.
##### `stopScreenShare(): Promise<void>`
Stop sharing screen.
##### `startRecording(recordingData: RecordingData, config?: RecordingConfig): Promise<void>`
Start recording the call.
##### `stopRecording(): Promise<void>`
Stop recording the call.
##### `checkCallStatus(): Promise<CallStatusResponse>`
Check the current status of the call.
##### `cleanup(): void`
Clean up all resources and disconnect.
### Configuration Types
#### `PeersCallerConfig`
```typescript
interface PeersCallerConfig {
conversationId: string; // Unique conversation identifier
userId: string; // Current user's unique identifier
token: string; // JWT authentication token
socketUrl: string; // WebSocket server URL
socketPath?: string; // Socket.IO path (default: '/apis/video-call')
iceServers?: RTCIceServer[]; // STUN/TURN servers
mediaConfig?: MediaStreamConfig; // Default media configuration
maxParticipants?: number; // Maximum participants (default: 4)
debug?: boolean; // Enable debug logging
}
```
#### `MediaStreamConfig`
```typescript
interface MediaStreamConfig {
video: boolean | MediaTrackConstraints;
audio: boolean | MediaTrackConstraints;
}
```
#### `PeersCallerCallbacks`
```typescript
interface PeersCallerCallbacks {
onCallStarted?: (data: { conversationId: string; success: boolean; participants: string[] }) => void;
onCallEnded?: (data: { conversationId: string; endedBy: string; reason: string }) => void;
onParticipantJoined?: (participant: CallParticipant) => void;
onParticipantLeft?: (userId: string) => void;
onParticipantStateChanged?: (userId: string, state: Partial<CallParticipant>) => void;
onStreamReceived?: (userId: string, stream: MediaStream) => void;
onCallStateChanged?: (state: "idle" | "connecting" | "connected" | "disconnecting" | "failed") => void;
onCallStatusChanged?: (statusInfo: CallStatusResponse) => void;
onRecordingStateChanged?: (isRecording: boolean) => void;
onError?: (error: PeersCallerError, message: string) => void;
}
```
### React Hooks
#### `useVideoCall(options: UseVideoCallOptions)`
A comprehensive React hook for video call functionality.
```typescript
const {
// Core methods
initialize,
startCall,
joinCall,
endCall,
// Media controls
toggleAudio,
toggleVideo,
startScreenShare,
stopScreenShare,
// Recording
startRecording,
stopRecording,
// State
callState,
participants,
localParticipant,
isConnected,
isRecording,
error,
// Utility
cleanup,
peersCaller
} = useVideoCall(options);
```
### Error Types
```typescript
type PeersCallerError =
| "MEDIA_ACCESS_DENIED"
| "PEER_CONNECTION_FAILED"
| "SIGNALING_ERROR"
| "RECORDING_FAILED"
| "TRANSCRIPTION_FAILED"
| "CALL_LIMIT_EXCEEDED"
| "INVALID_CONVERSATION_ID"
| "NETWORK_ERROR"
| "UNKNOWN_ERROR";
```
## ๐ง Advanced Usage
### Custom Media Constraints
```typescript
const peersCaller = new PeersCaller({
// ... other config
mediaConfig: {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
}
});
```
### Custom ICE Servers
```typescript
const peersCaller = new PeersCaller({
// ... other config
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'username',
credential: 'password'
}
]
});
```
### Recording with Custom Configuration
```typescript
await peersCaller.startRecording(
{
id: 'recording-123',
filename: 'meeting-recording.webm',
conversationId: 'conversation-456',
startTime: Date.now()
},
{
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 2500000,
audioBitsPerSecond: 128000,
interval: 1000 // 1 second chunks
}
);
```
### State Management Integration
```typescript
import { useCallStore } from '@sawport/peers-caller';
function CallStatus() {
const {
isCalling,
callStatus,
participants,
error
} = useCallStore();
return (
<div>
<p>Status: {callStatus}</p>
<p>Participants: {Object.keys(participants).length}</p>
{error && <p>Error: {error}</p>}
</div>
);
}
```
## ๐งช Development
### Prerequisites
- Node.js 18+ (recommended: 20+)
- Yarn (using Yarn Berry/v3+)
### Setup
```bash
# Clone the repository
git clone https://github.com/sawport/peers-caller.git
cd peers-caller
# Install dependencies
yarn install
# Start development server with hot reload
yarn dev
# Build the library
yarn build
# Build TypeScript declarations only
yarn build:types
```
### Development Scripts
```bash
# Development
yarn dev # Start Vite dev server with hot reload
yarn build # Build production bundle
yarn preview # Preview production build
# Testing
yarn test # Run tests once
yarn test:watch # Run tests in watch mode
yarn test:ui # Open Vitest UI
yarn test:coverage # Generate coverage report
# Type checking
yarn type-check # Check TypeScript types without building
```
### Testing
This project uses **Vitest** for testing with comprehensive coverage reporting and WebRTC API mocking.
### Testing
This project uses **Vitest** for testing with comprehensive coverage reporting and WebRTC API mocking.
#### Test Environment
The test setup includes:
- **WebRTC API Mocks**: RTCPeerConnection, MediaDevices, getUserMedia
- **Socket.IO Mocking**: Complete WebSocket simulation
- **jsdom Environment**: DOM testing capabilities
- **TypeScript Support**: Full type checking in tests
- **Coverage Reporting**: Detailed coverage analysis
#### Running Tests
```bash
# Run all tests once
yarn test
# Watch mode for development
yarn test:watch
# Generate coverage report
yarn test:coverage
# Interactive test UI
yarn test:ui
```
#### Writing Tests
```typescript
import { describe, it, expect, vi } from 'vitest';
import { PeersCaller } from '../core/PeersCaller';
import { mockWebRTC } from '../test-utils';
describe('PeersCaller', () => {
beforeEach(() => {
mockWebRTC(); // Set up WebRTC mocks
});
it('should initialize successfully', async () => {
const peersCaller = new PeersCaller({
conversationId: 'test-123',
userId: 'user-456',
token: 'fake-token',
socketUrl: 'http://localhost:3000'
});
await expect(peersCaller.initialize()).resolves.not.toThrow();
expect(peersCaller.getCallState().callStatus).toBe('idle');
});
});
```
#### Coverage Thresholds
- **Branches**: 80%
- **Functions**: 80%
- **Lines**: 80%
- **Statements**: 80%
### Project Structure
```
src/
โโโ core/ # Core classes and logic
โ โโโ PeersCaller.ts # Main orchestrator
โ โโโ CallSocket.ts # WebSocket signaling
โ โโโ CallParticipant.ts # Participant management
โ โโโ CallRecorder.ts # Recording functionality
โ โโโ ...
โโโ store/ # Zustand state management
โ โโโ index.ts
โโโ hooks/ # React hooks
โ โโโ index.ts
โโโ types/ # TypeScript definitions
โ โโโ index.ts
โโโ utils/ # Utility functions
โ โโโ index.ts
โโโ test-utils.ts # Test utilities and mocks
โโโ index.ts # Main entry point
```
### Contributing Guidelines
1. **Fork & Clone**: Fork the repository and clone your fork
2. **Branch**: Create a feature branch (`git checkout -b feature/amazing-feature`)
3. **Develop**: Make your changes following the coding standards
4. **Test**: Write tests for new functionality and ensure all tests pass
5. **Type Safety**: Maintain TypeScript strict mode compliance
6. **Documentation**: Update documentation as needed
7. **Commit**: Use conventional commit messages
8. **PR**: Open a Pull Request with a clear description
### Code Style Guidelines
- **TypeScript Strict Mode**: All code must pass strict type checking
- **ESLint + Prettier**: Follow the established code style
- **Functional Programming**: Prefer pure functions and immutability
- **Error Handling**: Always handle errors gracefully
- **Documentation**: Document public APIs and complex logic
- **Testing**: Write tests for all new functionality
### Build & Distribution
The library is built using **Vite** and generates multiple output formats:
```bash
dist/
โโโ peers-caller.es.js # ES modules
โโโ peers-caller.umd.js # UMD bundle
โโโ index.d.ts # TypeScript declarations
โโโ style.css # Optional styles
```
### CI/CD Pipeline
GitHub Actions automatically:
- โ
**Tests** on Node.js 18.x, 20.x, 22.x
- โ
**Type Checking** with TypeScript
- โ
**Linting** with ESLint
- โ
**Coverage Reports** with Codecov
- โ
**Build Validation** for all platforms
- ๐ **Automated Publishing** to npm (on release)
## ๐ค Contributing
We welcome contributions! Here's how you can help:
### Areas for Contribution
- ๐ **Bug Fixes**: Report and fix issues
- โจ **Features**: Propose and implement new features
- ๐ **Documentation**: Improve docs and examples
- ๐งช **Testing**: Add more test cases and improve coverage
- ๐ง **Performance**: Optimize performance and bundle size
- ๐จ **UI/UX**: Improve React hooks and developer experience
### Getting Started
1. Check existing [issues](https://github.com/sawport/peers-caller/issues) and [pull requests](https://github.com/sawport/peers-caller/pulls)
2. Open an issue to discuss major changes
3. Follow the development setup instructions
4. Make your changes and add tests
5. Submit a pull request
### Commit Convention
We use [Conventional Commits](https://www.conventionalcommits.org/):
```bash
feat: add screen sharing support
fix: resolve peer connection race condition
docs: update API documentation
test: add integration tests for recording
refactor: simplify state management logic
```
## ๐ Roadmap
### Current Version (v0.x)
- โ
Basic peer-to-peer video calls
- โ
Mesh architecture (up to 4 participants)
- โ
Media controls (audio/video toggle)
- โ
Screen sharing
- โ
Call recording
- โ
React hooks integration
- โ
TypeScript support
### Planned Features (v1.0)
- ๐ **Improved Error Handling**: Better error recovery and user feedback
- ๐ **Call Analytics**: Bandwidth monitoring and quality metrics
- ๐ **Audio Processing**: Noise suppression and echo cancellation
- ๐ฑ **Mobile Optimization**: Better mobile device support
- ๐ **Internationalization**: Multi-language support
- ๐ **Plugin System**: Extensible architecture for custom features
### Future Considerations
- **SFU Mode**: Support for Selective Forwarding Unit architecture
- **Chat Integration**: Text messaging during calls
- **Whiteboard**: Collaborative drawing and annotation
- **Virtual Backgrounds**: AI-powered background replacement
- **Call Waiting**: Queue management for busy participants
## ๐ Security Considerations
### WebRTC Security
- **DTLS Encryption**: All media streams are encrypted end-to-end
- **SRTP**: Secure Real-time Transport Protocol for media
- **ICE Candidates**: Secure NAT traversal with STUN/TURN servers
- **Origin Validation**: Server-side origin checking for WebSocket connections
### Authentication
- **JWT Tokens**: Secure authentication with JSON Web Tokens
- **Token Expiration**: Implement proper token refresh mechanisms
- **User Validation**: Server-side user validation and authorization
### Best Practices
- **HTTPS Only**: Always use HTTPS in production
- **CORS Configuration**: Properly configure Cross-Origin Resource Sharing
- **Input Validation**: Validate all user inputs and signaling data
- **Rate Limiting**: Implement rate limiting on signaling server
- **Audit Logging**: Log security-relevant events
## ๐ License
MIT License - see [LICENSE](./LICENSE) file for details.
<div align="center">
**Built with โค๏ธ by the [Sawport](https://github.com/sawport) team**
[๐ Star on GitHub](https://github.com/sawport/peers-caller) โข [๐ Report Issues](https://github.com/sawport/peers-caller/issues) โข [๐ฌ Discussions](https://github.com/sawport/peers-caller/discussions)
</div>