@anonotf/connect 0.3.1 → 0.4.1

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 CHANGED
@@ -141,4 +141,4 @@ recorder.onstop = async () => {
141
141
 
142
142
  ```js
143
143
  client.disconnect();
144
- ```
144
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anonotf/connect",
3
- "version": "0.3.1",
4
- "description": "Client SDK for AnonOtF — calls, group calls, live streaming, chat, and voice notes.",
3
+ "version": "0.4.1",
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
@@ -35,6 +35,8 @@ export class Calls {
35
35
  this._localStream = null;
36
36
  this.roomId = null; // set once a call is accepted/joined — null when idle
37
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
38
40
 
39
41
  this._socket.on('incoming-call', (data) => this._emit('incomingCall', data));
40
42
  this._socket.on('call-accepted', (data) => this._onCallAccepted(data));
@@ -99,7 +101,12 @@ export class Calls {
99
101
  end() {
100
102
  this._socket.emit('end-call', { otherParty: this._anyPeerId() });
101
103
  this._cleanupAll();
102
- this._emit('callEnded', { from: 'self' });
104
+ // Deliberately does NOT emit 'callEnded' here you already know
105
+ // the call ended, you just called end(). Firing it anyway created
106
+ // an infinite loop for any consumer whose 'callEnded' handler
107
+ // reasonably calls end()/cleanup again (the natural thing to do).
108
+ // 'callEnded' is for when the call ends from elsewhere — the
109
+ // other party hanging up, a peer connection failing, etc.
103
110
  }
104
111
 
105
112
  /** Notify everyone in the call that you've started/stopped a local recording. */
@@ -140,6 +147,61 @@ export class Calls {
140
147
  }
141
148
  }
142
149
 
150
+ /**
151
+ * Starts sharing your screen instead of your camera — swaps the
152
+ * outgoing video track on every peer connection in the mesh, same
153
+ * mechanism as camera flip. Remembers whether you had a camera
154
+ * track running so stopScreenShare() can restore it afterward.
155
+ * Audio is untouched either way.
156
+ *
157
+ * Fires 'screenShareStarted'/'screenShareEnded' — the latter also
158
+ * fires automatically if the user stops sharing from the browser's
159
+ * own "Stop sharing" control rather than your UI.
160
+ */
161
+ async startScreenShare() {
162
+ if (this._screenTrack) return; // already sharing
163
+ const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
164
+ const screenTrack = displayStream.getVideoTracks()[0];
165
+
166
+ this._hadCameraBeforeShare = !!this._localStream?.getVideoTracks().length;
167
+ this._screenTrack = screenTrack;
168
+
169
+ await this.replaceVideoTrack(screenTrack);
170
+ this._emit('screenShareStarted');
171
+
172
+ // Browser's native "Stop sharing" UI ends the track directly —
173
+ // this catches that case so state stays accurate even if the
174
+ // person never calls stopScreenShare() themselves.
175
+ screenTrack.onended = () => {
176
+ if (this._screenTrack === screenTrack) this.stopScreenShare();
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Stops screen sharing and restores your camera (if you had one
182
+ * running before sharing started) across the whole mesh.
183
+ */
184
+ async stopScreenShare() {
185
+ if (!this._screenTrack) return;
186
+ this._screenTrack.stop();
187
+ this._screenTrack = null;
188
+
189
+ if (this._hadCameraBeforeShare) {
190
+ const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
191
+ await this.replaceVideoTrack(cameraStream.getVideoTracks()[0]);
192
+ } else if (this._localStream) {
193
+ // Wasn't sending video before sharing (audio-only call) — go
194
+ // back to that, rather than silently turning video on.
195
+ const track = this._localStream.getVideoTracks()[0];
196
+ if (track) { this._localStream.removeTrack(track); track.stop(); }
197
+ }
198
+ this._emit('screenShareEnded');
199
+ }
200
+
201
+ get isScreenSharing() {
202
+ return !!this._screenTrack;
203
+ }
204
+
143
205
  /** Mute/unmute your own mic across the whole mesh — just disables the local track, no renegotiation needed. */
144
206
  setMicEnabled(enabled) {
145
207
  this._localStream?.getAudioTracks().forEach((t) => { t.enabled = enabled; });
@@ -285,6 +347,8 @@ export class Calls {
285
347
  _cleanupAll() {
286
348
  this._localStream?.getTracks().forEach((t) => t.stop());
287
349
  this._localStream = null;
350
+ this._screenTrack?.stop();
351
+ this._screenTrack = null;
288
352
  for (const pc of this._peers.values()) pc.close();
289
353
  this._peers.clear();
290
354
  this.roomId = null;