@sawport/peers-caller 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +782 -0
- package/dist/core/BaseStore.d.ts +13 -0
- package/dist/core/BaseStore.d.ts.map +1 -0
- package/dist/core/CallMediaStream.d.ts +101 -0
- package/dist/core/CallMediaStream.d.ts.map +1 -0
- package/dist/core/CallParticipant.d.ts +122 -0
- package/dist/core/CallParticipant.d.ts.map +1 -0
- package/dist/core/CallPeerConnection.d.ts +92 -0
- package/dist/core/CallPeerConnection.d.ts.map +1 -0
- package/dist/core/CallRecorder.d.ts +72 -0
- package/dist/core/CallRecorder.d.ts.map +1 -0
- package/dist/core/CallSocket.d.ts +131 -0
- package/dist/core/CallSocket.d.ts.map +1 -0
- package/dist/core/PeersCaller.d.ts +155 -0
- package/dist/core/PeersCaller.d.ts.map +1 -0
- package/dist/events/CallEventEmitter.d.ts +2 -0
- package/dist/events/CallEventEmitter.d.ts.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +2 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +90 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/peers-caller.es.js +7983 -0
- package/dist/peers-caller.es.js.map +1 -0
- package/dist/peers-caller.umd.js +22 -0
- package/dist/peers-caller.umd.js.map +1 -0
- package/dist/polyfills.d.ts +5 -0
- package/dist/polyfills.d.ts.map +1 -0
- package/dist/store/index.d.ts +57 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/test-polyfills.d.ts +2 -0
- package/dist/test-polyfills.d.ts.map +1 -0
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-utils.d.ts +2 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/tester/App.d.ts +3 -0
- package/dist/tester/App.d.ts.map +1 -0
- package/dist/tester/components/ConfigPanel.d.ts +17 -0
- package/dist/tester/components/ConfigPanel.d.ts.map +1 -0
- package/dist/tester/components/ConnectionStatus.d.ts +21 -0
- package/dist/tester/components/ConnectionStatus.d.ts.map +1 -0
- package/dist/tester/components/ControlPanel.d.ts +23 -0
- package/dist/tester/components/ControlPanel.d.ts.map +1 -0
- package/dist/tester/components/DebugConsole.d.ts +27 -0
- package/dist/tester/components/DebugConsole.d.ts.map +1 -0
- package/dist/tester/components/ParticipantList.d.ts +10 -0
- package/dist/tester/components/ParticipantList.d.ts.map +1 -0
- package/dist/tester/components/VideoGrid.d.ts +10 -0
- package/dist/tester/components/VideoGrid.d.ts.map +1 -0
- package/dist/tester/hooks/useTester.d.ts +54 -0
- package/dist/tester/hooks/useTester.d.ts.map +1 -0
- package/dist/tester/main.d.ts +3 -0
- package/dist/tester/main.d.ts.map +1 -0
- package/dist/types/index.d.ts +262 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +50 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# ๐ฅ PeersCaller
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/@sawport%2Fpeers-caller)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
A modern, TypeScript-first WebRTC library for multi-peer mesh video calls supporting up to 4 participants. Built with developer experience in mind.
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## โจ Features
|
|
16
|
+
|
|
17
|
+
- ๐ฅ **WebRTC-based P2P video calls** - Direct peer-to-peer communication
|
|
18
|
+
- ๏ฟฝ๏ธ **Mesh architecture** - Efficient network topology for up to 4 participants
|
|
19
|
+
- ๏ฟฝ **TypeScript-first** - Full type safety and excellent IntelliSense
|
|
20
|
+
- โก **Vite-powered** - Lightning-fast development and builds
|
|
21
|
+
- ๐ฏ **Zustand state management** - Predictable and reactive state
|
|
22
|
+
- ๐๏ธ **Media controls** - Audio/video toggle, screen sharing
|
|
23
|
+
- ๐น **Call recording** - Built-in recording capabilities
|
|
24
|
+
- ๐งช **Well-tested** - Comprehensive test suite with Vitest
|
|
25
|
+
- ๐จ **React hooks** - Ready-to-use React integration
|
|
26
|
+
- ๐ก **Real-time signaling** - WebSocket-based call coordination
|
|
27
|
+
|
|
28
|
+
## ๐ฆ Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @sawport/peers-caller
|
|
32
|
+
# or
|
|
33
|
+
yarn add @sawport/peers-caller
|
|
34
|
+
# or
|
|
35
|
+
pnpm add @sawport/peers-caller
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## ๐ Quick Start
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { PeersCaller } from '@sawport/peers-caller';
|
|
44
|
+
|
|
45
|
+
// Initialize the caller
|
|
46
|
+
const peersCaller = new PeersCaller({
|
|
47
|
+
conversationId: 'unique-conversation-id',
|
|
48
|
+
userId: 'current-user-id',
|
|
49
|
+
token: 'jwt-auth-token',
|
|
50
|
+
socketUrl: 'https://your-signaling-server.com',
|
|
51
|
+
maxParticipants: 4,
|
|
52
|
+
mediaConfig: {
|
|
53
|
+
video: true,
|
|
54
|
+
audio: true
|
|
55
|
+
}
|
|
56
|
+
}, {
|
|
57
|
+
onParticipantJoined: (participant) => console.log('User joined:', participant.userId),
|
|
58
|
+
onParticipantLeft: (userId) => console.log('User left:', userId),
|
|
59
|
+
onStreamReceived: (userId, stream) => {
|
|
60
|
+
// Attach stream to video element
|
|
61
|
+
const videoElement = document.getElementById(`video-${userId}`);
|
|
62
|
+
if (videoElement) videoElement.srcObject = stream;
|
|
63
|
+
},
|
|
64
|
+
onError: (error, message) => console.error('Call error:', error, message)
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Start or join a call
|
|
68
|
+
async function startCall() {
|
|
69
|
+
try {
|
|
70
|
+
await peersCaller.initialize();
|
|
71
|
+
await peersCaller.startCall();
|
|
72
|
+
console.log('Call started successfully!');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to start call:', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function joinCall() {
|
|
79
|
+
try {
|
|
80
|
+
await peersCaller.initialize();
|
|
81
|
+
await peersCaller.joinCall();
|
|
82
|
+
console.log('Joined call successfully!');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Failed to join call:', error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### React Integration
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useVideoCall } from '@sawport/peers-caller';
|
|
93
|
+
|
|
94
|
+
function VideoCallComponent() {
|
|
95
|
+
const {
|
|
96
|
+
startCall,
|
|
97
|
+
joinCall,
|
|
98
|
+
endCall,
|
|
99
|
+
toggleAudio,
|
|
100
|
+
toggleVideo,
|
|
101
|
+
startScreenShare,
|
|
102
|
+
stopScreenShare,
|
|
103
|
+
participants,
|
|
104
|
+
localParticipant,
|
|
105
|
+
isConnected,
|
|
106
|
+
error
|
|
107
|
+
} = useVideoCall({
|
|
108
|
+
conversationId: 'conversation-123',
|
|
109
|
+
userId: 'user-456',
|
|
110
|
+
token: 'your-jwt-token',
|
|
111
|
+
socketUrl: 'https://your-server.com',
|
|
112
|
+
callbacks: {
|
|
113
|
+
onStreamReceived: (userId, stream) => {
|
|
114
|
+
// Handle received video streams
|
|
115
|
+
console.log(`Received stream from ${userId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="video-call">
|
|
122
|
+
<div className="controls">
|
|
123
|
+
<button onClick={() => startCall()}>Start Call</button>
|
|
124
|
+
<button onClick={() => joinCall()}>Join Call</button>
|
|
125
|
+
<button onClick={() => endCall()}>End Call</button>
|
|
126
|
+
<button onClick={() => toggleAudio(!localParticipant?.audioOn)}>
|
|
127
|
+
{localParticipant?.audioOn ? 'Mute' : 'Unmute'}
|
|
128
|
+
</button>
|
|
129
|
+
<button onClick={() => toggleVideo(!localParticipant?.videoOn)}>
|
|
130
|
+
{localParticipant?.videoOn ? 'Stop Video' : 'Start Video'}
|
|
131
|
+
</button>
|
|
132
|
+
<button onClick={() => startScreenShare()}>Share Screen</button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="participants">
|
|
136
|
+
{Object.values(participants).map(participant => (
|
|
137
|
+
<div key={participant.userId} className="participant">
|
|
138
|
+
<video
|
|
139
|
+
autoPlay
|
|
140
|
+
playsInline
|
|
141
|
+
ref={ref => {
|
|
142
|
+
if (ref && participant.stream) {
|
|
143
|
+
ref.srcObject = participant.stream;
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
<span>{participant.userId}</span>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{error && <div className="error">Error: {error}</div>}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## ๐๏ธ Backend Signaling Requirements
|
|
159
|
+
|
|
160
|
+
PeersCaller requires a WebSocket signaling server to coordinate calls between peers. The server must implement the following Socket.IO events:
|
|
161
|
+
|
|
162
|
+
### ๐ก Client-to-Server Events (Outgoing)
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Call Management
|
|
166
|
+
socket.emit('call.start', { conversationId: string });
|
|
167
|
+
socket.emit('call.join', { conversationId: string });
|
|
168
|
+
socket.emit('call.leave', { conversationId: string });
|
|
169
|
+
socket.emit('call.end', { conversationId: string, targetUserId?: string });
|
|
170
|
+
socket.emit('call.status', { conversationId: string });
|
|
171
|
+
|
|
172
|
+
// WebRTC Signaling
|
|
173
|
+
socket.emit('call.offer', {
|
|
174
|
+
to: string,
|
|
175
|
+
offer: RTCSessionDescriptionInit,
|
|
176
|
+
conversationId: string
|
|
177
|
+
});
|
|
178
|
+
socket.emit('call.answer', {
|
|
179
|
+
to: string,
|
|
180
|
+
answer: RTCSessionDescriptionInit,
|
|
181
|
+
conversationId: string
|
|
182
|
+
});
|
|
183
|
+
socket.emit('call.candidate', {
|
|
184
|
+
to: string,
|
|
185
|
+
candidate: RTCIceCandidateInit,
|
|
186
|
+
conversationId: string
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// State Updates
|
|
190
|
+
socket.emit('call.state', {
|
|
191
|
+
to?: string,
|
|
192
|
+
state: Partial<CallParticipant>,
|
|
193
|
+
conversationId: string
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Recording & Transcription
|
|
197
|
+
socket.emit('call.recording.start', { conversationId: string, recordingId: string });
|
|
198
|
+
socket.emit('call.recording.chunk', { conversationId: string, recordingId: string, chunk: Blob });
|
|
199
|
+
socket.emit('call.recording.end', { conversationId: string, recordingId: string });
|
|
200
|
+
socket.emit('call.transcript', { conversationId: string, transcript: string, timestamp: number });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### ๐จ Server-to-Client Events (Incoming)
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Call Management Responses
|
|
207
|
+
socket.on('call.started', (data: {
|
|
208
|
+
conversationId: string,
|
|
209
|
+
userId: string,
|
|
210
|
+
success: boolean,
|
|
211
|
+
participants: string[]
|
|
212
|
+
}) => {});
|
|
213
|
+
|
|
214
|
+
socket.on('call.participant.joined', (data: {
|
|
215
|
+
userId: string,
|
|
216
|
+
participants: string[],
|
|
217
|
+
conversationId: string
|
|
218
|
+
}) => {});
|
|
219
|
+
|
|
220
|
+
socket.on('call.participant.left', (data: {
|
|
221
|
+
userId: string,
|
|
222
|
+
participants: string[],
|
|
223
|
+
conversationId: string
|
|
224
|
+
}) => {});
|
|
225
|
+
|
|
226
|
+
socket.on('call.participants', (data: {
|
|
227
|
+
participants: string[],
|
|
228
|
+
conversationId: string
|
|
229
|
+
}) => {});
|
|
230
|
+
|
|
231
|
+
socket.on('call.left', (data: {
|
|
232
|
+
conversationId: string,
|
|
233
|
+
success: boolean
|
|
234
|
+
}) => {});
|
|
235
|
+
|
|
236
|
+
socket.on('call.ended', (data: {
|
|
237
|
+
conversationId: string,
|
|
238
|
+
endedBy: string,
|
|
239
|
+
reason: string
|
|
240
|
+
}) => {});
|
|
241
|
+
|
|
242
|
+
socket.on('call.error', (data: {
|
|
243
|
+
error: string,
|
|
244
|
+
message: string
|
|
245
|
+
}) => {});
|
|
246
|
+
|
|
247
|
+
// WebRTC Signaling Forwarding
|
|
248
|
+
socket.on('call.offer', (data: {
|
|
249
|
+
from: string,
|
|
250
|
+
offer: RTCSessionDescriptionInit,
|
|
251
|
+
conversationId: string
|
|
252
|
+
}) => {});
|
|
253
|
+
|
|
254
|
+
socket.on('call.answer', (data: {
|
|
255
|
+
from: string,
|
|
256
|
+
answer: RTCSessionDescriptionInit,
|
|
257
|
+
conversationId: string
|
|
258
|
+
}) => {});
|
|
259
|
+
|
|
260
|
+
socket.on('call.candidate', (data: {
|
|
261
|
+
from: string,
|
|
262
|
+
candidate: RTCIceCandidateInit,
|
|
263
|
+
conversationId: string
|
|
264
|
+
}) => {});
|
|
265
|
+
|
|
266
|
+
socket.on('call.state', (data: {
|
|
267
|
+
from: string,
|
|
268
|
+
state: Partial<CallParticipant>,
|
|
269
|
+
conversationId: string
|
|
270
|
+
}) => {});
|
|
271
|
+
|
|
272
|
+
// Call Status Updates
|
|
273
|
+
socket.on('call.status.changed', (data: {
|
|
274
|
+
conversationId: string,
|
|
275
|
+
hasActiveCall: boolean,
|
|
276
|
+
participantCount: number,
|
|
277
|
+
maxParticipants: number,
|
|
278
|
+
participants: string[],
|
|
279
|
+
startedAt: Date | null,
|
|
280
|
+
canJoin: boolean,
|
|
281
|
+
status: "no_call" | "active" | "full" | "ending"
|
|
282
|
+
}) => {});
|
|
283
|
+
|
|
284
|
+
// Recording Events
|
|
285
|
+
socket.on('call.recording.start', (data: { recordingId: string, conversationId: string }) => {});
|
|
286
|
+
socket.on('call.recording.chunk.received', (data: { recordingId: string, conversationId: string, chunkSize: number, timestamp: number }) => {});
|
|
287
|
+
socket.on('call.recording.end', (data: { recordingId: string, conversationId: string }) => {});
|
|
288
|
+
|
|
289
|
+
// Transcription Events
|
|
290
|
+
socket.on('call.transcript', (data: { userId: string, transcript: string, timestamp: number, conversationId: string }) => {});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### ๐ Authentication
|
|
294
|
+
|
|
295
|
+
The signaling server should authenticate connections using the provided JWT token:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Client connection with auth
|
|
299
|
+
io(serverUrl, {
|
|
300
|
+
path: '/apis/video-call',
|
|
301
|
+
auth: {
|
|
302
|
+
token: 'your-jwt-token'
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### ๐ Server Implementation Requirements
|
|
308
|
+
|
|
309
|
+
1. **Room Management**: Track participants in conversation rooms
|
|
310
|
+
2. **Message Forwarding**: Route WebRTC signaling between specific participants
|
|
311
|
+
3. **Participant Limits**: Enforce maximum participant limits (default: 4)
|
|
312
|
+
4. **Authentication**: Validate JWT tokens and extract user information
|
|
313
|
+
5. **Error Handling**: Provide meaningful error messages and codes
|
|
314
|
+
6. **Graceful Cleanup**: Handle disconnections and cleanup resources
|
|
315
|
+
|
|
316
|
+
### ๐ Example Server Setup (Node.js + Socket.IO)
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { Server } from 'socket.io';
|
|
320
|
+
import jwt from 'jsonwebtoken';
|
|
321
|
+
|
|
322
|
+
const io = new Server(server, {
|
|
323
|
+
path: '/apis/video-call',
|
|
324
|
+
cors: { origin: "*" }
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Authentication middleware
|
|
328
|
+
io.use((socket, next) => {
|
|
329
|
+
const token = socket.handshake.auth.token;
|
|
330
|
+
try {
|
|
331
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
332
|
+
socket.userId = decoded.userId;
|
|
333
|
+
next();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
next(new Error('Authentication failed'));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
io.on('connection', (socket) => {
|
|
340
|
+
console.log(`User ${socket.userId} connected`);
|
|
341
|
+
|
|
342
|
+
// Handle call start
|
|
343
|
+
socket.on('call.start', async ({ conversationId }) => {
|
|
344
|
+
try {
|
|
345
|
+
// Join room
|
|
346
|
+
await socket.join(conversationId);
|
|
347
|
+
|
|
348
|
+
// Get existing participants
|
|
349
|
+
const room = io.sockets.adapter.rooms.get(conversationId);
|
|
350
|
+
const participants = Array.from(room || []);
|
|
351
|
+
|
|
352
|
+
// Emit success response
|
|
353
|
+
socket.emit('call.started', {
|
|
354
|
+
conversationId,
|
|
355
|
+
userId: socket.userId,
|
|
356
|
+
success: true,
|
|
357
|
+
participants: participants.map(id => io.sockets.sockets.get(id)?.userId).filter(Boolean)
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Notify other participants
|
|
361
|
+
socket.to(conversationId).emit('call.participant.joined', {
|
|
362
|
+
userId: socket.userId,
|
|
363
|
+
participants: participants.map(id => io.sockets.sockets.get(id)?.userId).filter(Boolean),
|
|
364
|
+
conversationId
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
socket.emit('call.error', { error: 'CALL_START_FAILED', message: error.message });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Handle WebRTC signaling
|
|
372
|
+
socket.on('call.offer', ({ to, offer, conversationId }) => {
|
|
373
|
+
const targetSocket = Array.from(io.sockets.sockets.values())
|
|
374
|
+
.find(s => s.userId === to);
|
|
375
|
+
|
|
376
|
+
if (targetSocket) {
|
|
377
|
+
targetSocket.emit('call.offer', {
|
|
378
|
+
from: socket.userId,
|
|
379
|
+
offer,
|
|
380
|
+
conversationId
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Handle disconnection
|
|
386
|
+
socket.on('disconnect', () => {
|
|
387
|
+
// Notify rooms about participant leaving
|
|
388
|
+
socket.rooms.forEach(room => {
|
|
389
|
+
if (room !== socket.id) {
|
|
390
|
+
socket.to(room).emit('call.participant.left', {
|
|
391
|
+
userId: socket.userId,
|
|
392
|
+
conversationId: room
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## ๐ API Reference
|
|
401
|
+
|
|
402
|
+
### PeersCaller Class
|
|
403
|
+
|
|
404
|
+
The main orchestrator class for managing video calls.
|
|
405
|
+
|
|
406
|
+
#### Constructor
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
new PeersCaller(config: PeersCallerConfig, callbacks?: PeersCallerCallbacks)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Parameters:**
|
|
413
|
+
- `config`: Configuration object for the caller
|
|
414
|
+
- `callbacks`: Optional event callbacks
|
|
415
|
+
|
|
416
|
+
#### Methods
|
|
417
|
+
|
|
418
|
+
##### `initialize(): Promise<void>`
|
|
419
|
+
Initialize the PeersCaller and establish WebSocket connection.
|
|
420
|
+
|
|
421
|
+
##### `startCall(mediaConfig?: MediaStreamConfig): Promise<void>`
|
|
422
|
+
Start a new video call.
|
|
423
|
+
|
|
424
|
+
##### `joinCall(mediaConfig?: MediaStreamConfig): Promise<void>`
|
|
425
|
+
Join an existing video call.
|
|
426
|
+
|
|
427
|
+
##### `endCall(): Promise<void>`
|
|
428
|
+
End the call for all participants.
|
|
429
|
+
|
|
430
|
+
##### `leaveCall(): Promise<void>`
|
|
431
|
+
Leave the call gracefully.
|
|
432
|
+
|
|
433
|
+
##### `toggleAudio(enabled: boolean): void`
|
|
434
|
+
Enable or disable local audio.
|
|
435
|
+
|
|
436
|
+
##### `toggleVideo(enabled: boolean): void`
|
|
437
|
+
Enable or disable local video.
|
|
438
|
+
|
|
439
|
+
##### `startScreenShare(): Promise<void>`
|
|
440
|
+
Start sharing screen.
|
|
441
|
+
|
|
442
|
+
##### `stopScreenShare(): Promise<void>`
|
|
443
|
+
Stop sharing screen.
|
|
444
|
+
|
|
445
|
+
##### `startRecording(recordingData: RecordingData, config?: RecordingConfig): Promise<void>`
|
|
446
|
+
Start recording the call.
|
|
447
|
+
|
|
448
|
+
##### `stopRecording(): Promise<void>`
|
|
449
|
+
Stop recording the call.
|
|
450
|
+
|
|
451
|
+
##### `checkCallStatus(): Promise<CallStatusResponse>`
|
|
452
|
+
Check the current status of the call.
|
|
453
|
+
|
|
454
|
+
##### `cleanup(): void`
|
|
455
|
+
Clean up all resources and disconnect.
|
|
456
|
+
|
|
457
|
+
### Configuration Types
|
|
458
|
+
|
|
459
|
+
#### `PeersCallerConfig`
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
interface PeersCallerConfig {
|
|
463
|
+
conversationId: string; // Unique conversation identifier
|
|
464
|
+
userId: string; // Current user's unique identifier
|
|
465
|
+
token: string; // JWT authentication token
|
|
466
|
+
socketUrl: string; // WebSocket server URL
|
|
467
|
+
socketPath?: string; // Socket.IO path (default: '/apis/video-call')
|
|
468
|
+
iceServers?: RTCIceServer[]; // STUN/TURN servers
|
|
469
|
+
mediaConfig?: MediaStreamConfig; // Default media configuration
|
|
470
|
+
maxParticipants?: number; // Maximum participants (default: 4)
|
|
471
|
+
debug?: boolean; // Enable debug logging
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### `MediaStreamConfig`
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
interface MediaStreamConfig {
|
|
479
|
+
video: boolean | MediaTrackConstraints;
|
|
480
|
+
audio: boolean | MediaTrackConstraints;
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### `PeersCallerCallbacks`
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
interface PeersCallerCallbacks {
|
|
488
|
+
onCallStarted?: (data: { conversationId: string; success: boolean; participants: string[] }) => void;
|
|
489
|
+
onCallEnded?: (data: { conversationId: string; endedBy: string; reason: string }) => void;
|
|
490
|
+
onParticipantJoined?: (participant: CallParticipant) => void;
|
|
491
|
+
onParticipantLeft?: (userId: string) => void;
|
|
492
|
+
onParticipantStateChanged?: (userId: string, state: Partial<CallParticipant>) => void;
|
|
493
|
+
onStreamReceived?: (userId: string, stream: MediaStream) => void;
|
|
494
|
+
onCallStateChanged?: (state: "idle" | "connecting" | "connected" | "disconnecting" | "failed") => void;
|
|
495
|
+
onCallStatusChanged?: (statusInfo: CallStatusResponse) => void;
|
|
496
|
+
onRecordingStateChanged?: (isRecording: boolean) => void;
|
|
497
|
+
onError?: (error: PeersCallerError, message: string) => void;
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### React Hooks
|
|
502
|
+
|
|
503
|
+
#### `useVideoCall(options: UseVideoCallOptions)`
|
|
504
|
+
|
|
505
|
+
A comprehensive React hook for video call functionality.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const {
|
|
509
|
+
// Core methods
|
|
510
|
+
initialize,
|
|
511
|
+
startCall,
|
|
512
|
+
joinCall,
|
|
513
|
+
endCall,
|
|
514
|
+
|
|
515
|
+
// Media controls
|
|
516
|
+
toggleAudio,
|
|
517
|
+
toggleVideo,
|
|
518
|
+
startScreenShare,
|
|
519
|
+
stopScreenShare,
|
|
520
|
+
|
|
521
|
+
// Recording
|
|
522
|
+
startRecording,
|
|
523
|
+
stopRecording,
|
|
524
|
+
|
|
525
|
+
// State
|
|
526
|
+
callState,
|
|
527
|
+
participants,
|
|
528
|
+
localParticipant,
|
|
529
|
+
isConnected,
|
|
530
|
+
isRecording,
|
|
531
|
+
error,
|
|
532
|
+
|
|
533
|
+
// Utility
|
|
534
|
+
cleanup,
|
|
535
|
+
peersCaller
|
|
536
|
+
} = useVideoCall(options);
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Error Types
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
type PeersCallerError =
|
|
543
|
+
| "MEDIA_ACCESS_DENIED"
|
|
544
|
+
| "PEER_CONNECTION_FAILED"
|
|
545
|
+
| "SIGNALING_ERROR"
|
|
546
|
+
| "RECORDING_FAILED"
|
|
547
|
+
| "TRANSCRIPTION_FAILED"
|
|
548
|
+
| "CALL_LIMIT_EXCEEDED"
|
|
549
|
+
| "INVALID_CONVERSATION_ID"
|
|
550
|
+
| "NETWORK_ERROR"
|
|
551
|
+
| "UNKNOWN_ERROR";
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## ๐ง Advanced Usage
|
|
555
|
+
|
|
556
|
+
### Custom Media Constraints
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
const peersCaller = new PeersCaller({
|
|
560
|
+
// ... other config
|
|
561
|
+
mediaConfig: {
|
|
562
|
+
video: {
|
|
563
|
+
width: { ideal: 1280 },
|
|
564
|
+
height: { ideal: 720 },
|
|
565
|
+
frameRate: { ideal: 30 }
|
|
566
|
+
},
|
|
567
|
+
audio: {
|
|
568
|
+
echoCancellation: true,
|
|
569
|
+
noiseSuppression: true,
|
|
570
|
+
autoGainControl: true
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Custom ICE Servers
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
const peersCaller = new PeersCaller({
|
|
580
|
+
// ... other config
|
|
581
|
+
iceServers: [
|
|
582
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
583
|
+
{
|
|
584
|
+
urls: 'turn:your-turn-server.com:3478',
|
|
585
|
+
username: 'username',
|
|
586
|
+
credential: 'password'
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Recording with Custom Configuration
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
await peersCaller.startRecording(
|
|
596
|
+
{
|
|
597
|
+
id: 'recording-123',
|
|
598
|
+
filename: 'meeting-recording.webm',
|
|
599
|
+
conversationId: 'conversation-456',
|
|
600
|
+
startTime: Date.now()
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
mimeType: 'video/webm;codecs=vp9',
|
|
604
|
+
videoBitsPerSecond: 2500000,
|
|
605
|
+
audioBitsPerSecond: 128000,
|
|
606
|
+
interval: 1000 // 1 second chunks
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### State Management Integration
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import { useCallStore } from '@sawport/peers-caller';
|
|
615
|
+
|
|
616
|
+
function CallStatus() {
|
|
617
|
+
const {
|
|
618
|
+
isCalling,
|
|
619
|
+
callStatus,
|
|
620
|
+
participants,
|
|
621
|
+
error
|
|
622
|
+
} = useCallStore();
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<div>
|
|
626
|
+
<p>Status: {callStatus}</p>
|
|
627
|
+
<p>Participants: {Object.keys(participants).length}</p>
|
|
628
|
+
{error && <p>Error: {error}</p>}
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## ๐งช Development
|
|
635
|
+
|
|
636
|
+
### Prerequisites
|
|
637
|
+
|
|
638
|
+
- Node.js 18+ (recommended: 20+)
|
|
639
|
+
- Yarn (using Yarn Berry/v3+)
|
|
640
|
+
|
|
641
|
+
### Setup
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
# Clone the repository
|
|
645
|
+
git clone https://github.com/sawport/peers-caller.git
|
|
646
|
+
cd peers-caller
|
|
647
|
+
|
|
648
|
+
# Install dependencies
|
|
649
|
+
yarn install
|
|
650
|
+
|
|
651
|
+
# Start development server with hot reload
|
|
652
|
+
yarn dev
|
|
653
|
+
|
|
654
|
+
# Build the library
|
|
655
|
+
yarn build
|
|
656
|
+
|
|
657
|
+
# Build TypeScript declarations only
|
|
658
|
+
yarn build:types
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Development Scripts
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
# Development
|
|
665
|
+
yarn dev # Start Vite dev server with hot reload
|
|
666
|
+
yarn build # Build production bundle
|
|
667
|
+
yarn preview # Preview production build
|
|
668
|
+
|
|
669
|
+
# Testing
|
|
670
|
+
yarn test # Run tests once
|
|
671
|
+
yarn test:watch # Run tests in watch mode
|
|
672
|
+
yarn test:ui # Open Vitest UI
|
|
673
|
+
yarn test:coverage # Generate coverage report
|
|
674
|
+
|
|
675
|
+
# Type checking
|
|
676
|
+
yarn type-check # Check TypeScript types without building
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Testing
|
|
680
|
+
|
|
681
|
+
This project uses **Vitest** for testing with comprehensive coverage reporting and WebRTC API mocking.
|
|
682
|
+
|
|
683
|
+
This project uses Vitest for testing with comprehensive coverage reporting.
|
|
684
|
+
|
|
685
|
+
#### Running Tests
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
# Run tests once
|
|
689
|
+
yarn test
|
|
690
|
+
|
|
691
|
+
# Run tests in watch mode (for development)
|
|
692
|
+
yarn test:watch
|
|
693
|
+
|
|
694
|
+
# Run tests with coverage report
|
|
695
|
+
yarn test:coverage
|
|
696
|
+
|
|
697
|
+
# Open Vitest UI
|
|
698
|
+
yarn test:ui
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
#### Test Structure
|
|
702
|
+
|
|
703
|
+
- `__tests__/` - Integration and setup tests
|
|
704
|
+
- `src/**/*.test.ts` - Unit tests alongside source code
|
|
705
|
+
- `src/test-utils.ts` - Shared test utilities and mocks
|
|
706
|
+
|
|
707
|
+
#### Writing Tests
|
|
708
|
+
|
|
709
|
+
The test environment includes:
|
|
710
|
+
|
|
711
|
+
- **WebRTC API mocks** - RTCPeerConnection, MediaDevices, etc.
|
|
712
|
+
- **External library mocks** - simple-peer, socket.io-client
|
|
713
|
+
- **jsdom environment** - For DOM testing
|
|
714
|
+
- **TypeScript support** - Full type checking in tests
|
|
715
|
+
|
|
716
|
+
Example test:
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
import { describe, it, expect } from 'vitest';
|
|
720
|
+
import { generatePeerId } from '../utils';
|
|
721
|
+
|
|
722
|
+
describe('generatePeerId', () => {
|
|
723
|
+
it('should generate unique peer IDs', () => {
|
|
724
|
+
const id1 = generatePeerId();
|
|
725
|
+
const id2 = generatePeerId();
|
|
726
|
+
|
|
727
|
+
expect(id1).not.toBe(id2);
|
|
728
|
+
expect(id1).toMatch(/^peer_\d+_[a-z0-9]{1,9}$/);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
#### Coverage Thresholds
|
|
734
|
+
|
|
735
|
+
The project maintains high test coverage standards:
|
|
736
|
+
|
|
737
|
+
- **Branches**: 80%
|
|
738
|
+
- **Functions**: 80%
|
|
739
|
+
- **Lines**: 80%
|
|
740
|
+
- **Statements**: 80%
|
|
741
|
+
|
|
742
|
+
### CI/CD
|
|
743
|
+
|
|
744
|
+
GitHub Actions automatically:
|
|
745
|
+
|
|
746
|
+
- โ
Runs tests on Node.js 18.x, 20.x, 22.x
|
|
747
|
+
- โ
Checks TypeScript compilation
|
|
748
|
+
- โ
Generates coverage reports
|
|
749
|
+
- โ
Builds the library
|
|
750
|
+
- โ
Uploads coverage to Codecov (optional)
|
|
751
|
+
|
|
752
|
+
## API Reference
|
|
753
|
+
|
|
754
|
+
### Utilities
|
|
755
|
+
|
|
756
|
+
### Store
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
### Types
|
|
760
|
+
|
|
761
|
+
## Contributing
|
|
762
|
+
|
|
763
|
+
1. Fork the repository
|
|
764
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
765
|
+
3. Make your changes
|
|
766
|
+
4. Add tests for new functionality
|
|
767
|
+
5. Ensure tests pass (`yarn test`)
|
|
768
|
+
6. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
769
|
+
7. Push to the branch (`git push origin feature/amazing-feature`)
|
|
770
|
+
8. Open a Pull Request
|
|
771
|
+
|
|
772
|
+
### Development Guidelines
|
|
773
|
+
|
|
774
|
+
- Write tests for all new functionality
|
|
775
|
+
- Maintain TypeScript strict mode compliance
|
|
776
|
+
- Follow the existing code style
|
|
777
|
+
- Update documentation as needed
|
|
778
|
+
- Ensure CI passes before submitting PR
|
|
779
|
+
|
|
780
|
+
## License
|
|
781
|
+
|
|
782
|
+
MIT License - see LICENSE file for details.
|