@unboundcx/video-sdk-client 1.1.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/AudioMixer.js +235 -0
- package/README.md +400 -0
- package/VideoMeetingClient.js +1210 -0
- package/VideoProcessor.js +375 -0
- package/index.js +42 -0
- package/managers/ConnectionManager.js +243 -0
- package/managers/LocalMediaManager.js +1051 -0
- package/managers/MediasoupManager.js +789 -0
- package/managers/RemoteMediaManager.js +972 -0
- package/managers/StatsCollector.js +710 -0
- package/package.json +56 -0
- package/utils/EventEmitter.js +103 -0
- package/utils/Logger.js +114 -0
- package/utils/errors.js +136 -0
package/AudioMixer.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioMixer - Web Audio API based mixer for scalable audio handling
|
|
3
|
+
* Handles multiple audio streams efficiently by mixing them in a single audio context
|
|
4
|
+
* Used automatically for meetings with 30+ participants
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class AudioMixer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.audioContext = null;
|
|
10
|
+
this.masterGain = null;
|
|
11
|
+
this.participants = new Map(); // Map<participantId, { source, gainNode, stream }>
|
|
12
|
+
this.isInitialized = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the audio context and master gain
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (this.isInitialized) {
|
|
20
|
+
console.log('SDK :: AudioMixer :: Already initialized');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Create audio context
|
|
26
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
27
|
+
|
|
28
|
+
// Create master gain node (output)
|
|
29
|
+
this.masterGain = this.audioContext.createGain();
|
|
30
|
+
this.masterGain.gain.value = 1.0;
|
|
31
|
+
this.masterGain.connect(this.audioContext.destination);
|
|
32
|
+
|
|
33
|
+
this.isInitialized = true;
|
|
34
|
+
console.log('SDK :: AudioMixer :: Initialized successfully', {
|
|
35
|
+
sampleRate: this.audioContext.sampleRate,
|
|
36
|
+
state: this.audioContext.state,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Handle audio context state changes
|
|
40
|
+
this.audioContext.addEventListener('statechange', () => {
|
|
41
|
+
console.log('SDK :: AudioMixer :: State changed:', this.audioContext.state);
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('SDK :: AudioMixer :: Failed to initialize:', err);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resume audio context (needed for user interaction requirement)
|
|
51
|
+
*/
|
|
52
|
+
async resume() {
|
|
53
|
+
if (this.audioContext && this.audioContext.state === 'suspended') {
|
|
54
|
+
await this.audioContext.resume();
|
|
55
|
+
console.log('SDK :: AudioMixer :: Context resumed');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add a participant's audio stream to the mixer
|
|
61
|
+
*/
|
|
62
|
+
addParticipant(participantId, stream, volume = 1.0) {
|
|
63
|
+
if (!this.isInitialized) {
|
|
64
|
+
console.error('SDK :: AudioMixer :: Not initialized');
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Remove existing participant if already added
|
|
69
|
+
if (this.participants.has(participantId)) {
|
|
70
|
+
this.removeParticipant(participantId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Create media stream source
|
|
75
|
+
const source = this.audioContext.createMediaStreamSource(stream);
|
|
76
|
+
|
|
77
|
+
// Create gain node for this participant (for individual volume control)
|
|
78
|
+
const gainNode = this.audioContext.createGain();
|
|
79
|
+
gainNode.gain.value = volume;
|
|
80
|
+
|
|
81
|
+
// Connect: source -> gainNode -> masterGain -> destination
|
|
82
|
+
source.connect(gainNode);
|
|
83
|
+
gainNode.connect(this.masterGain);
|
|
84
|
+
|
|
85
|
+
// Store participant info
|
|
86
|
+
this.participants.set(participantId, {
|
|
87
|
+
source,
|
|
88
|
+
gainNode,
|
|
89
|
+
stream,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log('SDK :: AudioMixer :: Added participant:', participantId, {
|
|
93
|
+
volume,
|
|
94
|
+
participantCount: this.participants.size,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('SDK :: AudioMixer :: Failed to add participant:', participantId, err);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove a participant's audio stream from the mixer
|
|
106
|
+
*/
|
|
107
|
+
removeParticipant(participantId) {
|
|
108
|
+
const participant = this.participants.get(participantId);
|
|
109
|
+
if (!participant) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Disconnect nodes
|
|
115
|
+
participant.source.disconnect();
|
|
116
|
+
participant.gainNode.disconnect();
|
|
117
|
+
|
|
118
|
+
// Remove from map
|
|
119
|
+
this.participants.delete(participantId);
|
|
120
|
+
|
|
121
|
+
console.log('SDK :: AudioMixer :: Removed participant:', participantId, {
|
|
122
|
+
participantCount: this.participants.size,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('SDK :: AudioMixer :: Failed to remove participant:', participantId, err);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set volume for a specific participant (0.0 to 1.0)
|
|
134
|
+
*/
|
|
135
|
+
setParticipantVolume(participantId, volume) {
|
|
136
|
+
const participant = this.participants.get(participantId);
|
|
137
|
+
if (!participant) {
|
|
138
|
+
console.warn('SDK :: AudioMixer :: Participant not found:', participantId);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Clamp volume between 0 and 1
|
|
144
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
145
|
+
participant.gainNode.gain.value = clampedVolume;
|
|
146
|
+
|
|
147
|
+
console.log('SDK :: AudioMixer :: Set volume:', participantId, clampedVolume);
|
|
148
|
+
return true;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('SDK :: AudioMixer :: Failed to set volume:', participantId, err);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get current volume for a participant
|
|
157
|
+
*/
|
|
158
|
+
getParticipantVolume(participantId) {
|
|
159
|
+
const participant = this.participants.get(participantId);
|
|
160
|
+
return participant ? participant.gainNode.gain.value : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set master volume (affects all participants)
|
|
165
|
+
*/
|
|
166
|
+
setMasterVolume(volume) {
|
|
167
|
+
if (!this.masterGain) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
172
|
+
this.masterGain.gain.value = clampedVolume;
|
|
173
|
+
console.log('SDK :: AudioMixer :: Set master volume:', clampedVolume);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get master volume
|
|
179
|
+
*/
|
|
180
|
+
getMasterVolume() {
|
|
181
|
+
return this.masterGain ? this.masterGain.gain.value : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a participant is in the mixer
|
|
186
|
+
*/
|
|
187
|
+
hasParticipant(participantId) {
|
|
188
|
+
return this.participants.has(participantId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get number of participants in mixer
|
|
193
|
+
*/
|
|
194
|
+
getParticipantCount() {
|
|
195
|
+
return this.participants.size;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Clear all participants and reset
|
|
200
|
+
*/
|
|
201
|
+
clear() {
|
|
202
|
+
console.log('SDK :: AudioMixer :: Clearing all participants');
|
|
203
|
+
|
|
204
|
+
// Remove all participants
|
|
205
|
+
for (const participantId of this.participants.keys()) {
|
|
206
|
+
this.removeParticipant(participantId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.participants.clear();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Cleanup and close the audio context
|
|
214
|
+
*/
|
|
215
|
+
async destroy() {
|
|
216
|
+
console.log('SDK :: AudioMixer :: Destroying');
|
|
217
|
+
|
|
218
|
+
// Clear all participants
|
|
219
|
+
this.clear();
|
|
220
|
+
|
|
221
|
+
// Close audio context
|
|
222
|
+
if (this.audioContext) {
|
|
223
|
+
try {
|
|
224
|
+
await this.audioContext.close();
|
|
225
|
+
console.log('SDK :: AudioMixer :: Audio context closed');
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('SDK :: AudioMixer :: Failed to close audio context:', err);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.audioContext = null;
|
|
232
|
+
this.masterGain = null;
|
|
233
|
+
this.isInitialized = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Video Meeting SDK
|
|
2
|
+
|
|
3
|
+
A standalone, framework-agnostic SDK for building video conferencing applications with mediasoup.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎥 Camera, microphone, and screen sharing
|
|
8
|
+
- 👥 Multi-party video conferencing
|
|
9
|
+
- 🔄 Real-time media stream management
|
|
10
|
+
- 🎨 Virtual backgrounds with blur and custom images (MediaPipe)
|
|
11
|
+
- 📡 Simulcast for adaptive bitrate (3 quality layers)
|
|
12
|
+
- 🎯 Simple, promise-based API
|
|
13
|
+
- 📦 Framework agnostic (works with React, Vue, Svelte, vanilla JS)
|
|
14
|
+
- 🔌 Socket.io + mediasoup integration
|
|
15
|
+
- 🖼️ No UI dependencies - bring your own interface
|
|
16
|
+
- ✅ Mac, Windows, Linux support (latest mediasoup v3.16+)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @unboundcx/video-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Note:** This SDK connects to Unbound's managed video server infrastructure. You don't need to set up your own media servers.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Option 1: Using with Unbound SDK (Recommended)
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
import { VideoMeetingClient } from '@unboundcx/video-sdk';
|
|
32
|
+
import { UnboundSDK } from '@unboundcx/sdk';
|
|
33
|
+
|
|
34
|
+
// Initialize SDK
|
|
35
|
+
const sdk = new UnboundSDK('your-namespace', 'api.yourdomain.com');
|
|
36
|
+
|
|
37
|
+
// Initialize Video Client
|
|
38
|
+
const client = new VideoMeetingClient({ debug: true });
|
|
39
|
+
|
|
40
|
+
// Join using Unbound SDK API response
|
|
41
|
+
const joinResponse = await sdk.video.joinRoom('room-123', 'password', 'user@example.com');
|
|
42
|
+
await client.joinFromApiResponse(joinResponse);
|
|
43
|
+
|
|
44
|
+
// Publish local media
|
|
45
|
+
const videoStream = await client.publishCamera({ resolution: '720p' });
|
|
46
|
+
await client.publishMicrophone();
|
|
47
|
+
|
|
48
|
+
// Listen for remote participants
|
|
49
|
+
client.on('stream:added', ({ participantId, stream, type }) => {
|
|
50
|
+
const videoElement = document.getElementById(`video-${participantId}`);
|
|
51
|
+
videoElement.srcObject = stream;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Leave meeting
|
|
55
|
+
await client.leaveRoom();
|
|
56
|
+
await client.disconnect();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Option 2: Direct Integration (Custom Backend)
|
|
60
|
+
|
|
61
|
+
If you're building your own backend (not using Unbound's video server):
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
import { VideoMeetingClient } from '@unboundcx/video-sdk';
|
|
65
|
+
|
|
66
|
+
// Initialize - no serverUrl needed, will be provided by your join API
|
|
67
|
+
const client = new VideoMeetingClient({ debug: true });
|
|
68
|
+
|
|
69
|
+
// Your custom API should return the same format as Unbound SDK
|
|
70
|
+
const joinResponse = await yourApi.joinVideoRoom('room-123');
|
|
71
|
+
// joinResponse should include: { videoRoom, server, participant, authorization }
|
|
72
|
+
|
|
73
|
+
await client.joinFromApiResponse(joinResponse);
|
|
74
|
+
|
|
75
|
+
// Publish local camera
|
|
76
|
+
const videoStream = await client.publishCamera();
|
|
77
|
+
|
|
78
|
+
// Listen for remote participants
|
|
79
|
+
client.on('participant:joined', (participant) => {
|
|
80
|
+
console.log('Participant joined:', participant.id);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client.on('stream:added', ({ participantId, stream, type }) => {
|
|
84
|
+
const videoElement = document.getElementById(`video-${participantId}`);
|
|
85
|
+
videoElement.srcObject = stream;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Leave meeting
|
|
89
|
+
await client.leaveRoom();
|
|
90
|
+
await client.disconnect();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Note:** This SDK is designed to work with Unbound's video server infrastructure. For custom backends, you'll need to implement compatible server-side APIs.
|
|
94
|
+
|
|
95
|
+
## API Documentation
|
|
96
|
+
|
|
97
|
+
### Constructor
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
new VideoMeetingClient(options)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Options:**
|
|
104
|
+
- `serverUrl` (string, optional) - WebSocket server URL (not required if using `joinFromApiResponse`)
|
|
105
|
+
- `debug` (boolean, optional) - Enable debug logging
|
|
106
|
+
|
|
107
|
+
### Methods
|
|
108
|
+
|
|
109
|
+
#### Connection
|
|
110
|
+
|
|
111
|
+
**`joinFromApiResponse(joinResponse)`** ⭐ Recommended
|
|
112
|
+
- Joins a meeting using the response from `api.video.joinRoom()`
|
|
113
|
+
- Automatically extracts server URL, auth token, and room info
|
|
114
|
+
- Connects to video server and joins room in one call
|
|
115
|
+
- Returns: `Promise<Object>` - Full join data including videoRoom, participant, server info
|
|
116
|
+
|
|
117
|
+
**Example:**
|
|
118
|
+
```javascript
|
|
119
|
+
const joinResponse = await api.video.joinRoom('room-123', 'password', 'user@example.com');
|
|
120
|
+
await client.joinFromApiResponse(joinResponse);
|
|
121
|
+
|
|
122
|
+
// Access join data
|
|
123
|
+
const videoRoom = client.getVideoRoom();
|
|
124
|
+
const participant = client.getCurrentParticipant();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**`connect(authToken)`**
|
|
128
|
+
- Connect to server (manual method)
|
|
129
|
+
- Returns: `Promise<void>`
|
|
130
|
+
|
|
131
|
+
**`disconnect()`**
|
|
132
|
+
- Disconnect from server
|
|
133
|
+
- Returns: `Promise<void>`
|
|
134
|
+
|
|
135
|
+
**`joinRoom(roomId, options?)`**
|
|
136
|
+
- Join a meeting room (manual method, requires `connect()` first)
|
|
137
|
+
- Returns: `Promise<Object>` - Room data
|
|
138
|
+
|
|
139
|
+
**`leaveRoom()`**
|
|
140
|
+
- Leave current room
|
|
141
|
+
- Returns: `Promise<void>`
|
|
142
|
+
|
|
143
|
+
#### Local Media
|
|
144
|
+
|
|
145
|
+
**`publishCamera(options?)`** - Start publishing camera
|
|
146
|
+
- `options.resolution` - '480p', '720p', '1080p' (default: '720p')
|
|
147
|
+
- `options.frameRate` - Frame rate (default: 24)
|
|
148
|
+
- `options.deviceId` - Specific camera device ID
|
|
149
|
+
- `options.background` - Virtual background options:
|
|
150
|
+
- `{ type: 'blur', blurLevel: 8 }` - Blur background
|
|
151
|
+
- `{ type: 'image', imageUrl: '/path/to/image.jpg' }` - Virtual background
|
|
152
|
+
- `{ type: 'none' }` - No effect (default)
|
|
153
|
+
- `options.simulcast` - Enable simulcast (default: true for video)
|
|
154
|
+
- Returns: `Promise<MediaStream>`
|
|
155
|
+
|
|
156
|
+
**Example:**
|
|
157
|
+
```javascript
|
|
158
|
+
// Publish with blur background
|
|
159
|
+
await client.publishCamera({
|
|
160
|
+
resolution: '720p',
|
|
161
|
+
background: { type: 'blur', blurLevel: 8 }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Publish with virtual background
|
|
165
|
+
await client.publishCamera({
|
|
166
|
+
background: {
|
|
167
|
+
type: 'image',
|
|
168
|
+
imageUrl: '/images/backgrounds/office.jpg'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**`updateCameraBackground(options)`** - Change background on active camera
|
|
174
|
+
- `options.type` - 'none', 'blur', or 'image'
|
|
175
|
+
- `options.blurLevel` - Blur amount in pixels (default: 8)
|
|
176
|
+
- `options.imageUrl` - Background image URL
|
|
177
|
+
- Returns: `Promise<MediaStream>`
|
|
178
|
+
|
|
179
|
+
**Other Methods:**
|
|
180
|
+
- `publishMicrophone(options?)` - Start publishing microphone
|
|
181
|
+
- `publishScreenShare()` - Start screen sharing
|
|
182
|
+
- `stopCamera()` - Stop camera stream
|
|
183
|
+
- `stopMicrophone()` - Stop microphone stream
|
|
184
|
+
- `stopScreenShare()` - Stop screen sharing
|
|
185
|
+
- `changeCamera(deviceId)` - Switch to different camera
|
|
186
|
+
- `changeMicrophone(deviceId)` - Switch to different microphone
|
|
187
|
+
- `muteCamera()` - Mute camera
|
|
188
|
+
- `unmuteCamera()` - Unmute camera
|
|
189
|
+
- `muteMicrophone()` - Mute microphone
|
|
190
|
+
- `unmuteMicrophone()` - Unmute microphone
|
|
191
|
+
|
|
192
|
+
#### Device Management
|
|
193
|
+
- `getDevices()` - Get available media devices
|
|
194
|
+
- `getLocalStream(type)` - Get local stream by type
|
|
195
|
+
|
|
196
|
+
#### State and Info
|
|
197
|
+
- `getState()` - Get current SDK state ('disconnected', 'connecting', 'connected', 'joining', 'in-room')
|
|
198
|
+
- `isConnected()` - Check if connected to server
|
|
199
|
+
- `isInRoom()` - Check if currently in a room
|
|
200
|
+
- `isCameraActive()` - Check if camera is active
|
|
201
|
+
- `isMicrophoneActive()` - Check if microphone is active
|
|
202
|
+
- `isCameraMuted()` - Check if camera is muted
|
|
203
|
+
- `isMicrophoneMuted()` - Check if microphone is muted
|
|
204
|
+
- `getParticipantCount()` - Get number of remote participants
|
|
205
|
+
|
|
206
|
+
#### Join Data Access (when using `joinFromApiResponse`)
|
|
207
|
+
- `getVideoRoom()` - Get video room info (id, name, friendlyName, waitingRoom, etc.)
|
|
208
|
+
- `getCurrentParticipant()` - Get current participant info (id, name, email, isHost, etc.)
|
|
209
|
+
- `getServerInfo()` - Get server info (url, serverId, socketPort)
|
|
210
|
+
- `getJoinData()` - Get full join data (videoRoom, participant, server, authorization)
|
|
211
|
+
|
|
212
|
+
### Events
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
client.on('event-name', (data) => {
|
|
216
|
+
// Handle event
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Connection Events:**
|
|
221
|
+
- `connected` - Connected to server
|
|
222
|
+
- `disconnected` - Disconnected from server
|
|
223
|
+
- `error` - Connection error
|
|
224
|
+
|
|
225
|
+
**Room Events:**
|
|
226
|
+
- `room:joined` - Successfully joined room
|
|
227
|
+
- `room:left` - Left the room
|
|
228
|
+
- `participant:joined` - New participant joined
|
|
229
|
+
- `participant:left` - Participant left
|
|
230
|
+
- `participant:updated` - Participant state changed
|
|
231
|
+
|
|
232
|
+
**Media Events:**
|
|
233
|
+
- `stream:added` - New media stream available
|
|
234
|
+
- `stream:removed` - Media stream removed
|
|
235
|
+
- `local-stream:added` - Local stream started
|
|
236
|
+
- `local-stream:removed` - Local stream stopped
|
|
237
|
+
|
|
238
|
+
**State Events:**
|
|
239
|
+
- `state:changed` - SDK state changed
|
|
240
|
+
|
|
241
|
+
## Example: Complete Video Call
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
import { VideoMeetingClient } from '@yourcompany/video-sdk';
|
|
245
|
+
|
|
246
|
+
class VideoCall {
|
|
247
|
+
constructor() {
|
|
248
|
+
this.client = new VideoMeetingClient({
|
|
249
|
+
serverUrl: 'wss://video.example.com',
|
|
250
|
+
debug: true
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this.setupEventListeners();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setupEventListeners() {
|
|
257
|
+
// Handle remote streams
|
|
258
|
+
this.client.on('stream:added', ({ participantId, stream, type }) => {
|
|
259
|
+
this.addRemoteStream(participantId, stream, type);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
this.client.on('stream:removed', ({ participantId, type }) => {
|
|
263
|
+
this.removeRemoteStream(participantId, type);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Handle participants
|
|
267
|
+
this.client.on('participant:joined', (participant) => {
|
|
268
|
+
this.addParticipantUI(participant);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
this.client.on('participant:left', ({ participantId }) => {
|
|
272
|
+
this.removeParticipantUI(participantId);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Handle errors
|
|
276
|
+
this.client.on('error', (error) => {
|
|
277
|
+
console.error('SDK Error:', error);
|
|
278
|
+
this.showErrorMessage(error.message);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async start(roomId, authToken) {
|
|
283
|
+
try {
|
|
284
|
+
// Connect to server
|
|
285
|
+
await this.client.connect(authToken);
|
|
286
|
+
|
|
287
|
+
// Join room
|
|
288
|
+
await this.client.joinRoom(roomId);
|
|
289
|
+
|
|
290
|
+
// Start local media
|
|
291
|
+
const videoStream = await this.client.publishCamera({
|
|
292
|
+
resolution: '720p'
|
|
293
|
+
});
|
|
294
|
+
const audioStream = await this.client.publishMicrophone();
|
|
295
|
+
|
|
296
|
+
// Display local video
|
|
297
|
+
this.showLocalVideo(videoStream);
|
|
298
|
+
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Failed to start call:', error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async toggleCamera() {
|
|
306
|
+
if (this.client.isCameraMuted()) {
|
|
307
|
+
await this.client.unmuteCamera();
|
|
308
|
+
} else {
|
|
309
|
+
await this.client.muteCamera();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async toggleMicrophone() {
|
|
314
|
+
if (this.client.isMicrophoneMuted()) {
|
|
315
|
+
await this.client.unmuteMicrophone();
|
|
316
|
+
} else {
|
|
317
|
+
await this.client.muteMicrophone();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async startScreenShare() {
|
|
322
|
+
try {
|
|
323
|
+
await this.client.publishScreenShare();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error.name === 'NotAllowedError') {
|
|
326
|
+
console.log('Screen share permission denied');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async end() {
|
|
332
|
+
await this.client.leaveRoom();
|
|
333
|
+
await this.client.disconnect();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// UI methods (implement based on your framework)
|
|
337
|
+
addRemoteStream(participantId, stream, type) { }
|
|
338
|
+
removeRemoteStream(participantId, type) { }
|
|
339
|
+
addParticipantUI(participant) { }
|
|
340
|
+
removeParticipantUI(participantId) { }
|
|
341
|
+
showLocalVideo(stream) { }
|
|
342
|
+
showErrorMessage(message) { }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Usage
|
|
346
|
+
const videoCall = new VideoCall();
|
|
347
|
+
await videoCall.start('room-123', 'auth-token-xyz');
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Architecture
|
|
351
|
+
|
|
352
|
+
The SDK is composed of several managers:
|
|
353
|
+
|
|
354
|
+
- **MediasoupManager** - Manages mediasoup Device and Transports
|
|
355
|
+
- **LocalMediaManager** - Handles camera, microphone, screen share
|
|
356
|
+
- **RemoteMediaManager** - Manages remote participant streams
|
|
357
|
+
- **ConnectionManager** - Socket.io connection and signaling
|
|
358
|
+
|
|
359
|
+
## State Machine
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
disconnected -> connecting -> connected -> joining -> in-room -> leaving -> connected
|
|
363
|
+
↓ ↓
|
|
364
|
+
error error
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Error Handling
|
|
368
|
+
|
|
369
|
+
All errors are wrapped in custom error classes:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
try {
|
|
373
|
+
await client.publishCamera();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error.name === 'DeviceNotFoundError') {
|
|
376
|
+
console.log('Camera not found');
|
|
377
|
+
} else if (error.name === 'PermissionDeniedError') {
|
|
378
|
+
console.log('Camera permission denied');
|
|
379
|
+
} else if (error.name === 'ConnectionError') {
|
|
380
|
+
console.log('Connection failed');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Development
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
# Install dependencies
|
|
389
|
+
npm install
|
|
390
|
+
|
|
391
|
+
# Run tests
|
|
392
|
+
npm test
|
|
393
|
+
|
|
394
|
+
# Build
|
|
395
|
+
npm run build
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT
|