UNPKG

@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
# ๐ŸŽฅ PeersCaller <div align="center"> [![npm version](https://badge.fury.io/js/@sawport%2Fpeers-caller.svg)](https://badge.fury.io/js/@sawport%2Fpeers-caller) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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>