# 🎥 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>
