@anonotf/connect 0.1.0 → 0.4.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 +27 -8
- package/package.json +2 -2
- package/src/calls.js +204 -45
- package/src/index.js +19 -0
- package/src/rooms.js +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @anonotf/connect
|
|
2
2
|
|
|
3
|
-
Client SDK for AnonOtF — handles calls
|
|
3
|
+
Client SDK for AnonOtF — handles calls (1-on-1 and group, up to 9 people),
|
|
4
|
+
live streaming to many viewers, live chat, and voice notes/recordings, without struggling with raw webrtc.
|
|
5
|
+
|
|
6
|
+
> **v0.2.0 — breaking change:** group calls are now a mesh (everyone
|
|
7
|
+
> connects to everyone). `calls.on('remoteStream', ...)` now receives
|
|
8
|
+
> `{ userId, stream }` instead of a bare `stream` — update any v0.1.x
|
|
9
|
+
> integration accordingly. See the Calls section below.
|
|
4
10
|
|
|
5
11
|
## Install
|
|
6
12
|
|
|
@@ -13,7 +19,7 @@ npm install @anonotf/connect
|
|
|
13
19
|
This SDK runs **in the browser**. Two things must never end up there:
|
|
14
20
|
|
|
15
21
|
- Your AnonOtF **x-api-key** (it can create/delete apps, read all data)
|
|
16
|
-
- Your **API secret**
|
|
22
|
+
- Your **Your API secret**
|
|
17
23
|
|
|
18
24
|
So your own backend needs two small pieces:
|
|
19
25
|
|
|
@@ -45,15 +51,22 @@ client.connect();
|
|
|
45
51
|
await client.register({ name: 'Alex' });
|
|
46
52
|
```
|
|
47
53
|
|
|
48
|
-
## Calls (1-on-1, grows to group)
|
|
54
|
+
## Calls (1-on-1, grows to real group calls — full mesh)
|
|
49
55
|
|
|
50
56
|
Every accepted call gets a room behind the scenes — that's what lets you
|
|
51
|
-
add people later without restarting the call.
|
|
57
|
+
add people later without restarting the call. For 2+ people this is a
|
|
58
|
+
genuine mesh: everyone gets a separate peer connection to everyone else,
|
|
59
|
+
so everyone sees/hears everyone, the same way WhatsApp/Telegram group
|
|
60
|
+
calls behave.
|
|
52
61
|
|
|
53
62
|
```js
|
|
54
63
|
// Caller
|
|
55
64
|
client.calls.on('callAccepted', ({ roomId }) => console.log('connected', roomId));
|
|
56
|
-
client.calls.on('remoteStream', (
|
|
65
|
+
client.calls.on('remoteStream', ({ userId, stream }) => {
|
|
66
|
+
// one event per remote participant — render a tile per userId
|
|
67
|
+
renderVideoTile(userId, stream);
|
|
68
|
+
});
|
|
69
|
+
client.calls.on('peerLeft', ({ userId }) => removeVideoTile(userId));
|
|
57
70
|
await client.calls.call('user_456', { callType: 'video' });
|
|
58
71
|
|
|
59
72
|
// Callee
|
|
@@ -62,11 +75,17 @@ client.calls.on('incomingCall', async ({ from, callType, roomId }) => {
|
|
|
62
75
|
});
|
|
63
76
|
|
|
64
77
|
// Mid-call — ring a third person into the SAME call (cap: 9 total)
|
|
78
|
+
// Everyone already in the call automatically meshes with the newcomer —
|
|
79
|
+
// nothing else to wire up.
|
|
65
80
|
client.calls.addToCall('user_789', { callType: 'video' });
|
|
66
81
|
|
|
67
|
-
|
|
82
|
+
// Ends the call for yourself — closes every peer connection in your mesh
|
|
83
|
+
client.calls.end();
|
|
68
84
|
```
|
|
69
85
|
|
|
86
|
+
`client.calls.participantIds` gives you the current list of remote user
|
|
87
|
+
IDs in the mesh at any time, if you need to render an initial roster.
|
|
88
|
+
|
|
70
89
|
## Live streaming (broadcast to many viewers)
|
|
71
90
|
|
|
72
91
|
Whether you can publish (vs. just watch) is decided by
|
|
@@ -88,7 +107,7 @@ await client.streaming.leave();
|
|
|
88
107
|
|
|
89
108
|
## Live chat + raise hand
|
|
90
109
|
|
|
91
|
-
Ephemeral —
|
|
110
|
+
Ephemeral — Everything is Live, nothing is saved server-side.
|
|
92
111
|
|
|
93
112
|
```js
|
|
94
113
|
client.chat.on('message', ({ fromUserId, text }) => renderMessage(fromUserId, text));
|
|
@@ -122,4 +141,4 @@ recorder.onstop = async () => {
|
|
|
122
141
|
|
|
123
142
|
```js
|
|
124
143
|
client.disconnect();
|
|
125
|
-
```
|
|
144
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonotf/connect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Client SDK for AnonOtF — calls, group calls, live streaming, chat, and voice notes.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Client SDK for AnonOtF — calls, group calls, live streaming, chat, and voice notes, without touching raw WebRTC or Socket.IO directly.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": ["src"],
|
package/src/calls.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// src/calls.js
|
|
2
2
|
//
|
|
3
|
-
// Wraps
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// candidate themselves
|
|
7
|
-
// and listen for simple events like 'remoteStream'.
|
|
3
|
+
// Wraps raw WebRTC + the server's call-user / accept-call / signal /
|
|
4
|
+
// add-to-call / join-room socket events. A developer using this
|
|
5
|
+
// module never creates an RTCPeerConnection or handles an ICE
|
|
6
|
+
// candidate themselves.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// .
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// MESH MODEL: for 2+ people, this is a full mesh — one separate
|
|
9
|
+
// RTCPeerConnection per remote participant, tracked by userId, not a
|
|
10
|
+
// single shared connection. That's what lets everyone see/hear
|
|
11
|
+
// everyone in a group call: N participants means N-1 peer
|
|
12
|
+
// connections per device.
|
|
13
|
+
//
|
|
14
|
+
// Every accepted call gets a backing room on the server — that's
|
|
15
|
+
// what makes .addToCall() possible later without restarting anything.
|
|
14
16
|
|
|
15
17
|
const RTC_CONFIG_DEFAULTS = {
|
|
16
18
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], // overridden by fetchIceServers() below when available
|
|
@@ -28,26 +30,34 @@ export class Calls {
|
|
|
28
30
|
constructor(socketManager, { fetchIceServers } = {}) {
|
|
29
31
|
this._socket = socketManager;
|
|
30
32
|
this._fetchIceServers = fetchIceServers;
|
|
31
|
-
this._listeners = new Map();
|
|
32
|
-
this.
|
|
33
|
+
this._listeners = new Map(); // event -> Set<handler>
|
|
34
|
+
this._peers = new Map(); // userId -> RTCPeerConnection, one per remote participant
|
|
33
35
|
this._localStream = null;
|
|
34
|
-
this.roomId = null;
|
|
35
|
-
this.
|
|
36
|
+
this.roomId = null; // set once a call is accepted/joined — null when idle
|
|
37
|
+
this._inCall = false; // true from accept/connect onward, used to decide whether to mesh-connect to newcomers
|
|
38
|
+
this._screenTrack = null; // active screen-capture track, if currently sharing
|
|
39
|
+
this._hadCameraBeforeShare = false; // so stopScreenShare() knows whether to restore the camera or just go video-off
|
|
36
40
|
|
|
37
41
|
this._socket.on('incoming-call', (data) => this._emit('incomingCall', data));
|
|
38
42
|
this._socket.on('call-accepted', (data) => this._onCallAccepted(data));
|
|
39
43
|
this._socket.on('call-declined', (data) => this._emit('callDeclined', data));
|
|
40
44
|
this._socket.on('call-cancelled', (data) => this._emit('callCancelled', data));
|
|
41
|
-
this._socket.on('call-ended', (data) => this.
|
|
45
|
+
this._socket.on('call-ended', (data) => this._onCallEnded(data));
|
|
42
46
|
this._socket.on('call-error', (data) => this._emit('callError', data));
|
|
43
47
|
this._socket.on('signal', (payload) => this._onSignal(payload));
|
|
48
|
+
|
|
49
|
+
// Mesh growth/shrink — fired by the room itself (rooms.js also
|
|
50
|
+
// listens to these for its own purposes; both can coexist since
|
|
51
|
+
// socket.on supports multiple handlers per event).
|
|
52
|
+
this._socket.on('user-joined', (data) => this._onUserJoined(data));
|
|
53
|
+
this._socket.on('user-left', (data) => this._onUserLeft(data));
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// ---- public event subscription ----
|
|
47
57
|
on(event, handler) {
|
|
48
58
|
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
|
49
59
|
this._listeners.get(event).add(handler);
|
|
50
|
-
return () => this._listeners.get(event)?.delete(handler);
|
|
60
|
+
return () => this._listeners.get(event)?.delete(handler);
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
_emit(event, data) {
|
|
@@ -67,7 +77,7 @@ export class Calls {
|
|
|
67
77
|
|
|
68
78
|
cancelCall(calleeId) {
|
|
69
79
|
this._socket.emit('call-cancelled', { calleeId });
|
|
70
|
-
this.
|
|
80
|
+
this._cleanupAll();
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// ---- incoming call ----
|
|
@@ -87,9 +97,11 @@ export class Calls {
|
|
|
87
97
|
this._socket.emit('add-to-call', { roomId: this.roomId, newUserId, callType, callerName });
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
100
|
+
/** Ends the call for yourself — closes every peer connection in the mesh, notifies the room. */
|
|
101
|
+
end() {
|
|
102
|
+
this._socket.emit('end-call', { otherParty: this._anyPeerId() });
|
|
103
|
+
this._cleanupAll();
|
|
104
|
+
this._emit('callEnded', { from: 'self' });
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
/** Notify everyone in the call that you've started/stopped a local recording. */
|
|
@@ -104,7 +116,102 @@ export class Calls {
|
|
|
104
116
|
return this._localStream;
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
/** Current remote participant user IDs in the mesh. */
|
|
120
|
+
get participantIds() {
|
|
121
|
+
return [...this._peers.keys()];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Swaps the outgoing video track on EVERY peer connection in the
|
|
126
|
+
* mesh at once — for camera flip (front/back), or any other
|
|
127
|
+
* "swap what I'm sending" need. Also updates localStream so your
|
|
128
|
+
* own preview reflects the new track. Does nothing to audio.
|
|
129
|
+
*/
|
|
130
|
+
async replaceVideoTrack(newTrack) {
|
|
131
|
+
for (const pc of this._peers.values()) {
|
|
132
|
+
const sender = pc.getSenders().find((s) => s.track && s.track.kind === 'video');
|
|
133
|
+
if (sender) await sender.replaceTrack(newTrack);
|
|
134
|
+
}
|
|
135
|
+
if (this._localStream) {
|
|
136
|
+
const oldTrack = this._localStream.getVideoTracks()[0];
|
|
137
|
+
if (oldTrack) {
|
|
138
|
+
this._localStream.removeTrack(oldTrack);
|
|
139
|
+
oldTrack.stop();
|
|
140
|
+
}
|
|
141
|
+
this._localStream.addTrack(newTrack);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Starts sharing your screen instead of your camera — swaps the
|
|
147
|
+
* outgoing video track on every peer connection in the mesh, same
|
|
148
|
+
* mechanism as camera flip. Remembers whether you had a camera
|
|
149
|
+
* track running so stopScreenShare() can restore it afterward.
|
|
150
|
+
* Audio is untouched either way.
|
|
151
|
+
*
|
|
152
|
+
* Fires 'screenShareStarted'/'screenShareEnded' — the latter also
|
|
153
|
+
* fires automatically if the user stops sharing from the browser's
|
|
154
|
+
* own "Stop sharing" control rather than your UI.
|
|
155
|
+
*/
|
|
156
|
+
async startScreenShare() {
|
|
157
|
+
if (this._screenTrack) return; // already sharing
|
|
158
|
+
const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
159
|
+
const screenTrack = displayStream.getVideoTracks()[0];
|
|
160
|
+
|
|
161
|
+
this._hadCameraBeforeShare = !!this._localStream?.getVideoTracks().length;
|
|
162
|
+
this._screenTrack = screenTrack;
|
|
163
|
+
|
|
164
|
+
await this.replaceVideoTrack(screenTrack);
|
|
165
|
+
this._emit('screenShareStarted');
|
|
166
|
+
|
|
167
|
+
// Browser's native "Stop sharing" UI ends the track directly —
|
|
168
|
+
// this catches that case so state stays accurate even if the
|
|
169
|
+
// person never calls stopScreenShare() themselves.
|
|
170
|
+
screenTrack.onended = () => {
|
|
171
|
+
if (this._screenTrack === screenTrack) this.stopScreenShare();
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Stops screen sharing and restores your camera (if you had one
|
|
177
|
+
* running before sharing started) across the whole mesh.
|
|
178
|
+
*/
|
|
179
|
+
async stopScreenShare() {
|
|
180
|
+
if (!this._screenTrack) return;
|
|
181
|
+
this._screenTrack.stop();
|
|
182
|
+
this._screenTrack = null;
|
|
183
|
+
|
|
184
|
+
if (this._hadCameraBeforeShare) {
|
|
185
|
+
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
186
|
+
await this.replaceVideoTrack(cameraStream.getVideoTracks()[0]);
|
|
187
|
+
} else if (this._localStream) {
|
|
188
|
+
// Wasn't sending video before sharing (audio-only call) — go
|
|
189
|
+
// back to that, rather than silently turning video on.
|
|
190
|
+
const track = this._localStream.getVideoTracks()[0];
|
|
191
|
+
if (track) { this._localStream.removeTrack(track); track.stop(); }
|
|
192
|
+
}
|
|
193
|
+
this._emit('screenShareEnded');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get isScreenSharing() {
|
|
197
|
+
return !!this._screenTrack;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Mute/unmute your own mic across the whole mesh — just disables the local track, no renegotiation needed. */
|
|
201
|
+
setMicEnabled(enabled) {
|
|
202
|
+
this._localStream?.getAudioTracks().forEach((t) => { t.enabled = enabled; });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Pause/resume your own camera across the whole mesh — same idea as setMicEnabled. */
|
|
206
|
+
setCameraEnabled(enabled) {
|
|
207
|
+
this._localStream?.getVideoTracks().forEach((t) => { t.enabled = enabled; });
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
// ---- internals ----
|
|
211
|
+
_anyPeerId() {
|
|
212
|
+
return this._peers.keys().next().value;
|
|
213
|
+
}
|
|
214
|
+
|
|
108
215
|
async _ensureLocalStream(callType) {
|
|
109
216
|
if (this._localStream) return this._localStream;
|
|
110
217
|
const constraints = callType === 'video' ? { audio: true, video: true } : { audio: true, video: false };
|
|
@@ -113,58 +220,112 @@ export class Calls {
|
|
|
113
220
|
return this._localStream;
|
|
114
221
|
}
|
|
115
222
|
|
|
116
|
-
|
|
117
|
-
|
|
223
|
+
// Creates (or returns the existing) peer connection for one specific
|
|
224
|
+
// remote user. Every track/ICE/connection-state handler is scoped to
|
|
225
|
+
// that userId, so multi-party events (remoteStream, peerLeft) always
|
|
226
|
+
// tell you WHICH participant they're about.
|
|
227
|
+
async _ensurePeerFor(remoteUserId) {
|
|
228
|
+
let pc = this._peers.get(remoteUserId);
|
|
229
|
+
if (pc) return pc;
|
|
118
230
|
|
|
119
231
|
const config = this._fetchIceServers ? await this._fetchIceServers() : RTC_CONFIG_DEFAULTS;
|
|
120
|
-
|
|
232
|
+
pc = new RTCPeerConnection(config);
|
|
233
|
+
this._peers.set(remoteUserId, pc);
|
|
121
234
|
|
|
122
|
-
this._localStream?.getTracks().forEach((track) =>
|
|
235
|
+
this._localStream?.getTracks().forEach((track) => pc.addTrack(track, this._localStream));
|
|
123
236
|
|
|
124
|
-
|
|
237
|
+
pc.onicecandidate = (event) => {
|
|
125
238
|
if (event.candidate) {
|
|
126
239
|
this._socket.emit('signal', {
|
|
127
240
|
type: 'ice-candidate',
|
|
128
241
|
candidate: event.candidate,
|
|
129
242
|
roomId: this.roomId,
|
|
243
|
+
to: remoteUserId,
|
|
130
244
|
});
|
|
131
245
|
}
|
|
132
246
|
};
|
|
133
247
|
|
|
134
|
-
|
|
135
|
-
this._emit('remoteStream', event.streams[0]);
|
|
248
|
+
pc.ontrack = (event) => {
|
|
249
|
+
this._emit('remoteStream', { userId: remoteUserId, stream: event.streams[0] });
|
|
136
250
|
};
|
|
137
251
|
|
|
138
|
-
|
|
139
|
-
this._emit('
|
|
140
|
-
if (
|
|
141
|
-
this.
|
|
252
|
+
pc.onconnectionstatechange = () => {
|
|
253
|
+
this._emit('peerConnectionStateChange', { userId: remoteUserId, state: pc.connectionState });
|
|
254
|
+
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
255
|
+
this._removePeer(remoteUserId);
|
|
142
256
|
}
|
|
143
257
|
};
|
|
144
258
|
|
|
145
|
-
return
|
|
259
|
+
return pc;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_removePeer(remoteUserId) {
|
|
263
|
+
const pc = this._peers.get(remoteUserId);
|
|
264
|
+
if (!pc) return;
|
|
265
|
+
pc.close();
|
|
266
|
+
this._peers.delete(remoteUserId);
|
|
267
|
+
this._emit('peerLeft', { userId: remoteUserId });
|
|
268
|
+
// Mesh of one (just you) left in the room — the call is effectively over.
|
|
269
|
+
if (this._peers.size === 0 && this._inCall) {
|
|
270
|
+
this._cleanupAll();
|
|
271
|
+
this._emit('callEnded', { reason: 'all-peers-left' });
|
|
272
|
+
}
|
|
146
273
|
}
|
|
147
274
|
|
|
275
|
+
// The ORIGINAL caller, once the callee accepts: send them the first offer.
|
|
148
276
|
async _onCallAccepted({ from, roomId, callType }) {
|
|
149
277
|
this.roomId = roomId;
|
|
150
|
-
|
|
278
|
+
this._inCall = true;
|
|
279
|
+
await this._sendOfferTo(from);
|
|
280
|
+
this._emit('callAccepted', { from, roomId, callType });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async _sendOfferTo(remoteUserId) {
|
|
284
|
+
const pc = await this._ensurePeerFor(remoteUserId);
|
|
151
285
|
const offer = await pc.createOffer();
|
|
152
286
|
await pc.setLocalDescription(offer);
|
|
153
|
-
this._socket.emit('signal', { type: 'offer', sdp: offer, roomId
|
|
154
|
-
|
|
287
|
+
this._socket.emit('signal', { type: 'offer', sdp: offer, roomId: this.roomId, to: remoteUserId });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Mesh growth: someone new joined the room we're actively calling
|
|
291
|
+
// in. Whoever was already in the call proactively offers to the
|
|
292
|
+
// newcomer — the newcomer doesn't need to do anything special,
|
|
293
|
+
// they'll just receive offers from everyone already present.
|
|
294
|
+
async _onUserJoined({ userId }) {
|
|
295
|
+
if (!this._inCall || !this.roomId || this._peers.has(userId)) return;
|
|
296
|
+
this._inCall = true;
|
|
297
|
+
await this._sendOfferTo(userId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_onUserLeft({ userId }) {
|
|
301
|
+
this._removePeer(userId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_onCallEnded(data) {
|
|
305
|
+
// A specific peer telling us they've ended — close just that
|
|
306
|
+
// connection; the mesh (and call) only fully tears down once
|
|
307
|
+
// everyone's gone (handled in _removePeer).
|
|
308
|
+
if (data?.from && this._peers.has(data.from)) {
|
|
309
|
+
this._removePeer(data.from);
|
|
310
|
+
} else {
|
|
311
|
+
this._cleanupAll();
|
|
312
|
+
this._emit('callEnded', data);
|
|
313
|
+
}
|
|
155
314
|
}
|
|
156
315
|
|
|
157
316
|
async _onSignal(payload) {
|
|
158
317
|
const { type, sdp, candidate, from, roomId } = payload;
|
|
159
318
|
if (roomId) this.roomId = roomId;
|
|
319
|
+
if (!from) return; // shouldn't happen — server always stamps `from`
|
|
160
320
|
|
|
161
|
-
const pc = await this.
|
|
321
|
+
const pc = await this._ensurePeerFor(from);
|
|
322
|
+
this._inCall = true;
|
|
162
323
|
|
|
163
324
|
if (type === 'offer') {
|
|
164
325
|
await pc.setRemoteDescription(sdp);
|
|
165
326
|
const answer = await pc.createAnswer();
|
|
166
327
|
await pc.setLocalDescription(answer);
|
|
167
|
-
this._socket.emit('signal', { type: 'answer', sdp: answer, roomId
|
|
328
|
+
this._socket.emit('signal', { type: 'answer', sdp: answer, roomId: this.roomId, to: from });
|
|
168
329
|
} else if (type === 'answer') {
|
|
169
330
|
await pc.setRemoteDescription(sdp);
|
|
170
331
|
} else if (type === 'ice-candidate' && candidate) {
|
|
@@ -178,16 +339,14 @@ export class Calls {
|
|
|
178
339
|
}
|
|
179
340
|
}
|
|
180
341
|
|
|
181
|
-
|
|
342
|
+
_cleanupAll() {
|
|
182
343
|
this._localStream?.getTracks().forEach((t) => t.stop());
|
|
183
344
|
this._localStream = null;
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
345
|
+
this._screenTrack?.stop();
|
|
346
|
+
this._screenTrack = null;
|
|
347
|
+
for (const pc of this._peers.values()) pc.close();
|
|
348
|
+
this._peers.clear();
|
|
186
349
|
this.roomId = null;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
_teardown(event, data) {
|
|
190
|
-
this._cleanupLocal();
|
|
191
|
-
this._emit(event, data);
|
|
350
|
+
this._inCall = false;
|
|
192
351
|
}
|
|
193
352
|
}
|
package/src/index.js
CHANGED
|
@@ -73,6 +73,25 @@ export class AnonOtFConnect {
|
|
|
73
73
|
heartbeat() {
|
|
74
74
|
this._socketManager.emit('heartbeat');
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Presence events, scoped to the whole connection (not any one
|
|
79
|
+
* call/room) — who's online, who went offline, profile changes.
|
|
80
|
+
* Events: 'onlineUsersUpdate' ({userId,name,photo,emoji,about,online}[]),
|
|
81
|
+
* 'profileUpdated' ({userId,name,photo,emoji,about}),
|
|
82
|
+
* 'userOffline' ({userId,lastSeen}).
|
|
83
|
+
*/
|
|
84
|
+
on(event, handler) {
|
|
85
|
+
const serverEventMap = {
|
|
86
|
+
onlineUsersUpdate: 'online-users-update',
|
|
87
|
+
profileUpdated: 'profile-updated',
|
|
88
|
+
userOffline: 'user-offline',
|
|
89
|
+
};
|
|
90
|
+
const serverEvent = serverEventMap[event];
|
|
91
|
+
if (!serverEvent) throw new Error(`[AnonOtFConnect] Unknown event "${event}"`);
|
|
92
|
+
this._socketManager.on(serverEvent, handler);
|
|
93
|
+
return () => this._socketManager.off(serverEvent, handler);
|
|
94
|
+
}
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
export { Calls, Rooms, Chat, Streaming, MediaClips };
|
package/src/rooms.js
CHANGED
|
@@ -47,11 +47,11 @@ export class Rooms {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Create a new room. Goes through your backend proxy, not directly to AnonOtF, to keep the API key server-side. */
|
|
50
|
-
async create({ roomName, maxParticipants } = {}) {
|
|
50
|
+
async create({ roomName, maxParticipants, userId } = {}) {
|
|
51
51
|
const res = await fetch(`${this._apiBase}/rooms`, {
|
|
52
52
|
method: 'POST',
|
|
53
53
|
headers: { 'Content-Type': 'application/json' },
|
|
54
|
-
body: JSON.stringify({ roomName, maxParticipants }),
|
|
54
|
+
body: JSON.stringify({ roomName, maxParticipants, userId }),
|
|
55
55
|
});
|
|
56
56
|
if (!res.ok) throw new Error(`[AnonOtFConnect] Failed to create room: ${res.status}`);
|
|
57
57
|
return res.json();
|