mediasfu-shared 1.2.1 → 1.2.3

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +17 -4
  2. package/README.md +145 -131
  3. package/dist/consumers/connectIps.d.ts.map +1 -1
  4. package/dist/consumers/index.cjs +1 -1
  5. package/dist/consumers/index.js +1 -1
  6. package/dist/{getParticipantMedia-DC_Nca8S.cjs → getParticipantMedia-AwwtMCTz.cjs} +4 -4
  7. package/dist/getParticipantMedia-AwwtMCTz.cjs.map +1 -0
  8. package/dist/{getParticipantMedia-Bu_aZlxe.js → getParticipantMedia-CyaOk2oD.js} +4 -4
  9. package/dist/getParticipantMedia-CyaOk2oD.js.map +1 -0
  10. package/dist/index.cjs +36 -7
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +37 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.native.cjs +3 -3
  15. package/dist/index.native.js +3 -3
  16. package/dist/{joinLocalRoom-7ibqv7zZ.js → joinLocalRoom-Bu3gz27d.js} +2 -2
  17. package/dist/{joinLocalRoom-7ibqv7zZ.js.map → joinLocalRoom-Bu3gz27d.js.map} +1 -1
  18. package/dist/{joinLocalRoom-BJBDvTYh.cjs → joinLocalRoom-Cii5D5IG.cjs} +2 -2
  19. package/dist/{joinLocalRoom-BJBDvTYh.cjs.map → joinLocalRoom-Cii5D5IG.cjs.map} +1 -1
  20. package/dist/methods/recording/startRecording.d.ts.map +1 -1
  21. package/dist/methods/utils/headless/virtualBackground.d.ts +13 -1
  22. package/dist/methods/utils/headless/virtualBackground.d.ts.map +1 -1
  23. package/dist/methods/utils/virtualBackgroundCompositor.d.ts +4 -0
  24. package/dist/methods/utils/virtualBackgroundCompositor.d.ts.map +1 -1
  25. package/dist/sockets/SocketManager.d.ts +0 -27
  26. package/dist/sockets/SocketManager.d.ts.map +1 -1
  27. package/dist/types/index.cjs +2 -2
  28. package/dist/types/index.js +2 -2
  29. package/dist/{updateParticipantAudioDecibels-C_MygF6q.js → updateParticipantAudioDecibels-CnEXJBmc.js} +85 -13
  30. package/dist/updateParticipantAudioDecibels-CnEXJBmc.js.map +1 -0
  31. package/dist/{updateParticipantAudioDecibels-bZ6R8WHj.cjs → updateParticipantAudioDecibels-QZyahisU.cjs} +85 -13
  32. package/dist/updateParticipantAudioDecibels-QZyahisU.cjs.map +1 -0
  33. package/package.json +1 -1
  34. package/src/consumers/connectIps.ts +46 -7
  35. package/src/methods/recording/startRecording.ts +7 -1
  36. package/src/methods/utils/headless/virtualBackground.ts +34 -2
  37. package/src/methods/utils/virtualBackgroundCompositor.ts +18 -1
  38. package/src/sockets/SocketManager.ts +54 -7
  39. package/dist/getParticipantMedia-Bu_aZlxe.js.map +0 -1
  40. package/dist/getParticipantMedia-DC_Nca8S.cjs.map +0 -1
  41. package/dist/updateParticipantAudioDecibels-C_MygF6q.js.map +0 -1
  42. package/dist/updateParticipantAudioDecibels-bZ6R8WHj.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasfu-shared",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "mediasfu-shared – framework-agnostic WebRTC runtime for MediaSFU. Room helpers, mediasoup signaling, socket management, media state, and TypeScript types for React, Vue, Angular, Svelte, and plain TS.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -50,10 +50,22 @@ const normalizeConsumeEndpoint = (ip: string): string =>
50
50
  const hasConsumeEndpoint = (
51
51
  consumeSockets: ConsumeSocket[],
52
52
  endpoint: string
53
- ): boolean => consumeSockets.some((socketObj) => {
54
- const ip = Object.keys(socketObj)[0];
55
- return Boolean(ip) && normalizeConsumeEndpoint(ip) === endpoint;
56
- });
53
+ ): boolean => {
54
+ for (let index = consumeSockets.length - 1; index >= 0; index -= 1) {
55
+ const socketObj = consumeSockets[index];
56
+ const ip = Object.keys(socketObj)[0];
57
+ if (!ip || normalizeConsumeEndpoint(ip) !== endpoint) continue;
58
+
59
+ const socket = socketObj[ip];
60
+ if (socket?.connected && socket.id) return true;
61
+
62
+ socket?.removeAllListeners?.();
63
+ socket?.disconnect?.();
64
+ consumeSockets.splice(index, 1);
65
+ }
66
+
67
+ return false;
68
+ };
57
69
 
