@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/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unboundcx/video-sdk-client",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Framework-agnostic WebRTC video meeting SDK powered by mediasoup",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./VideoMeetingClient.js",
|
|
10
|
+
"./managers/*": "./managers/*.js",
|
|
11
|
+
"./utils/*": "./utils/*.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"webrtc",
|
|
18
|
+
"video",
|
|
19
|
+
"meeting",
|
|
20
|
+
"mediasoup",
|
|
21
|
+
"conferencing",
|
|
22
|
+
"real-time",
|
|
23
|
+
"streaming",
|
|
24
|
+
"sfu"
|
|
25
|
+
],
|
|
26
|
+
"author": "UnboundCX",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"mediasoup-client": "^3.x",
|
|
30
|
+
"socket.io-client": "^4.x"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@mediapipe/selfie_segmentation": "^0.1.1675465747"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/unboundcx/video-sdk"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/unboundcx/video-sdk/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/unboundcx/video-sdk#readme",
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"VideoMeetingClient.js",
|
|
46
|
+
"VideoProcessor.js",
|
|
47
|
+
"AudioMixer.js",
|
|
48
|
+
"managers/",
|
|
49
|
+
"utils/",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=14.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter implementation for the SDK
|
|
3
|
+
* Allows subscribing to and emitting events
|
|
4
|
+
*/
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._events = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Subscribe to an event
|
|
12
|
+
* @param {string} event - Event name
|
|
13
|
+
* @param {Function} handler - Event handler function
|
|
14
|
+
* @returns {Function} Unsubscribe function
|
|
15
|
+
*/
|
|
16
|
+
on(event, handler) {
|
|
17
|
+
if (!this._events.has(event)) {
|
|
18
|
+
this._events.set(event, []);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handlers = this._events.get(event);
|
|
22
|
+
handlers.push(handler);
|
|
23
|
+
|
|
24
|
+
// Return unsubscribe function
|
|
25
|
+
return () => {
|
|
26
|
+
const index = handlers.indexOf(handler);
|
|
27
|
+
if (index > -1) {
|
|
28
|
+
handlers.splice(index, 1);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to an event that only fires once
|
|
35
|
+
* @param {string} event - Event name
|
|
36
|
+
* @param {Function} handler - Event handler function
|
|
37
|
+
* @returns {Function} Unsubscribe function
|
|
38
|
+
*/
|
|
39
|
+
once(event, handler) {
|
|
40
|
+
const wrappedHandler = (...args) => {
|
|
41
|
+
handler(...args);
|
|
42
|
+
this.off(event, wrappedHandler);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return this.on(event, wrappedHandler);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unsubscribe from an event
|
|
50
|
+
* @param {string} event - Event name
|
|
51
|
+
* @param {Function} handler - Event handler function to remove
|
|
52
|
+
*/
|
|
53
|
+
off(event, handler) {
|
|
54
|
+
const handlers = this._events.get(event);
|
|
55
|
+
if (!handlers) return;
|
|
56
|
+
|
|
57
|
+
const index = handlers.indexOf(handler);
|
|
58
|
+
if (index > -1) {
|
|
59
|
+
handlers.splice(index, 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emit an event to all subscribers
|
|
65
|
+
* @param {string} event - Event name
|
|
66
|
+
* @param {*} data - Event data
|
|
67
|
+
*/
|
|
68
|
+
emit(event, data) {
|
|
69
|
+
const handlers = this._events.get(event);
|
|
70
|
+
if (!handlers || handlers.length === 0) return;
|
|
71
|
+
|
|
72
|
+
// Call all handlers with the event data
|
|
73
|
+
for (const handler of handlers) {
|
|
74
|
+
try {
|
|
75
|
+
handler(data);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`Error in event handler for "${event}":`, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Remove all event listeners
|
|
84
|
+
* @param {string?} event - Optional event name to clear only that event
|
|
85
|
+
*/
|
|
86
|
+
removeAllListeners(event = null) {
|
|
87
|
+
if (event) {
|
|
88
|
+
this._events.delete(event);
|
|
89
|
+
} else {
|
|
90
|
+
this._events.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get count of listeners for an event
|
|
96
|
+
* @param {string} event - Event name
|
|
97
|
+
* @returns {number} Number of listeners
|
|
98
|
+
*/
|
|
99
|
+
listenerCount(event) {
|
|
100
|
+
const handlers = this._events.get(event);
|
|
101
|
+
return handlers ? handlers.length : 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/utils/Logger.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility with namespaced prefixes and debug mode
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Global filters for verbose logs (stats, keepalive, etc.)
|
|
6
|
+
const globalFilters = {
|
|
7
|
+
stats: false,
|
|
8
|
+
keepalive: false,
|
|
9
|
+
performance: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class Logger {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} namespace - Logger namespace (e.g., 'SDK:ConnectionManager')
|
|
15
|
+
* @param {boolean} debug - Enable debug logging
|
|
16
|
+
*/
|
|
17
|
+
constructor(namespace, debug = false) {
|
|
18
|
+
this.namespace = namespace;
|
|
19
|
+
this.debug = debug;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set global filter for verbose log categories
|
|
24
|
+
* @param {string} category - Category name (stats, keepalive, performance)
|
|
25
|
+
* @param {boolean} enabled - Enable or disable this category
|
|
26
|
+
*/
|
|
27
|
+
static setFilter(category, enabled) {
|
|
28
|
+
if (category in globalFilters) {
|
|
29
|
+
globalFilters[category] = enabled;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable/disable all verbose logging categories
|
|
35
|
+
* @param {boolean} enabled - Enable or disable verbose logs
|
|
36
|
+
*/
|
|
37
|
+
static setVerbose(enabled) {
|
|
38
|
+
Object.keys(globalFilters).forEach(key => {
|
|
39
|
+
globalFilters[key] = enabled;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a message should be filtered based on content
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
_shouldFilter(args) {
|
|
48
|
+
const message = args.join(' ').toLowerCase();
|
|
49
|
+
|
|
50
|
+
if (!globalFilters.stats && (message.includes('stats') || message.includes('mos'))) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (!globalFilters.keepalive && message.includes('keepalive')) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (!globalFilters.performance && message.includes('performance')) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format message with namespace prefix
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
_format(level, ...args) {
|
|
68
|
+
const prefix = `[${this.namespace}]`;
|
|
69
|
+
return [prefix, ...args];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Log info message
|
|
74
|
+
*/
|
|
75
|
+
info(...args) {
|
|
76
|
+
if (this.debug && !this._shouldFilter(args)) {
|
|
77
|
+
console.log(...this._format('INFO', ...args));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log warning message
|
|
83
|
+
*/
|
|
84
|
+
warn(...args) {
|
|
85
|
+
if (!this._shouldFilter(args)) {
|
|
86
|
+
console.warn(...this._format('WARN', ...args));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log error message (never filtered)
|
|
92
|
+
*/
|
|
93
|
+
error(...args) {
|
|
94
|
+
console.error(...this._format('ERROR', ...args));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Log debug message (only in debug mode)
|
|
99
|
+
*/
|
|
100
|
+
log(...args) {
|
|
101
|
+
if (this.debug && !this._shouldFilter(args)) {
|
|
102
|
+
console.log(...this._format('DEBUG', ...args));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a child logger with additional namespace
|
|
108
|
+
* @param {string} childNamespace - Additional namespace to append
|
|
109
|
+
* @returns {Logger} New logger instance
|
|
110
|
+
*/
|
|
111
|
+
child(childNamespace) {
|
|
112
|
+
return new Logger(`${this.namespace}:${childNamespace}`, this.debug);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/utils/errors.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for the SDK
|
|
3
|
+
* These provide better error handling and debugging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base SDK Error class
|
|
8
|
+
*/
|
|
9
|
+
export class SDKError extends Error {
|
|
10
|
+
constructor(message, code = 'SDK_ERROR') {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SDKError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.timestamp = new Date().toISOString();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connection-related errors
|
|
20
|
+
*/
|
|
21
|
+
export class ConnectionError extends SDKError {
|
|
22
|
+
constructor(message, details = {}) {
|
|
23
|
+
super(message, 'CONNECTION_ERROR');
|
|
24
|
+
this.name = 'ConnectionError';
|
|
25
|
+
this.details = details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Device/media permission errors
|
|
31
|
+
*/
|
|
32
|
+
export class PermissionDeniedError extends SDKError {
|
|
33
|
+
constructor(deviceType, originalError = null) {
|
|
34
|
+
super(
|
|
35
|
+
`Permission denied for ${deviceType}. Please allow access in your browser settings.`,
|
|
36
|
+
'PERMISSION_DENIED'
|
|
37
|
+
);
|
|
38
|
+
this.name = 'PermissionDeniedError';
|
|
39
|
+
this.deviceType = deviceType;
|
|
40
|
+
this.originalError = originalError;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Device not found errors
|
|
46
|
+
*/
|
|
47
|
+
export class DeviceNotFoundError extends SDKError {
|
|
48
|
+
constructor(deviceType, deviceId = null) {
|
|
49
|
+
super(
|
|
50
|
+
`${deviceType} device not found${deviceId ? ` (ID: ${deviceId})` : ''}`,
|
|
51
|
+
'DEVICE_NOT_FOUND'
|
|
52
|
+
);
|
|
53
|
+
this.name = 'DeviceNotFoundError';
|
|
54
|
+
this.deviceType = deviceType;
|
|
55
|
+
this.deviceId = deviceId;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* mediasoup-related errors
|
|
61
|
+
*/
|
|
62
|
+
export class MediasoupError extends SDKError {
|
|
63
|
+
constructor(message, operation = null, originalError = null) {
|
|
64
|
+
super(message, 'MEDIASOUP_ERROR');
|
|
65
|
+
this.name = 'MediasoupError';
|
|
66
|
+
this.operation = operation;
|
|
67
|
+
this.originalError = originalError;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* State-related errors (invalid state transitions)
|
|
73
|
+
*/
|
|
74
|
+
export class StateError extends SDKError {
|
|
75
|
+
constructor(message, currentState = null, expectedState = null) {
|
|
76
|
+
super(message, 'STATE_ERROR');
|
|
77
|
+
this.name = 'StateError';
|
|
78
|
+
this.currentState = currentState;
|
|
79
|
+
this.expectedState = expectedState;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Room/meeting errors
|
|
85
|
+
*/
|
|
86
|
+
export class RoomError extends SDKError {
|
|
87
|
+
constructor(message, roomId = null) {
|
|
88
|
+
super(message, 'ROOM_ERROR');
|
|
89
|
+
this.name = 'RoomError';
|
|
90
|
+
this.roomId = roomId;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Timeout errors
|
|
96
|
+
*/
|
|
97
|
+
export class TimeoutError extends SDKError {
|
|
98
|
+
constructor(operation, timeoutMs) {
|
|
99
|
+
super(
|
|
100
|
+
`Operation "${operation}" timed out after ${timeoutMs}ms`,
|
|
101
|
+
'TIMEOUT_ERROR'
|
|
102
|
+
);
|
|
103
|
+
this.name = 'TimeoutError';
|
|
104
|
+
this.operation = operation;
|
|
105
|
+
this.timeoutMs = timeoutMs;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Helper function to wrap unknown errors
|
|
111
|
+
* @param {Error} error - Original error
|
|
112
|
+
* @param {string} context - Context where error occurred
|
|
113
|
+
* @returns {SDKError} Wrapped error
|
|
114
|
+
*/
|
|
115
|
+
export function wrapError(error, context) {
|
|
116
|
+
if (error instanceof SDKError) {
|
|
117
|
+
return error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for common browser errors
|
|
121
|
+
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
|
|
122
|
+
return new PermissionDeniedError(context, error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
|
|
126
|
+
return new DeviceNotFoundError(context, null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Wrap as generic SDK error
|
|
130
|
+
const sdkError = new SDKError(
|
|
131
|
+
`${context}: ${error.message}`,
|
|
132
|
+
'UNKNOWN_ERROR'
|
|
133
|
+
);
|
|
134
|
+
sdkError.originalError = error;
|
|
135
|
+
return sdkError;
|
|
136
|
+
}
|