videocall-client-socket 0.1.6 → 0.1.8
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 +35 -20
- package/package.json +1 -1
- package/src/index.d.ts +2 -2
- package/src/index.js +72 -30
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 📞 videocall-client
|
|
1
|
+
# 📞 videocall-socket-client
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A WebRTC client library for multi-user video calls (mesh per room) using [simple-peer](https://www.npmjs.com/package/simple-peer) and [socket.io-client](https://www.npmjs.com/package/socket.io-client).
|
|
4
4
|
|
|
5
5
|
> ✅ Designed to work with [videocall-server](https://github.com/EmersonJaraG28/videocall-server)
|
|
6
6
|
|
|
@@ -30,7 +30,10 @@ This makes SimplePeer available globally in the browser.
|
|
|
30
30
|
import * as VideoClient from "videocall-client-socket";
|
|
31
31
|
import { v4 as uuidv4 } from "uuid";
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// Stable user identity (same user across devices/sessions)
|
|
34
|
+
const userId = "user-123";
|
|
35
|
+
// Unique device/session identity
|
|
36
|
+
const userUUID = uuidv4();
|
|
34
37
|
const channelName = "roomABC";
|
|
35
38
|
|
|
36
39
|
// Step 1: Subscribe to events before anything else
|
|
@@ -39,31 +42,34 @@ VideoClient.on("user-published", (data) => {
|
|
|
39
42
|
const video = document.createElement("video");
|
|
40
43
|
video.srcObject = user.videotrack;
|
|
41
44
|
video.autoplay = true;
|
|
42
|
-
video.id = user.
|
|
45
|
+
video.id = user.userUUID;
|
|
43
46
|
document.body.appendChild(video);
|
|
44
47
|
});
|
|
45
48
|
VideoClient.on("user-unpublished", (data) => {
|
|
46
49
|
const { user, mediaType } = data;
|
|
47
50
|
if (mediaType === "video") {
|
|
48
|
-
const video = document.getElementById(`${user.
|
|
51
|
+
const video = document.getElementById(`${user.userUUID}`);
|
|
49
52
|
if (video) {
|
|
50
53
|
video.remove();
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
});
|
|
54
57
|
VideoClient.on("user-media-toggled", (data) => {
|
|
55
|
-
console.log(data.user.
|
|
58
|
+
console.log(data.user.userId, data.user.userUUID, data.type, data.enabled);
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
// Step 2: Create media stream
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Step 3: Play local stream in <video> tag
|
|
62
|
-
VideoClient.playVideoTrack("localVideo");
|
|
63
|
-
|
|
64
|
-
// Step 4: Join the signaling channel
|
|
65
|
-
VideoClient.
|
|
66
|
-
|
|
62
|
+
VideoClient.createMediaStream()
|
|
63
|
+
.then(() => {
|
|
64
|
+
// Step 3: Play local stream in <video> tag
|
|
65
|
+
VideoClient.playVideoTrack("localVideo");
|
|
66
|
+
VideoClient.setServerURL("http://localhost:3000");
|
|
67
|
+
// Step 4: Join the signaling channel
|
|
68
|
+
VideoClient.joinChannel(userId, userUUID, channelName);
|
|
69
|
+
})
|
|
70
|
+
.catch((err) => {
|
|
71
|
+
console.error(err);
|
|
72
|
+
});
|
|
67
73
|
```
|
|
68
74
|
|
|
69
75
|
---
|
|
@@ -77,7 +83,7 @@ VideoClient.joinChannel(userId, channelName);
|
|
|
77
83
|
| 1️⃣ | `on(...)` | You must subscribe to events **before** joining the channel. |
|
|
78
84
|
| 2️⃣ | `createMediaStream()` | This requests access to camera and microphone. |
|
|
79
85
|
| 3️⃣ | `playVideoTrack(videoElementId)` | This shows your local stream in a `<video>` tag. |
|
|
80
|
-
| 4️⃣ | `joinChannel(userId, room)`
|
|
86
|
+
| 4️⃣ | `joinChannel(userId, userUUID, room)` | Finally, join the signaling server and start peer connections. |
|
|
81
87
|
|
|
82
88
|
> 🧠 Skipping or reordering these steps may cause video/audio to not work properly or event listeners to be missed.
|
|
83
89
|
|
|
@@ -88,7 +94,7 @@ VideoClient.joinChannel(userId, channelName);
|
|
|
88
94
|
### 🔌 Connection
|
|
89
95
|
|
|
90
96
|
- `setServerURL(url: string): void`
|
|
91
|
-
- `joinChannel(userId: string, room: string): void`
|
|
97
|
+
- `joinChannel(userId: string, userUUID: string, room: string): void`
|
|
92
98
|
- `leaveChannel(): void`
|
|
93
99
|
|
|
94
100
|
### 🎥 Media Controls
|
|
@@ -104,9 +110,9 @@ VideoClient.joinChannel(userId, channelName);
|
|
|
104
110
|
|
|
105
111
|
Use `on(event, callback)` and `off(event, callback)`.
|
|
106
112
|
|
|
107
|
-
- `user-published`: `{ user: {
|
|
108
|
-
- `user-unpublished`: `{ user: {
|
|
109
|
-
- `user-media-toggled`: `{ user: {
|
|
113
|
+
- `user-published`: `{ user: { userId, userUUID, videotrack }, mediaType }`
|
|
114
|
+
- `user-unpublished`: `{ user: { userId, userUUID }, mediaType }`
|
|
115
|
+
- `user-media-toggled`: `{ user: { userId, userUUID }, type, enabled }`
|
|
110
116
|
|
|
111
117
|
#### Example:
|
|
112
118
|
|
|
@@ -115,13 +121,22 @@ VideoClient.on("user-published", ({ user }) => {
|
|
|
115
121
|
const video = document.createElement("video");
|
|
116
122
|
video.srcObject = user.videotrack;
|
|
117
123
|
video.autoplay = true;
|
|
118
|
-
video.id = user.
|
|
124
|
+
video.id = user.userUUID;
|
|
119
125
|
document.body.appendChild(video);
|
|
120
126
|
});
|
|
121
127
|
```
|
|
122
128
|
|
|
123
129
|
---
|
|
124
130
|
|
|
131
|
+
## 🕸 Multi-user Model
|
|
132
|
+
|
|
133
|
+
- Each room supports multiple participants.
|
|
134
|
+
- WebRTC connections are created in mesh mode (one peer connection per remote participant).
|
|
135
|
+
- `userId` identifies the person.
|
|
136
|
+
- `userUUID` identifies a specific device/session for that person.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
125
140
|
## 🧩 Backend Integration
|
|
126
141
|
|
|
127
142
|
This library is designed to work with the following signaling server:
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ declare module 'videocall-client-socket' {
|
|
|
14
14
|
|
|
15
15
|
export function playVideoTrack(localVideoId: string): void;
|
|
16
16
|
|
|
17
|
-
export function joinChannel(userId: string, channelName: string): void;
|
|
17
|
+
export function joinChannel(userId: string, userUUID: string, channelName: string): void;
|
|
18
18
|
export function leaveChannel(): void;
|
|
19
|
-
}
|
|
19
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,8 @@ var socket = null;
|
|
|
5
5
|
var mediastream = null;
|
|
6
6
|
var localVideoId = "";
|
|
7
7
|
var userId = "";
|
|
8
|
-
var
|
|
8
|
+
var userUUID = "";
|
|
9
|
+
var serverURL = 'http://localhost:3000';
|
|
9
10
|
let events = {};
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -42,20 +43,39 @@ let emit = (_event, data) => {
|
|
|
42
43
|
* @param {string} _url - Server URL (e.g. "http://localhost:3000").
|
|
43
44
|
*/
|
|
44
45
|
export function setServerURL(_url) {
|
|
45
|
-
|
|
46
|
+
serverURL = _url;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
|
-
* Requests access to
|
|
50
|
-
*
|
|
50
|
+
* Requests access to both camera and microphone.
|
|
51
|
+
* Throws an error if either device is missing or denied.
|
|
52
|
+
* @returns {Promise<MediaStream>}
|
|
51
53
|
*/
|
|
52
54
|
export function createMediaStream() {
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((stream) => {
|
|
57
|
+
const hasVideo = stream.getVideoTracks().length > 0;
|
|
58
|
+
const hasAudio = stream.getAudioTracks().length > 0;
|
|
59
|
+
|
|
60
|
+
if (!hasVideo || !hasAudio) {
|
|
61
|
+
stream.getTracks().forEach(track => track.stop()); // cleanup
|
|
62
|
+
throw new Error(
|
|
63
|
+
!hasVideo && !hasAudio
|
|
64
|
+
? "Camera and microphone are not available."
|
|
65
|
+
: !hasVideo
|
|
66
|
+
? "Camera is not available."
|
|
67
|
+
: "Microphone is not available."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
mediastream = stream;
|
|
72
|
+
resolve(mediastream);
|
|
73
|
+
}).catch((err) => {
|
|
74
|
+
reject("Camera and microphone are not available.");
|
|
75
|
+
});
|
|
56
76
|
});
|
|
57
|
-
}
|
|
58
77
|
|
|
78
|
+
}
|
|
59
79
|
/**
|
|
60
80
|
* Plays the local video stream into a specified <video> element.
|
|
61
81
|
* @param {string} _localVideoId - The ID of the <video> element in the DOM.
|
|
@@ -71,53 +91,64 @@ export function playVideoTrack(_localVideoId) {
|
|
|
71
91
|
/**
|
|
72
92
|
* Connects to the signaling server and joins a room.
|
|
73
93
|
* @param {string} _userId - Unique identifier for the user.
|
|
94
|
+
* @param {string} _userUUID - Unique identifier for this device/session.
|
|
74
95
|
* @param {string} channelName - The name of the room to join.
|
|
75
96
|
*/
|
|
76
|
-
export function joinChannel(_userId, channelName) {
|
|
97
|
+
export function joinChannel(_userId, _userUUID, channelName) {
|
|
77
98
|
userId = _userId;
|
|
99
|
+
userUUID = _userUUID;
|
|
78
100
|
|
|
79
|
-
socket = io(
|
|
80
|
-
query: { room: channelName, userId }
|
|
101
|
+
socket = io(serverURL, {
|
|
102
|
+
query: { room: channelName, userId, userUUID }
|
|
81
103
|
});
|
|
82
104
|
|
|
83
105
|
socket.on('all-users', users => {
|
|
84
|
-
users.forEach(({ userId: remoteUserId, socketId }) => {
|
|
85
|
-
const peer = createPeer(socket, mediastream, socketId, remoteUserId, true);
|
|
86
|
-
peers[
|
|
106
|
+
users.forEach(({ userId: remoteUserId, userUUID: remoteUserUUID, socketId, }) => {
|
|
107
|
+
const peer = createPeer(socket, mediastream, socketId, remoteUserId, remoteUserUUID, true);
|
|
108
|
+
peers[remoteUserUUID] = peer;
|
|
87
109
|
});
|
|
88
110
|
});
|
|
89
111
|
|
|
90
|
-
socket.on('user-joined', ({ userId: newUserId, socketId: newSocketId }) => {
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
socket.on('user-joined', ({ userId: newUserId, userUUID: newUserUUID, socketId: newSocketId }) => {
|
|
113
|
+
// console.log("User joined", newUserId, newUserUUID, newSocketId);
|
|
114
|
+
|
|
115
|
+
const peer = createPeer(socket, mediastream, newSocketId, newUserId, newUserUUID, false);
|
|
116
|
+
peers[newUserUUID] = peer;
|
|
93
117
|
});
|
|
94
118
|
|
|
95
|
-
socket.on('signal', ({ fromUserId, signal }) => {
|
|
96
|
-
const peer = peers[fromUserId];
|
|
119
|
+
socket.on('signal', ({ fromUserId, fromUserUUID, signal }) => {
|
|
120
|
+
const peer = peers[fromUserUUID || fromUserId];
|
|
97
121
|
if (peer) {
|
|
98
122
|
peer.signal(signal);
|
|
99
123
|
}
|
|
100
124
|
});
|
|
101
125
|
|
|
102
|
-
socket.on('user-left', ({ userId: leftUserId }) => {
|
|
103
|
-
if (peers[
|
|
104
|
-
peers[
|
|
105
|
-
delete peers[
|
|
126
|
+
socket.on('user-left', ({ userId: leftUserId, userUUID: leftUserUUID }) => {
|
|
127
|
+
if (peers[leftUserUUID]) {
|
|
128
|
+
peers[leftUserUUID].destroy();
|
|
129
|
+
delete peers[leftUserUUID];
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
emit("user-unpublished", {
|
|
109
|
-
user: {
|
|
133
|
+
user: {
|
|
134
|
+
userId: leftUserId,
|
|
135
|
+
userUUID: leftUserUUID
|
|
136
|
+
},
|
|
110
137
|
mediaType: "video"
|
|
111
138
|
});
|
|
112
139
|
});
|
|
113
140
|
|
|
114
|
-
socket.on('user-media-toggled', ({ userId, type, enabled }) => {
|
|
141
|
+
socket.on('user-media-toggled', ({ userId, userUUID, type, enabled }) => {
|
|
115
142
|
emit('user-media-toggled', {
|
|
116
|
-
user: {
|
|
143
|
+
user: {
|
|
144
|
+
userId,
|
|
145
|
+
userUUID
|
|
146
|
+
},
|
|
117
147
|
type,
|
|
118
148
|
enabled
|
|
119
149
|
});
|
|
120
150
|
});
|
|
151
|
+
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
/**
|
|
@@ -155,10 +186,11 @@ export function leaveChannel() {
|
|
|
155
186
|
* @param {MediaStream} localStream - The local media stream.
|
|
156
187
|
* @param {string} targetSocketId - The socket ID of the remote user.
|
|
157
188
|
* @param {string} remoteUserId - The ID of the remote user.
|
|
189
|
+
* @param {string} remoteUserUUID - The device/session ID of the remote user.
|
|
158
190
|
* @param {boolean} initiator - Whether this peer initiates the connection.
|
|
159
191
|
* @returns {SimplePeer.Instance} - The created peer.
|
|
160
192
|
*/
|
|
161
|
-
function createPeer(socket, localStream, targetSocketId, remoteUserId, initiator) {
|
|
193
|
+
function createPeer(socket, localStream, targetSocketId, remoteUserId, remoteUserUUID, initiator) {
|
|
162
194
|
const peer = new SimplePeer({
|
|
163
195
|
initiator,
|
|
164
196
|
trickle: false,
|
|
@@ -173,13 +205,23 @@ function createPeer(socket, localStream, targetSocketId, remoteUserId, initiator
|
|
|
173
205
|
});
|
|
174
206
|
|
|
175
207
|
peer.on('stream', stream => {
|
|
208
|
+
|
|
176
209
|
emit("user-published", {
|
|
177
210
|
user: {
|
|
178
|
-
|
|
211
|
+
userId: remoteUserId,
|
|
212
|
+
userUUID: remoteUserUUID,
|
|
179
213
|
videotrack: stream
|
|
180
214
|
},
|
|
181
215
|
mediaType: "video"
|
|
182
216
|
});
|
|
217
|
+
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
socket.emit('initial-media-status', {
|
|
220
|
+
targetId: targetSocketId,
|
|
221
|
+
audio: isAudioOn(),
|
|
222
|
+
video: isCameraOn()
|
|
223
|
+
});
|
|
224
|
+
}, 100);
|
|
183
225
|
});
|
|
184
226
|
|
|
185
227
|
peer.on('error', err => {
|
|
@@ -199,7 +241,7 @@ export function toggleCamera(on) {
|
|
|
199
241
|
track.enabled = on;
|
|
200
242
|
});
|
|
201
243
|
if (socket) {
|
|
202
|
-
socket.emit('media-toggle', { userId, type: 'video', enabled: on });
|
|
244
|
+
socket.emit('media-toggle', { userId, userUUID, type: 'video', enabled: on });
|
|
203
245
|
}
|
|
204
246
|
}
|
|
205
247
|
|
|
@@ -222,7 +264,7 @@ export function toggleAudio(on) {
|
|
|
222
264
|
track.enabled = on;
|
|
223
265
|
});
|
|
224
266
|
if (socket) {
|
|
225
|
-
socket.emit('media-toggle', { userId, type: 'audio', enabled: on });
|
|
267
|
+
socket.emit('media-toggle', { userId, userUUID, type: 'audio', enabled: on });
|
|
226
268
|
}
|
|
227
269
|
}
|
|
228
270
|
|