58
70
  async function reserveConsumeEndpoint(
59
71
  consumeSockets: ConsumeSocket[],
@@ -87,6 +99,9 @@ async function reserveConsumeEndpoint(
87
99
  return (didConnect = false) => {
88
100
  if (pendingForSockets?.get(endpoint) === reservation) {
89
101
  pendingForSockets.delete(endpoint);
102
+ if (pendingForSockets.size === 0) {
103
+ pendingConsumeConnections.delete(reservationOwner);
104
+ }
90
105
  }
91
106
  resolvePending(didConnect === true);
92
107
  };
@@ -170,9 +185,10 @@ export const connectIps = async ({
170
185
  if (!releaseReservation) continue;
171
186
 
172
187
  let connected = false;
188
+ let remote_sock: Awaited<ReturnType<typeof connectSocket>> | null = null;
173
189
  try {
174
190
  // Connect to the remote socket using socket.io-client
175
- const remote_sock = await connectSocket({ apiUserName, apiKey, apiToken, link: `https://${ip}.mediasfu.com` });
191
+ remote_sock = await connectSocket({ apiUserName, apiKey, apiToken, link: `https://${ip}.mediasfu.com` });
176
192
 
177
193
  if (remote_sock.id) {
178
194
  // Check if the IP is in the roomRecvIPs, if not, add it
@@ -187,7 +203,7 @@ export const connectIps = async ({
187
203
  await newProducerMethod({
188
204
  producerId,
189
205
  islevel,
190
- nsock: remote_sock,
206
+ nsock: remote_sock!,
191
207
  parameters,
192
208
  isTranslation,
193
209
  translationMeta,
@@ -211,7 +227,7 @@ export const connectIps = async ({
211
227
  parameters,
212
228
  });
213
229
  if (!data.rtpCapabilities) {
214
- return [consume_sockets, roomRecvIPs];
230
+ throw new Error(`Consume room ${ip} did not return RTP capabilities.`);
215
231
  }
216
232
  }
217
233
 
@@ -219,11 +235,34 @@ export const connectIps = async ({
219
235
  consume_sockets.push({ [ip]: remote_sock });
220
236
  updateConsume_sockets(consume_sockets);
221
237
  connected = true;
238
+
239
+ remote_sock.on("disconnect", () => {
240
+ const socketIndex = consume_sockets.findIndex(
241
+ (socketObj) => socketObj[ip] === remote_sock
242
+ );
243
+ if (socketIndex >= 0) {
244
+ consume_sockets.splice(socketIndex, 1);
245
+ updateConsume_sockets(consume_sockets);
246
+ }
247
+
248
+ const recvIndex = roomRecvIPs.indexOf(ip);
249
+ if (recvIndex >= 0) {
250
+ roomRecvIPs.splice(recvIndex, 1);
251
+ updateRoomRecvIPs(roomRecvIPs);
252
+ }
253
+
254
+ remote_sock?.removeAllListeners?.();
255
+ remote_sock?.disconnect();
256
+ });
222
257
  }
223
258
  } catch (error) {
224
259
  // Handle the error
225
260
  console.log("connectIps error", error);
226
261
  } finally {
262
+ if (!connected && remote_sock) {
263
+ remote_sock.removeAllListeners?.();
264
+ remote_sock.disconnect();
265
+ }
227
266
  releaseReservation(connected);
228
267
  }
229
268
  }
@@ -222,7 +222,13 @@ export const startRecording: StartRecordingType = async ({
222
222
  updateRecordPaused(recordPaused)
223
223
 
224
224
  if (action === 'startRecord') {
225
- await rePort({ parameters: parameters.getUpdatedAllParams() })
225
+ // restart: force an immediate updateScreenClient. Without it,
226
+ // rePort only emits when activeNames or screenStates differ from
227
+ // their previous values — and nothing has changed at the moment a
228
+ // recording starts, so the recorder got no layout until something
229
+ // later did (a speaker change, a join, a share). Resume already
230
+ // forced it; start now matches.
231
+ await rePort({ restart: true, parameters: parameters.getUpdatedAllParams() })
226
232
  await recordStartTimer({ parameters })
227
233
  } else {
228
234
  updateRecordResumed(true)
@@ -3,7 +3,10 @@ import { HeadlessOptions } from './headlessTypes';
3
3
  import { getCurrentParams } from './getCurrentParams';
4
4
  import { getLocalVideoStream } from './getMediaStreams';
5
5
  import { HeadlessActionResult } from './roomActions';
6
- import { compositeVirtualBackgroundFrame } from '../virtualBackgroundCompositor';
6
+ import {
7
+ compositeVirtualBackgroundFrame,
8
+ DEFAULT_BACKGROUND_BLUR_PIXELS,
9
+ } from '../virtualBackgroundCompositor';
7
10
 
8
11
  const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation';
9
12
 
@@ -57,6 +60,11 @@ export interface ApplyVirtualBackgroundOptions extends HeadlessOptions {
57
60
  * A trailing slash is added if missing.
58
61
  */
59
62
  assetPath?: string;
63
+ /**
64
+ * Blur the area behind the segmented person by this many pixels. When this
65
+ * is greater than zero it takes precedence over `image`. Defaults to 0.
66
+ */
67
+ blurPixels?: number;
60
68
  }
61
69
 
62
70
  export type ApplyVirtualBackgroundType = (
@@ -98,6 +106,7 @@ export async function applyVirtualBackground({
98
106
  modelSelection = 1,
99
107
  publish = true,
100
108
  assetPath = MEDIAPIPE_CDN,
109
+ blurPixels = 0,
101
110
  }: ApplyVirtualBackgroundOptions): Promise<HeadlessActionResult & { stream: MediaStream | null }> {
102
111
  const live = getCurrentParams({ parameters });
103
112
  if (live.audioOnlyRoom) {
@@ -124,7 +133,10 @@ export async function applyVirtualBackground({
124
133
  const width = Number(settings.width) || 640;
125
134
  const height = Number(settings.height) || 360;
126
135
 
127
- const backgroundImage = typeof image === 'string' ? await loadImage(image) : image;
136
+ const resolvedBlurPixels = Math.max(0, Number(blurPixels) || 0);
137
+ const backgroundImage = resolvedBlurPixels > 0
138
+ ? null
139
+ : (typeof image === 'string' ? await loadImage(image) : image);
128
140
  const producer = publish ? (live.videoProducer || live.localVideoProducer) : null;
129
141
 
130
142
  // mediasoup Producers normally own their track (`stopTracks: true`). Their
@@ -169,6 +181,7 @@ export async function applyVirtualBackground({
169
181
  backgroundImage: backgroundImage as CanvasImageSource | null,
170
182
  width: canvas.width,
171
183
  height: canvas.height,
184
+ blurFallbackPixels: resolvedBlurPixels,
172
185
  });
173
186
  } catch {
174
187
  // A dropped frame must never kill the loop.
@@ -236,6 +249,25 @@ export async function applyVirtualBackground({
236
249
  }
237
250
  }
238
251
 
252
+ export interface ApplyBackgroundBlurOptions extends Omit<ApplyVirtualBackgroundOptions, 'image'> {
253
+ /** Blur strength in CSS pixels. Defaults to 16. */
254
+ blurPixels?: number;
255
+ }
256
+
257
+ /** Apply person-aware background blur through the standard processed-track lifecycle. */
258
+ export function applyBackgroundBlur({
259
+ blurPixels = DEFAULT_BACKGROUND_BLUR_PIXELS,
260
+ ...options
261
+ }: ApplyBackgroundBlurOptions): ReturnType<ApplyVirtualBackgroundType> {
262
+ return applyVirtualBackground({
263
+ ...options,
264
+ image: null,
265
+ blurPixels,
266
+ });
267
+ }
268
+
269
+ export type ApplyBackgroundBlurType = typeof applyBackgroundBlur;
270
+
239
271
  export type ClearVirtualBackgroundType = (
240
272
  options: HeadlessOptions
241
273
  ) => Promise<HeadlessActionResult>;
@@ -10,6 +10,14 @@ export interface CompositeVirtualBackgroundFrameOptions {
10
10
  blurFallbackPixels?: number;
11
11
  }
12
12
 
13
+ /** Stable room-state value used by every web UI surface for background blur. */
14
+ export const VIRTUAL_BACKGROUND_BLUR = 'blur';
15
+ export const DEFAULT_BACKGROUND_BLUR_PIXELS = 16;
16
+
17
+ export function isVirtualBackgroundBlur(value: unknown): boolean {
18
+ return value === VIRTUAL_BACKGROUND_BLUR;
19
+ }
20
+
13
21
  /**
14
22
  * Paint the segmented person over a replacement background.
15
23
  *
@@ -43,7 +51,16 @@ export function compositeVirtualBackgroundFrame({
43
51
  ctx.fillRect(0, 0, width, height);
44
52
  } else if (blurFallbackPixels > 0) {
45
53
  ctx.filter = `blur(${blurFallbackPixels}px)`;
46
- ctx.drawImage(sourceImage, 0, 0, width, height);
54
+ // Draw beyond the visible bounds so the blur kernel does not expose a
55
+ // transparent/black fringe around the processed frame.
56
+ const bleed = Math.max(2, blurFallbackPixels * 2);
57
+ ctx.drawImage(
58
+ sourceImage,
59
+ -bleed,
60
+ -bleed,
61
+ width + bleed * 2,
62
+ height + bleed * 2,
63
+ );
47
64
  }
48
65
  } finally {
49
66
  ctx.filter = previousFilter || 'none';
@@ -80,6 +80,9 @@ export type ConnectLocalSocketType = (options: ConnectLocalSocketOptions) => Pro
80
80
  * ```
81
81
  */
82
82
 
83
+ /** How long to wait for `connection-success` before giving up. */
84
+ const CONNECT_TIMEOUT_MS = 12000;
85
+
83
86
  async function connectSocket(
84
87
  { apiUserName, apiKey, apiToken, link }: ConnectSocketOptions,
85
88
  ): Promise<Socket> {
@@ -132,8 +135,24 @@ async function connectSocket(
132
135
  });
133
136
  }
134
137
 
135
- // Handle socket connection events
136
- socket.on('connection-success', ({ socketId }: { socketId: string }) => {
138
+ // Dispose failed handshakes immediately. Otherwise Socket.IO continues
139
+ // reconnecting a client that was never handed to the room lifecycle.
140
+ let settled = false;
141
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
142
+ const cleanupHandshake = () => {
143
+ if (timeoutHandle) clearTimeout(timeoutHandle);
144
+ socket.off('connection-success', handleConnectionSuccess);
145
+ socket.off('connect_error', handleConnectError);
146
+ socket.off('disconnect', handleDisconnectBeforeReady);
147
+ };
148
+ const settle = (action: () => void) => {
149
+ if (settled) return;
150
+ settled = true;
151
+ cleanupHandshake();
152
+ action();
153
+ };
154
+
155
+ const handleConnectionSuccess = ({ socketId }: { socketId: string }) => {
137
156
  //check if link contains mediasfu.com and contains more than one c
138
157
  let conn = 'media';
139
158
  try {
@@ -145,12 +164,40 @@ async function connectSocket(
145
164
  }
146
165
 
147
166
  console.log(`Connected to ${conn} socket with ID: ${socketId}`);
148
- resolve(socket);
149
- });
167
+ settle(() => resolve(socket));
168
+ };
150
169
 
151
- socket.on('connect_error', (error: Error) => {
152
- reject(new Error('Error connecting to media socket: ' + error.message));
153
- });
170
+ const handleConnectError = (error: Error) => {
171
+ settle(() => {
172
+ socket.disconnect();
173
+ reject(new Error('Error connecting to media socket: ' + error.message));
174
+ });
175
+ };
176
+
177
+ const handleDisconnectBeforeReady = (reason: string) => {
178
+ settle(() =>
179
+ reject(
180
+ new Error(
181
+ 'Media socket closed before the room was ready (' + reason + ').'
182
+ )
183
+ )
184
+ );
185
+ };
186
+
187
+ socket.on('connection-success', handleConnectionSuccess);
188
+ socket.on('connect_error', handleConnectError);
189
+ socket.on('disconnect', handleDisconnectBeforeReady);
190
+
191
+ timeoutHandle = setTimeout(() => {
192
+ settle(() => {
193
+ try {
194
+ socket.disconnect();
195
+ } catch {
196
+ // Nothing useful to do here.
197
+ }
198
+ reject(new Error('Timed out connecting to media socket.'));
199
+ });
200
+ }, CONNECT_TIMEOUT_MS);
154
201
  });
155
202
  }
156
203